@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 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":"AAwDA,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"}
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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
267
+ toastServerSuccess(res.data, { t });
267
268
  onOpenChange(false);
268
269
  onSuccess();
269
270
  }
270
271
  else {
271
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
272
+ toastServerError({ response: { data: res.data } }, { t });
272
273
  }
273
274
  }
274
275
  catch (err) {
275
- toast.error(err?.response?.data?.message || t('common.error'));
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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
364
+ toastServerSuccess(res.data, { t });
364
365
  onOpenChange(false);
365
366
  onSuccess();
366
367
  }
367
368
  else {
368
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
369
+ toastServerError({ response: { data: res.data } }, { t });
369
370
  }
370
371
  }
371
372
  catch (err) {
372
- toast.error(err?.response?.data?.message || t('common.error'));
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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
493
+ toastServerSuccess(res.data, { t });
493
494
  onOpenChange(false);
494
495
  onSuccess();
495
496
  }
496
497
  else {
497
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
498
+ toastServerError({ response: { data: res.data } }, { t });
498
499
  }
499
500
  }
500
501
  catch (err) {
501
- toast.error(err?.response?.data?.message || t('common.error'));
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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "25.0.0",
3
+ "version": "25.1.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'))
421
+ toastServerSuccess(res.data, { t })
421
422
  onOpenChange(false)
422
423
  onSuccess()
423
424
  } else {
424
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'))
425
+ toastServerError({ response: { data: res.data } }, { t })
425
426
  }
426
427
  } catch (err: any) {
427
- toast.error(err?.response?.data?.message || t('common.error'))
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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'))
543
+ toastServerSuccess(res.data, { t })
543
544
  onOpenChange(false)
544
545
  onSuccess()
545
546
  } else {
546
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'))
547
+ toastServerError({ response: { data: res.data } }, { t })
547
548
  }
548
549
  } catch (err: any) {
549
- toast.error(err?.response?.data?.message || t('common.error'))
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
- toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'))
748
+ toastServerSuccess(res.data, { t })
748
749
  onOpenChange(false)
749
750
  onSuccess()
750
751
  } else {
751
- toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'))
752
+ toastServerError({ response: { data: res.data } }, { t })
752
753
  }
753
754
  } catch (err: any) {
754
- toast.error(err?.response?.data?.message || t('common.error'))
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
+ }