@asteby/metacore-runtime-react 25.0.0 → 25.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +10 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/server-error.d.ts +47 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +100 -0
- package/package.json +1 -1
- package/src/__tests__/server-error.test.ts +43 -0
- package/src/action-modal-dispatcher.tsx +10 -9
- package/src/index.ts +7 -0
- package/src/server-error.ts +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 25.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a7da67d: feat(errors): los toasts de mutación/acción muestran la causa real del servidor
|
|
8
|
+
|
|
9
|
+
Nuevo helper compartido `toastServerError` / `extractServerError` (`server-error.ts`): en vez de tragarse el `details` y mostrar solo el `message` genérico ("Error creating record"), el toast muestra el `message` como título y el `details`/`errors` de validación como descripción. Cableado en `action-modal-dispatcher` (create/edit/acciones declarativas). Cualquier consumidor puede reusarlo. Así un 500 del kernel (p. ej. un error de Postgres o un guard declarativo) llega legible al usuario/operador.
|
|
10
|
+
|
|
3
11
|
## 25.0.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAyDA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCA+DlB"}
|
|
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
12
12
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
|
|
13
13
|
import { Loader2 } from 'lucide-react';
|
|
14
14
|
import { toast } from 'sonner';
|
|
15
|
+
import { toastServerError, toastServerSuccess } from './server-error';
|
|
15
16
|
import { useApi } from './api-context';
|
|
16
17
|
import { DynamicIcon } from './dynamic-icon';
|
|
17
18
|
import { DynamicLineItems } from './dynamic-line-items';
|
|
@@ -263,16 +264,16 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
263
264
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
264
265
|
const res = await api.post(url, {});
|
|
265
266
|
if (res.data.success) {
|
|
266
|
-
|
|
267
|
+
toastServerSuccess(res.data, { t });
|
|
267
268
|
onOpenChange(false);
|
|
268
269
|
onSuccess();
|
|
269
270
|
}
|
|
270
271
|
else {
|
|
271
|
-
|
|
272
|
+
toastServerError({ response: { data: res.data } }, { t });
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
catch (err) {
|
|
275
|
-
|
|
276
|
+
toastServerError(err, { t });
|
|
276
277
|
}
|
|
277
278
|
finally {
|
|
278
279
|
setExecuting(false);
|
|
@@ -360,16 +361,16 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
360
361
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
361
362
|
const res = await api.post(url, formData);
|
|
362
363
|
if (res.data.success) {
|
|
363
|
-
|
|
364
|
+
toastServerSuccess(res.data, { t });
|
|
364
365
|
onOpenChange(false);
|
|
365
366
|
onSuccess();
|
|
366
367
|
}
|
|
367
368
|
else {
|
|
368
|
-
|
|
369
|
+
toastServerError({ response: { data: res.data } }, { t });
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
catch (err) {
|
|
372
|
-
|
|
373
|
+
toastServerError(err, { t });
|
|
373
374
|
}
|
|
374
375
|
finally {
|
|
375
376
|
setExecuting(false);
|
|
@@ -489,16 +490,16 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
489
490
|
const url = buildActionUrl(endpoint, model, record?.id, action.key);
|
|
490
491
|
const res = await api.post(url, formData);
|
|
491
492
|
if (res.data.success) {
|
|
492
|
-
|
|
493
|
+
toastServerSuccess(res.data, { t });
|
|
493
494
|
onOpenChange(false);
|
|
494
495
|
onSuccess();
|
|
495
496
|
}
|
|
496
497
|
else {
|
|
497
|
-
|
|
498
|
+
toastServerError({ response: { data: res.data } }, { t });
|
|
498
499
|
}
|
|
499
500
|
}
|
|
500
501
|
catch (err) {
|
|
501
|
-
|
|
502
|
+
toastServerError(err, { t });
|
|
502
503
|
}
|
|
503
504
|
finally {
|
|
504
505
|
setExecuting(false);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { LicenseGate, LicenseExpiryBanner, LicenseStatusBadge, isLicenseOperable, isLicenseBlocking, isPresetEntitled, isTrialExpired, type LicenseGateProps, type LicenseExpiryBannerProps, type LicenseStatusBadgeProps, type LicenseState, type LicenseStatus, type LicenseBranding, } from './license';
|
|
3
3
|
export * from './options-context';
|
|
4
|
+
export { extractServerError, toastServerError, toastServerSuccess, type ExtractedError, type Translate, } from './server-error';
|
|
4
5
|
export * from './dynamic-table';
|
|
5
6
|
export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
6
7
|
export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, type StageAutomation, type StageAutomationAction, type StageAutomationActionType, type NewStageAutomation, type UseStageAutomationsResult, type StageAutomationsButtonProps, } from './stage-automations';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export * from './types';
|
|
7
7
|
export { LicenseGate, LicenseExpiryBanner, LicenseStatusBadge, isLicenseOperable, isLicenseBlocking, isPresetEntitled, isTrialExpired, } from './license';
|
|
8
8
|
export * from './options-context';
|
|
9
|
+
export { extractServerError, toastServerError, toastServerSuccess, } from './server-error';
|
|
9
10
|
export * from './dynamic-table';
|
|
10
11
|
export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
11
12
|
export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, } from './stage-automations';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Structured, display-ready view of an error: a headline + an optional cause. */
|
|
2
|
+
export interface ExtractedError {
|
|
3
|
+
/** Primary line — the server's `message` (may be an i18n key) or a fallback. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Secondary line — the specific cause: `details`, `error`, joined validation
|
|
6
|
+
* `errors`, or a raw network/thrown message. Undefined when nothing more
|
|
7
|
+
* specific than the title is available. */
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pull the best available headline + cause out of an axios error, a raw
|
|
12
|
+
* `{ success:false, ... }` response body, or a thrown Error/string. Pure — no
|
|
13
|
+
* i18n, no toast — so it is unit-testable and reusable (dialogs, inline errors).
|
|
14
|
+
*
|
|
15
|
+
* Resolution:
|
|
16
|
+
* title ← data.message (else the specific cause, so the toast is never
|
|
17
|
+
* empty; else `fallbackTitle`)
|
|
18
|
+
* description ← data.details → data.error → joined data.errors → raw err.message
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractServerError(err: unknown, fallbackTitle: string): ExtractedError;
|
|
21
|
+
/** i18n translator, tolerant of non-keys (returns `defaultValue`/the input). */
|
|
22
|
+
export type Translate = (key: string, opts?: {
|
|
23
|
+
defaultValue?: string;
|
|
24
|
+
}) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Toast a successful mutation/action response, LOCALIZED. The kernel/host often
|
|
27
|
+
* returns a hardcoded English `message` ("Record created successfully"); echoing
|
|
28
|
+
* it verbatim leaks English into a Spanish UI. So we only translate the server
|
|
29
|
+
* message when it is an i18n KEY (dotted, e.g. "pos.rate.created"); a prose
|
|
30
|
+
* message is dropped in favour of the localized `fallbackKey` (default
|
|
31
|
+
* "common.success"). Pass the app's `t`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function toastServerSuccess(data: unknown, opts?: {
|
|
34
|
+
t?: Translate;
|
|
35
|
+
fallbackKey?: string;
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* Toast a server/network error, surfacing the REAL cause as the description
|
|
39
|
+
* instead of a bare generic line. Pass the app's `t` so an i18n-key `message`
|
|
40
|
+
* (e.g. "pos.error.no_rate") is localized; a plain-text message passes through
|
|
41
|
+
* unchanged. Use everywhere a mutation/action can fail.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toastServerError(err: unknown, opts?: {
|
|
44
|
+
t?: Translate;
|
|
45
|
+
fallback?: string;
|
|
46
|
+
}): void;
|
|
47
|
+
//# sourceMappingURL=server-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-error.d.ts","sourceRoot":"","sources":["../src/server-error.ts"],"names":[],"mappings":"AAaA,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC3B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAA;IACb;;gDAE4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,GAAG,cAAc,CAuBtF;AAED,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAA;AAUjF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;IAAE,CAAC,CAAC,EAAE,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,IAAI,CAUN;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;IAAE,CAAC,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAOhG"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Shared server/network error surfacing for declarative mutations & actions.
|
|
2
|
+
//
|
|
3
|
+
// Kernel/host handlers answer a failed write with a consistent envelope:
|
|
4
|
+
// { success: false, message: "<headline / i18n key>", details: "<real cause>" }
|
|
5
|
+
// where `details` carries what actually went wrong (a Postgres error, a
|
|
6
|
+
// declarative guard failure, a validation reason). The historical toast pattern
|
|
7
|
+
// toast.error(err?.response?.data?.message || t('common.error'))
|
|
8
|
+
// threw `details` away and showed only the generic headline ("Error creating
|
|
9
|
+
// record") — so a user/operator saw "something failed" with no way to know WHY
|
|
10
|
+
// or report it. This module keeps the headline but ALSO surfaces the cause as
|
|
11
|
+
// the toast description, in ONE place so every call site behaves identically.
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
/** Flattens a validation `errors` payload (string | string[] | field→msgs map)
|
|
14
|
+
* into a single newline-joined string, or undefined when empty. */
|
|
15
|
+
function joinErrors(errors) {
|
|
16
|
+
if (!errors)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof errors === 'string')
|
|
19
|
+
return errors || undefined;
|
|
20
|
+
if (Array.isArray(errors))
|
|
21
|
+
return errors.filter(Boolean).map(String).join('\n') || undefined;
|
|
22
|
+
if (typeof errors === 'object') {
|
|
23
|
+
const parts = Object.entries(errors).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : String(v)}`);
|
|
24
|
+
return parts.join('\n') || undefined;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pull the best available headline + cause out of an axios error, a raw
|
|
30
|
+
* `{ success:false, ... }` response body, or a thrown Error/string. Pure — no
|
|
31
|
+
* i18n, no toast — so it is unit-testable and reusable (dialogs, inline errors).
|
|
32
|
+
*
|
|
33
|
+
* Resolution:
|
|
34
|
+
* title ← data.message (else the specific cause, so the toast is never
|
|
35
|
+
* empty; else `fallbackTitle`)
|
|
36
|
+
* description ← data.details → data.error → joined data.errors → raw err.message
|
|
37
|
+
*/
|
|
38
|
+
export function extractServerError(err, fallbackTitle) {
|
|
39
|
+
// Accept either an axios error (`err.response.data`) or a bare response body
|
|
40
|
+
// (`{ success, message, details }`) passed straight in.
|
|
41
|
+
const maybeAxios = err?.response?.data;
|
|
42
|
+
const data = maybeAxios ?? err;
|
|
43
|
+
if (data && typeof data === 'object' && ('message' in data || 'details' in data || 'errors' in data || 'error' in data)) {
|
|
44
|
+
const d = data;
|
|
45
|
+
const message = typeof d.message === 'string' ? d.message.trim() : '';
|
|
46
|
+
const details = (typeof d.details === 'string' && d.details.trim()) ||
|
|
47
|
+
(typeof d.error === 'string' && d.error.trim()) ||
|
|
48
|
+
joinErrors(d.errors) ||
|
|
49
|
+
'';
|
|
50
|
+
if (message && details && message !== details)
|
|
51
|
+
return { title: message, description: details };
|
|
52
|
+
if (message || details)
|
|
53
|
+
return { title: message || details };
|
|
54
|
+
}
|
|
55
|
+
// Non-HTTP failure (network down, CORS, a thrown string/Error): show it as
|
|
56
|
+
// the cause under the generic fallback headline.
|
|
57
|
+
const raw = typeof err === 'string' ? err : err?.message;
|
|
58
|
+
if (typeof raw === 'string' && raw.trim())
|
|
59
|
+
return { title: fallbackTitle, description: raw.trim() };
|
|
60
|
+
return { title: fallbackTitle };
|
|
61
|
+
}
|
|
62
|
+
/** A dotted, space-free token (e.g. "pos.rate.created") — the shape of an i18n
|
|
63
|
+
* key, as opposed to human prose ("Record created successfully"). Used to
|
|
64
|
+
* decide whether a server-sent message is safe to translate or should be
|
|
65
|
+
* replaced by a localized fallback. */
|
|
66
|
+
function looksLikeI18nKey(s) {
|
|
67
|
+
return /^[a-z0-9_-]+(\.[a-z0-9_-]+)+$/i.test(s);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Toast a successful mutation/action response, LOCALIZED. The kernel/host often
|
|
71
|
+
* returns a hardcoded English `message` ("Record created successfully"); echoing
|
|
72
|
+
* it verbatim leaks English into a Spanish UI. So we only translate the server
|
|
73
|
+
* message when it is an i18n KEY (dotted, e.g. "pos.rate.created"); a prose
|
|
74
|
+
* message is dropped in favour of the localized `fallbackKey` (default
|
|
75
|
+
* "common.success"). Pass the app's `t`.
|
|
76
|
+
*/
|
|
77
|
+
export function toastServerSuccess(data, opts) {
|
|
78
|
+
const t = opts?.t;
|
|
79
|
+
const fallbackKey = opts?.fallbackKey ?? 'common.success';
|
|
80
|
+
const fallback = t ? t(fallbackKey, { defaultValue: 'Success' }) : 'Success';
|
|
81
|
+
const msg = data?.message;
|
|
82
|
+
if (t && typeof msg === 'string' && msg && looksLikeI18nKey(msg)) {
|
|
83
|
+
toast.success(t(msg, { defaultValue: fallback }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
toast.success(fallback);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Toast a server/network error, surfacing the REAL cause as the description
|
|
90
|
+
* instead of a bare generic line. Pass the app's `t` so an i18n-key `message`
|
|
91
|
+
* (e.g. "pos.error.no_rate") is localized; a plain-text message passes through
|
|
92
|
+
* unchanged. Use everywhere a mutation/action can fail.
|
|
93
|
+
*/
|
|
94
|
+
export function toastServerError(err, opts) {
|
|
95
|
+
const t = opts?.t;
|
|
96
|
+
const fallback = opts?.fallback ?? (t ? t('common.error', { defaultValue: 'Something went wrong' }) : 'Something went wrong');
|
|
97
|
+
const { title, description } = extractServerError(err, fallback);
|
|
98
|
+
const shownTitle = t ? t(title, { defaultValue: title }) : title;
|
|
99
|
+
toast.error(shownTitle, description ? { description } : undefined);
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { extractServerError } from '../server-error'
|
|
3
|
+
|
|
4
|
+
describe('extractServerError', () => {
|
|
5
|
+
it('surfaces details as description under message headline', () => {
|
|
6
|
+
const err = { response: { data: { success: false, message: 'Error creating record', details: 'ERROR: column "x" does not exist (SQLSTATE 42703)' } } }
|
|
7
|
+
expect(extractServerError(err, 'fallback')).toEqual({ title: 'Error creating record', description: 'ERROR: column "x" does not exist (SQLSTATE 42703)' })
|
|
8
|
+
})
|
|
9
|
+
it('accepts a bare response body', () => {
|
|
10
|
+
expect(extractServerError({ success: false, message: 'm', details: 'd' }, 'fb')).toEqual({ title: 'm', description: 'd' })
|
|
11
|
+
})
|
|
12
|
+
it('uses details as title when no message', () => {
|
|
13
|
+
expect(extractServerError({ response: { data: { details: 'boom' } } }, 'fb')).toEqual({ title: 'boom' })
|
|
14
|
+
})
|
|
15
|
+
it('joins validation errors object', () => {
|
|
16
|
+
const r = extractServerError({ response: { data: { message: 'Validation error', errors: { name: ['required'], sku: 'dup' } } } }, 'fb')
|
|
17
|
+
expect(r.title).toBe('Validation error'); expect(r.description).toContain('name: required'); expect(r.description).toContain('sku: dup')
|
|
18
|
+
})
|
|
19
|
+
it('falls back for a thrown string / network error', () => {
|
|
20
|
+
expect(extractServerError('Network Error', 'fb')).toEqual({ title: 'fb', description: 'Network Error' })
|
|
21
|
+
expect(extractServerError({}, 'fb')).toEqual({ title: 'fb' })
|
|
22
|
+
})
|
|
23
|
+
it('does not duplicate when message equals details', () => {
|
|
24
|
+
expect(extractServerError({ response: { data: { message: 'same', details: 'same' } } }, 'fb')).toEqual({ title: 'same' })
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
import { vi } from 'vitest'
|
|
29
|
+
import { toastServerSuccess } from '../server-error'
|
|
30
|
+
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
|
31
|
+
import { toast } from 'sonner'
|
|
32
|
+
describe('toastServerSuccess', () => {
|
|
33
|
+
const t = (k: string, o?: { defaultValue?: string }) => (k === 'common.success' ? 'Registro creado' : (o?.defaultValue ?? k))
|
|
34
|
+
it('drops English prose in favour of localized fallback', () => {
|
|
35
|
+
toastServerSuccess({ message: 'Record created successfully' }, { t })
|
|
36
|
+
expect((toast.success as any).mock.calls.at(-1)[0]).toBe('Registro creado')
|
|
37
|
+
})
|
|
38
|
+
it('translates a dotted i18n key', () => {
|
|
39
|
+
const t2 = (k: string, o?: { defaultValue?: string }) => (k === 'pos.rate.created' ? 'Tasa creada' : (k === 'common.success' ? 'Registro creado' : (o?.defaultValue ?? k)))
|
|
40
|
+
toastServerSuccess({ message: 'pos.rate.created' }, { t: t2 })
|
|
41
|
+
expect((toast.success as any).mock.calls.at(-1)[0]).toBe('Tasa creada')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from '@asteby/metacore-ui/primitives'
|
|
36
36
|
import { Loader2 } from 'lucide-react'
|
|
37
37
|
import { toast } from 'sonner'
|
|
38
|
+
import { toastServerError, toastServerSuccess } from './server-error'
|
|
38
39
|
import { useApi } from './api-context'
|
|
39
40
|
import { DynamicIcon } from './dynamic-icon'
|
|
40
41
|
import { DynamicLineItems } from './dynamic-line-items'
|
|
@@ -417,14 +418,14 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
417
418
|
const url = buildActionUrl(endpoint, model, record.id, action.key)
|
|
418
419
|
const res = await api.post(url, {})
|
|
419
420
|
if (res.data.success) {
|
|
420
|
-
|
|
421
|
+
toastServerSuccess(res.data, { t })
|
|
421
422
|
onOpenChange(false)
|
|
422
423
|
onSuccess()
|
|
423
424
|
} else {
|
|
424
|
-
|
|
425
|
+
toastServerError({ response: { data: res.data } }, { t })
|
|
425
426
|
}
|
|
426
427
|
} catch (err: any) {
|
|
427
|
-
|
|
428
|
+
toastServerError(err, { t })
|
|
428
429
|
} finally {
|
|
429
430
|
setExecuting(false)
|
|
430
431
|
}
|
|
@@ -539,14 +540,14 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
539
540
|
const url = buildActionUrl(endpoint, model, record.id, action.key)
|
|
540
541
|
const res = await api.post(url, formData)
|
|
541
542
|
if (res.data.success) {
|
|
542
|
-
|
|
543
|
+
toastServerSuccess(res.data, { t })
|
|
543
544
|
onOpenChange(false)
|
|
544
545
|
onSuccess()
|
|
545
546
|
} else {
|
|
546
|
-
|
|
547
|
+
toastServerError({ response: { data: res.data } }, { t })
|
|
547
548
|
}
|
|
548
549
|
} catch (err: any) {
|
|
549
|
-
|
|
550
|
+
toastServerError(err, { t })
|
|
550
551
|
} finally {
|
|
551
552
|
setExecuting(false)
|
|
552
553
|
}
|
|
@@ -744,14 +745,14 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
744
745
|
const url = buildActionUrl(endpoint, model, record?.id, action.key)
|
|
745
746
|
const res = await api.post(url, formData)
|
|
746
747
|
if (res.data.success) {
|
|
747
|
-
|
|
748
|
+
toastServerSuccess(res.data, { t })
|
|
748
749
|
onOpenChange(false)
|
|
749
750
|
onSuccess()
|
|
750
751
|
} else {
|
|
751
|
-
|
|
752
|
+
toastServerError({ response: { data: res.data } }, { t })
|
|
752
753
|
}
|
|
753
754
|
} catch (err: any) {
|
|
754
|
-
|
|
755
|
+
toastServerError(err, { t })
|
|
755
756
|
} finally {
|
|
756
757
|
setExecuting(false)
|
|
757
758
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,13 @@ export {
|
|
|
20
20
|
type LicenseBranding,
|
|
21
21
|
} from './license'
|
|
22
22
|
export * from './options-context'
|
|
23
|
+
export {
|
|
24
|
+
extractServerError,
|
|
25
|
+
toastServerError,
|
|
26
|
+
toastServerSuccess,
|
|
27
|
+
type ExtractedError,
|
|
28
|
+
type Translate,
|
|
29
|
+
} from './server-error'
|
|
23
30
|
export * from './dynamic-table'
|
|
24
31
|
export {
|
|
25
32
|
DynamicKanban,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Shared server/network error surfacing for declarative mutations & actions.
|
|
2
|
+
//
|
|
3
|
+
// Kernel/host handlers answer a failed write with a consistent envelope:
|
|
4
|
+
// { success: false, message: "<headline / i18n key>", details: "<real cause>" }
|
|
5
|
+
// where `details` carries what actually went wrong (a Postgres error, a
|
|
6
|
+
// declarative guard failure, a validation reason). The historical toast pattern
|
|
7
|
+
// toast.error(err?.response?.data?.message || t('common.error'))
|
|
8
|
+
// threw `details` away and showed only the generic headline ("Error creating
|
|
9
|
+
// record") — so a user/operator saw "something failed" with no way to know WHY
|
|
10
|
+
// or report it. This module keeps the headline but ALSO surfaces the cause as
|
|
11
|
+
// the toast description, in ONE place so every call site behaves identically.
|
|
12
|
+
import { toast } from 'sonner'
|
|
13
|
+
|
|
14
|
+
/** Structured, display-ready view of an error: a headline + an optional cause. */
|
|
15
|
+
export interface ExtractedError {
|
|
16
|
+
/** Primary line — the server's `message` (may be an i18n key) or a fallback. */
|
|
17
|
+
title: string
|
|
18
|
+
/** Secondary line — the specific cause: `details`, `error`, joined validation
|
|
19
|
+
* `errors`, or a raw network/thrown message. Undefined when nothing more
|
|
20
|
+
* specific than the title is available. */
|
|
21
|
+
description?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Flattens a validation `errors` payload (string | string[] | field→msgs map)
|
|
25
|
+
* into a single newline-joined string, or undefined when empty. */
|
|
26
|
+
function joinErrors(errors: unknown): string | undefined {
|
|
27
|
+
if (!errors) return undefined
|
|
28
|
+
if (typeof errors === 'string') return errors || undefined
|
|
29
|
+
if (Array.isArray(errors)) return errors.filter(Boolean).map(String).join('\n') || undefined
|
|
30
|
+
if (typeof errors === 'object') {
|
|
31
|
+
const parts = Object.entries(errors as Record<string, unknown>).map(
|
|
32
|
+
([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : String(v)}`,
|
|
33
|
+
)
|
|
34
|
+
return parts.join('\n') || undefined
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pull the best available headline + cause out of an axios error, a raw
|
|
41
|
+
* `{ success:false, ... }` response body, or a thrown Error/string. Pure — no
|
|
42
|
+
* i18n, no toast — so it is unit-testable and reusable (dialogs, inline errors).
|
|
43
|
+
*
|
|
44
|
+
* Resolution:
|
|
45
|
+
* title ← data.message (else the specific cause, so the toast is never
|
|
46
|
+
* empty; else `fallbackTitle`)
|
|
47
|
+
* description ← data.details → data.error → joined data.errors → raw err.message
|
|
48
|
+
*/
|
|
49
|
+
export function extractServerError(err: unknown, fallbackTitle: string): ExtractedError {
|
|
50
|
+
// Accept either an axios error (`err.response.data`) or a bare response body
|
|
51
|
+
// (`{ success, message, details }`) passed straight in.
|
|
52
|
+
const maybeAxios = (err as { response?: { data?: unknown } } | undefined)?.response?.data
|
|
53
|
+
const data = maybeAxios ?? err
|
|
54
|
+
|
|
55
|
+
if (data && typeof data === 'object' && ('message' in data || 'details' in data || 'errors' in data || 'error' in data)) {
|
|
56
|
+
const d = data as { message?: unknown; details?: unknown; error?: unknown; errors?: unknown }
|
|
57
|
+
const message = typeof d.message === 'string' ? d.message.trim() : ''
|
|
58
|
+
const details =
|
|
59
|
+
(typeof d.details === 'string' && d.details.trim()) ||
|
|
60
|
+
(typeof d.error === 'string' && d.error.trim()) ||
|
|
61
|
+
joinErrors(d.errors) ||
|
|
62
|
+
''
|
|
63
|
+
if (message && details && message !== details) return { title: message, description: details }
|
|
64
|
+
if (message || details) return { title: message || details }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Non-HTTP failure (network down, CORS, a thrown string/Error): show it as
|
|
68
|
+
// the cause under the generic fallback headline.
|
|
69
|
+
const raw = typeof err === 'string' ? err : (err as { message?: unknown } | undefined)?.message
|
|
70
|
+
if (typeof raw === 'string' && raw.trim()) return { title: fallbackTitle, description: raw.trim() }
|
|
71
|
+
return { title: fallbackTitle }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** i18n translator, tolerant of non-keys (returns `defaultValue`/the input). */
|
|
75
|
+
export type Translate = (key: string, opts?: { defaultValue?: string }) => string
|
|
76
|
+
|
|
77
|
+
/** A dotted, space-free token (e.g. "pos.rate.created") — the shape of an i18n
|
|
78
|
+
* key, as opposed to human prose ("Record created successfully"). Used to
|
|
79
|
+
* decide whether a server-sent message is safe to translate or should be
|
|
80
|
+
* replaced by a localized fallback. */
|
|
81
|
+
function looksLikeI18nKey(s: string): boolean {
|
|
82
|
+
return /^[a-z0-9_-]+(\.[a-z0-9_-]+)+$/i.test(s)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Toast a successful mutation/action response, LOCALIZED. The kernel/host often
|
|
87
|
+
* returns a hardcoded English `message` ("Record created successfully"); echoing
|
|
88
|
+
* it verbatim leaks English into a Spanish UI. So we only translate the server
|
|
89
|
+
* message when it is an i18n KEY (dotted, e.g. "pos.rate.created"); a prose
|
|
90
|
+
* message is dropped in favour of the localized `fallbackKey` (default
|
|
91
|
+
* "common.success"). Pass the app's `t`.
|
|
92
|
+
*/
|
|
93
|
+
export function toastServerSuccess(
|
|
94
|
+
data: unknown,
|
|
95
|
+
opts?: { t?: Translate; fallbackKey?: string },
|
|
96
|
+
): void {
|
|
97
|
+
const t = opts?.t
|
|
98
|
+
const fallbackKey = opts?.fallbackKey ?? 'common.success'
|
|
99
|
+
const fallback = t ? t(fallbackKey, { defaultValue: 'Success' }) : 'Success'
|
|
100
|
+
const msg = (data as { message?: unknown } | undefined)?.message
|
|
101
|
+
if (t && typeof msg === 'string' && msg && looksLikeI18nKey(msg)) {
|
|
102
|
+
toast.success(t(msg, { defaultValue: fallback }))
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
toast.success(fallback)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Toast a server/network error, surfacing the REAL cause as the description
|
|
110
|
+
* instead of a bare generic line. Pass the app's `t` so an i18n-key `message`
|
|
111
|
+
* (e.g. "pos.error.no_rate") is localized; a plain-text message passes through
|
|
112
|
+
* unchanged. Use everywhere a mutation/action can fail.
|
|
113
|
+
*/
|
|
114
|
+
export function toastServerError(err: unknown, opts?: { t?: Translate; fallback?: string }): void {
|
|
115
|
+
const t = opts?.t
|
|
116
|
+
const fallback =
|
|
117
|
+
opts?.fallback ?? (t ? t('common.error', { defaultValue: 'Something went wrong' }) : 'Something went wrong')
|
|
118
|
+
const { title, description } = extractServerError(err, fallback)
|
|
119
|
+
const shownTitle = t ? t(title, { defaultValue: title }) : title
|
|
120
|
+
toast.error(shownTitle, description ? { description } : undefined)
|
|
121
|
+
}
|