@asteby/metacore-runtime-react 31.1.1 → 32.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 +27 -0
- package/dist/action-modal-dispatcher.d.ts +20 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +52 -74
- package/dist/addon-fiber.d.ts +57 -0
- package/dist/addon-fiber.d.ts.map +1 -0
- package/dist/addon-fiber.js +122 -0
- package/dist/addon-loader.d.ts +19 -3
- package/dist/addon-loader.d.ts.map +1 -1
- package/dist/addon-loader.js +36 -37
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +27 -28
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/server-error.d.ts +17 -8
- package/dist/server-error.d.ts.map +1 -1
- package/dist/server-error.js +75 -28
- package/dist/types.d.ts +4 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-catalog.d.ts +8 -0
- package/dist/validation-catalog.d.ts.map +1 -0
- package/dist/validation-catalog.js +59 -0
- package/dist/validator.d.ts +22 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +261 -0
- package/package.json +3 -3
- package/src/__tests__/addon-fiber.test.ts +111 -0
- package/src/__tests__/extract-field-errors.test.ts +25 -1
- package/src/__tests__/prefill-from-record.test.ts +146 -0
- package/src/__tests__/validator.test.ts +70 -0
- package/src/action-modal-dispatcher.tsx +57 -72
- package/src/addon-fiber.ts +155 -0
- package/src/addon-loader.tsx +66 -47
- package/src/dialogs/dynamic-record.tsx +25 -28
- package/src/dynamic-form-schema.ts +3 -2
- package/src/index.ts +24 -0
- package/src/server-error.ts +85 -30
- package/src/types.ts +9 -12
- package/src/validation-catalog.ts +60 -0
- package/src/validator.ts +275 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 32.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fa25f99: Laravel-style field validator: localize 422 `{errors:{field:[{code,params}]}}`
|
|
8
|
+
into the operator's language (es/en catalogs, overridable via `validation.*`),
|
|
9
|
+
map keys to translated labels, and pre-flight the same regex/min/max/custom
|
|
10
|
+
rules the kernel enforces — no more generic "validation failed" / `[object Object]`.
|
|
11
|
+
|
|
12
|
+
## 32.0.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 5a932ed: Addon fiber hot-swap without a page reload: Registry.scope/unbind, Plugin.register
|
|
17
|
+
may return a Disposable, AddonLoader re-registers federation remotes when `?v=`
|
|
18
|
+
changes, and purgeAddonFrontendCache drops only that addon's SW cache (L1).
|
|
19
|
+
Hosts call acknowledgeRunningVersion after a successful swap instead of
|
|
20
|
+
location.reload / unregistering the service worker.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 5a932ed: Export PrefillSpec helpers (`isPrefillSpec`, `buildPrefillRows`, `applyPrefillLock`)
|
|
25
|
+
so addons can unit-test `$prefillFromRecord` without reaching into private
|
|
26
|
+
dispatcher internals. Kernel now types the same shape in the v3 schema.
|
|
27
|
+
- Updated dependencies [5a932ed]
|
|
28
|
+
- @asteby/metacore-sdk@3.5.0
|
|
29
|
+
|
|
3
30
|
## 31.1.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
import type { ActionFieldDef } from './types';
|
|
1
2
|
import { type ActionMetadata, type ActionModalProps } from '@asteby/metacore-sdk';
|
|
2
3
|
export type { ActionMetadata, ActionModalProps };
|
|
4
|
+
export interface PrefillSpec {
|
|
5
|
+
$prefillFromRecord: string;
|
|
6
|
+
map?: Record<string, string>;
|
|
7
|
+
remaining?: {
|
|
8
|
+
target: string;
|
|
9
|
+
of: string;
|
|
10
|
+
minus?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Item-field keys to lock (render read-only) in the prefilled rows — e.g. the
|
|
14
|
+
* product of a receive-goods line is dictated by the source document, so it
|
|
15
|
+
* is shown as a resolved name but cannot be changed. Editable in the create
|
|
16
|
+
* flow (which carries no prefill spec); only the prefilled action locks them.
|
|
17
|
+
*/
|
|
18
|
+
lock?: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function isPrefillSpec(v: unknown): v is PrefillSpec;
|
|
21
|
+
export declare function applyPrefillLock(field: ActionFieldDef): ActionFieldDef;
|
|
22
|
+
export declare function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<string, any>>;
|
|
3
23
|
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }: ActionModalProps): import("react").JSX.Element | null;
|
|
4
24
|
//# sourceMappingURL=action-modal-dispatcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAoElB"}
|
|
@@ -14,7 +14,9 @@ import { useTranslation } from 'react-i18next';
|
|
|
14
14
|
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';
|
|
15
15
|
import { Loader2 } from 'lucide-react';
|
|
16
16
|
import { toast } from 'sonner';
|
|
17
|
-
import { toastServerError, toastServerSuccess, extractFieldErrors,
|
|
17
|
+
import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
|
|
18
|
+
import { validateValues, bagHasErrors } from './validator';
|
|
19
|
+
import { validationCatalog } from './validation-catalog';
|
|
18
20
|
import { useApi } from './api-context';
|
|
19
21
|
import { DynamicIcon } from './dynamic-icon';
|
|
20
22
|
import { DynamicLineItems } from './dynamic-line-items';
|
|
@@ -28,7 +30,7 @@ import { useMetadataCache } from './metadata-cache';
|
|
|
28
30
|
import { ViewValue, objectLabel, relationSiblingValue, isLineItemsField as isRecordLineItemsField, } from './dialogs/dynamic-record';
|
|
29
31
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
30
32
|
import { getActionComponent, } from '@asteby/metacore-sdk';
|
|
31
|
-
function isPrefillSpec(v) {
|
|
33
|
+
export function isPrefillSpec(v) {
|
|
32
34
|
return (typeof v === 'object' &&
|
|
33
35
|
v !== null &&
|
|
34
36
|
typeof v.$prefillFromRecord === 'string');
|
|
@@ -51,7 +53,7 @@ function toNum(v) {
|
|
|
51
53
|
// untouched when there is no prefill spec or no lock list (the create flow,
|
|
52
54
|
// which carries no prefill, stays fully editable). The readonly flag is set on
|
|
53
55
|
// BOTH itemFields aliases the renderers tolerate.
|
|
54
|
-
function applyPrefillLock(field) {
|
|
56
|
+
export function applyPrefillLock(field) {
|
|
55
57
|
const spec = lineItemsDefault(field);
|
|
56
58
|
if (!isPrefillSpec(spec) || !spec.lock || spec.lock.length === 0)
|
|
57
59
|
return field;
|
|
@@ -64,7 +66,7 @@ function applyPrefillLock(field) {
|
|
|
64
66
|
return { ...field, itemFields: patched, item_fields: patched };
|
|
65
67
|
}
|
|
66
68
|
// buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
|
|
67
|
-
function buildPrefillRows(spec, record) {
|
|
69
|
+
export function buildPrefillRows(spec, record) {
|
|
68
70
|
const src = record?.[spec.$prefillFromRecord];
|
|
69
71
|
if (!Array.isArray(src))
|
|
70
72
|
return [];
|
|
@@ -264,34 +266,31 @@ function humanizeKey(k) {
|
|
|
264
266
|
/** Localize a failed action submit's per-field `errors` (422 map or `{errors}`
|
|
265
267
|
* body) into `{ [fieldKey]: localizedMessage }`, using each field's label.
|
|
266
268
|
* Returns undefined when there is no usable per-field map. */
|
|
267
|
-
function localizeActionFieldErrors(err, fields, t) {
|
|
269
|
+
function localizeActionFieldErrors(err, fields, t, language) {
|
|
268
270
|
const map = extractFieldErrors(err);
|
|
269
271
|
if (!map)
|
|
270
272
|
return undefined;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const [k, issues] of Object.entries(map))
|
|
277
|
-
out[k] = localizeFieldIssue(issues[0], labelFor(k), t);
|
|
278
|
-
return out;
|
|
273
|
+
const labels = {};
|
|
274
|
+
for (const f of fields ?? []) {
|
|
275
|
+
labels[f.key] = f.label ? t(f.label, { defaultValue: f.label }) : humanizeKey(f.key);
|
|
276
|
+
}
|
|
277
|
+
return localizeFieldErrorMap(map, t, { labels, language });
|
|
279
278
|
}
|
|
280
279
|
/** Toast a failed action: a summary + localized per-field lines when the server
|
|
281
280
|
* returned a per-field `errors` map, else the standard cause-carrying toast.
|
|
282
281
|
* Used where inline rendering isn't wired (confirm / multi-step wizard). */
|
|
283
|
-
function toastActionError(err, fields, t) {
|
|
284
|
-
const localized = localizeActionFieldErrors(err, fields, t);
|
|
282
|
+
function toastActionError(err, fields, t, language) {
|
|
283
|
+
const localized = localizeActionFieldErrors(err, fields, t, language);
|
|
285
284
|
if (localized) {
|
|
286
|
-
toast.error(t('
|
|
285
|
+
toast.error(t('validation.failed', { defaultValue: validationCatalog(language).failed }), {
|
|
287
286
|
description: Object.values(localized).join('\n'),
|
|
288
287
|
});
|
|
289
288
|
return;
|
|
290
289
|
}
|
|
291
|
-
toastServerError(err, { t });
|
|
290
|
+
toastServerError(err, { t, language });
|
|
292
291
|
}
|
|
293
292
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
294
|
-
const { t } = useTranslation();
|
|
293
|
+
const { t, i18n } = useTranslation();
|
|
295
294
|
const api = useApi();
|
|
296
295
|
const [executing, setExecuting] = useState(false);
|
|
297
296
|
// `action.label` is an addon-contributed i18n key; its locale bundle loads
|
|
@@ -309,11 +308,11 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
309
308
|
onSuccess();
|
|
310
309
|
}
|
|
311
310
|
else {
|
|
312
|
-
toastActionError({ response: { data: res.data } }, action.fields, t);
|
|
311
|
+
toastActionError({ response: { data: res.data } }, action.fields, t, i18n.language);
|
|
313
312
|
}
|
|
314
313
|
}
|
|
315
314
|
catch (err) {
|
|
316
|
-
toastActionError(err, action.fields, t);
|
|
315
|
+
toastActionError(err, action.fields, t, i18n.language);
|
|
317
316
|
}
|
|
318
317
|
finally {
|
|
319
318
|
setExecuting(false);
|
|
@@ -322,7 +321,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
322
321
|
return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
|
|
323
322
|
}
|
|
324
323
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
325
|
-
const { t } = useTranslation();
|
|
324
|
+
const { t, i18n } = useTranslation();
|
|
326
325
|
// Addon-contributed labels (action + fields) are i18n keys whose locale
|
|
327
326
|
// bundle loads asynchronously; translate at render so they don't render raw.
|
|
328
327
|
// defaultValue keeps an already-localized string unchanged.
|
|
@@ -389,39 +388,25 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
389
388
|
return next;
|
|
390
389
|
});
|
|
391
390
|
};
|
|
391
|
+
const lang = i18n.language;
|
|
392
392
|
const handleActionError = (err) => {
|
|
393
|
-
const localized = localizeActionFieldErrors(err, action.fields, t);
|
|
393
|
+
const localized = localizeActionFieldErrors(err, action.fields, t, lang);
|
|
394
394
|
if (localized) {
|
|
395
395
|
setFieldErrors(localized);
|
|
396
|
-
toast.error(t('
|
|
396
|
+
toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
|
|
397
397
|
return;
|
|
398
398
|
}
|
|
399
|
-
toastServerError(err, { t });
|
|
399
|
+
toastServerError(err, { t, language: lang });
|
|
400
400
|
};
|
|
401
401
|
const execute = async () => {
|
|
402
402
|
if (action.fields) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
411
|
-
missing[field.key] = t('validation.line_items_required', {
|
|
412
|
-
defaultValue: '{{label}} requiere al menos un renglón',
|
|
413
|
-
label: tl(field.label),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
if (!formData[field.key] && formData[field.key] !== false) {
|
|
419
|
-
missing[field.key] = localizeFieldIssue({ code: 'required' }, tl(field.label), t);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
if (Object.keys(missing).length) {
|
|
423
|
-
setFieldErrors(missing);
|
|
424
|
-
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
|
|
403
|
+
const bag = validateValues(action.fields, formData);
|
|
404
|
+
if (bagHasErrors(bag)) {
|
|
405
|
+
const labels = {};
|
|
406
|
+
for (const f of action.fields)
|
|
407
|
+
labels[f.key] = tl(f.label);
|
|
408
|
+
setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
|
|
409
|
+
toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
|
|
425
410
|
return;
|
|
426
411
|
}
|
|
427
412
|
}
|
|
@@ -486,25 +471,14 @@ function buildFieldDefaults(fields, record) {
|
|
|
486
471
|
}
|
|
487
472
|
return defaults;
|
|
488
473
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const rows = formData[field.key];
|
|
498
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
499
|
-
return `${tl(field.label)} requiere al menos un renglón`;
|
|
500
|
-
}
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
if (!formData[field.key] && formData[field.key] !== false) {
|
|
504
|
-
return `${tl(field.label)} es requerido`;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return null;
|
|
474
|
+
function wizardStepErrors(fields, formData, t, language) {
|
|
475
|
+
const bag = validateValues(fields, formData);
|
|
476
|
+
if (!bagHasErrors(bag))
|
|
477
|
+
return undefined;
|
|
478
|
+
const labels = {};
|
|
479
|
+
for (const f of fields)
|
|
480
|
+
labels[f.key] = t(f.label, { defaultValue: f.label });
|
|
481
|
+
return localizeFieldErrorMap(bag, t, { labels, language });
|
|
508
482
|
}
|
|
509
483
|
// WizardActionModal — the third render-path: a multi-step form. It accumulates
|
|
510
484
|
// every step's fields into ONE formData object and, on the final step, POSTs all
|
|
@@ -514,7 +488,7 @@ function validateFields(fields, formData, tl) {
|
|
|
514
488
|
// so line-items, dynamic_select, uploads and dates behave identically to a
|
|
515
489
|
// single-page action form — no widget is duplicated.
|
|
516
490
|
function WizardActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
517
|
-
const { t } = useTranslation();
|
|
491
|
+
const { t, i18n } = useTranslation();
|
|
518
492
|
const tl = (s) => t(s, { defaultValue: s });
|
|
519
493
|
const api = useApi();
|
|
520
494
|
const steps = action.steps ?? [];
|
|
@@ -537,9 +511,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
537
511
|
const hasLineItems = useMemo(() => stepFields.some(isLineItemsField), [stepFields]);
|
|
538
512
|
const widthPx = hasLineItems ? '820px' : undefined;
|
|
539
513
|
const goNext = () => {
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
542
|
-
toast.error(
|
|
514
|
+
const localized = wizardStepErrors(stepFields, formData, t, i18n.language);
|
|
515
|
+
if (localized) {
|
|
516
|
+
toast.error(t('validation.failed', { defaultValue: validationCatalog(i18n.language).failed }), {
|
|
517
|
+
description: Object.values(localized).join('\n'),
|
|
518
|
+
});
|
|
543
519
|
return;
|
|
544
520
|
}
|
|
545
521
|
setStepIndex((i) => Math.min(i + 1, steps.length - 1));
|
|
@@ -549,9 +525,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
549
525
|
// Guard every step's required fields on final submit (a user could reach
|
|
550
526
|
// the last step with an untouched earlier line-items grid otherwise).
|
|
551
527
|
for (const s of steps) {
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
toast.error(
|
|
528
|
+
const localized = wizardStepErrors(s.fields ?? [], formData, t, i18n.language);
|
|
529
|
+
if (localized) {
|
|
530
|
+
toast.error(t('validation.failed', { defaultValue: validationCatalog(i18n.language).failed }), {
|
|
531
|
+
description: Object.values(localized).join('\n'),
|
|
532
|
+
});
|
|
555
533
|
return;
|
|
556
534
|
}
|
|
557
535
|
}
|
|
@@ -565,11 +543,11 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
565
543
|
onSuccess();
|
|
566
544
|
}
|
|
567
545
|
else {
|
|
568
|
-
toastActionError({ response: { data: res.data } }, steps.flatMap(s => s.fields ?? []), t);
|
|
546
|
+
toastActionError({ response: { data: res.data } }, steps.flatMap(s => s.fields ?? []), t, i18n.language);
|
|
569
547
|
}
|
|
570
548
|
}
|
|
571
549
|
catch (err) {
|
|
572
|
-
toastActionError(err, steps.flatMap(s => s.fields ?? []), t);
|
|
550
|
+
toastActionError(err, steps.flatMap(s => s.fields ?? []), t, i18n.language);
|
|
573
551
|
}
|
|
574
552
|
finally {
|
|
575
553
|
setExecuting(false);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addon fiber primitives — Cordis-style dispose/reload for federated modules
|
|
3
|
+
* running inside a PWA host. Pure helpers so they unit-test without React
|
|
4
|
+
* or the Module Federation runtime.
|
|
5
|
+
*/
|
|
6
|
+
import type { AddonAPI, Disposable, Plugin } from '@asteby/metacore-sdk';
|
|
7
|
+
export declare const PURGE_ADDON_MESSAGE: "PURGE_ADDON";
|
|
8
|
+
export interface PurgeAddonMessage {
|
|
9
|
+
type: typeof PURGE_ADDON_MESSAGE;
|
|
10
|
+
key: string;
|
|
11
|
+
}
|
|
12
|
+
/** Shape of the exposed `./register` (or `./plugin`) module. */
|
|
13
|
+
export interface AddonRegisterModule {
|
|
14
|
+
register?: Plugin['register'];
|
|
15
|
+
dispose?: Plugin['dispose'];
|
|
16
|
+
default?: Plugin['register'] | Plugin;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedPlugin {
|
|
19
|
+
register?: Plugin['register'];
|
|
20
|
+
dispose?: Plugin['dispose'];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Accept every historical export shape:
|
|
24
|
+
* - `{ register, dispose? }`
|
|
25
|
+
* - `{ default: { register, dispose? } }` (definePlugin)
|
|
26
|
+
* - `{ default: (api) => ... }` (function module)
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolvePluginExports(mod: AddonRegisterModule | null | undefined): ResolvedPlugin;
|
|
29
|
+
export declare function runDispose(dispose?: Disposable | null): Promise<void>;
|
|
30
|
+
export declare function composeDisposables(...fns: Array<Disposable | undefined | null>): Disposable;
|
|
31
|
+
/**
|
|
32
|
+
* True when a Cache API / SW request is a federated frontend asset of `addonKey`.
|
|
33
|
+
* Matches both `/api/addons/<key>/frontend` and `/api/metacore/addons/<key>/frontend`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isAddonFrontendCacheUrl(url: string, addonKey: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Drop cached federation assets for one addon. Primary path is the page-side
|
|
38
|
+
* Cache API (works even before the SW learns `PURGE_ADDON`). Also posts the
|
|
39
|
+
* message so a current SW can drop its own entries.
|
|
40
|
+
*
|
|
41
|
+
* Never unregisters the SW and never deletes other caches — L1, not L2.
|
|
42
|
+
*/
|
|
43
|
+
export declare function purgeAddonFrontendCache(addonKey: string): Promise<void>;
|
|
44
|
+
/** Test seam: which remote URL is currently registered per federation scope. */
|
|
45
|
+
export declare const remoteEntryByScope: Map<string, string>;
|
|
46
|
+
export declare function shouldReregisterRemote(scope: string, url: string): boolean;
|
|
47
|
+
export declare function markRemoteRegistered(scope: string, url: string): void;
|
|
48
|
+
/** @internal tests */
|
|
49
|
+
export declare function resetRemoteRegistry(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Safe no-op when `register()` returns void. Used by AddonLoader to treat
|
|
52
|
+
* both historical and fiber-style plugins uniformly.
|
|
53
|
+
*/
|
|
54
|
+
export declare function disposableFromRegisterResult(result: void | Disposable): Disposable | undefined;
|
|
55
|
+
/** Type-only re-export so AddonLoader does not import Plugin internals twice. */
|
|
56
|
+
export type { AddonAPI, Disposable };
|
|
57
|
+
//# sourceMappingURL=addon-fiber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addon-fiber.d.ts","sourceRoot":"","sources":["../src/addon-fiber.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAExE,eAAO,MAAM,mBAAmB,EAAG,aAAsB,CAAA;AAEzD,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,OAAO,mBAAmB,CAAA;IAChC,GAAG,EAAE,MAAM,CAAA;CACd;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,CAAA;CACxC;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAqBhG;AAED,wBAAsB,UAAU,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,UAAU,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,UAAU,CAO3F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7E;AAED,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,qBAA4B,CAAA;AAE3D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAErE;AAED,sBAAsB;AACtB,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,IAAI,GAAG,UAAU,GAC1B,UAAU,GAAG,SAAS,CAExB;AAED,iFAAiF;AACjF,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addon fiber primitives — Cordis-style dispose/reload for federated modules
|
|
3
|
+
* running inside a PWA host. Pure helpers so they unit-test without React
|
|
4
|
+
* or the Module Federation runtime.
|
|
5
|
+
*/
|
|
6
|
+
export const PURGE_ADDON_MESSAGE = 'PURGE_ADDON';
|
|
7
|
+
/**
|
|
8
|
+
* Accept every historical export shape:
|
|
9
|
+
* - `{ register, dispose? }`
|
|
10
|
+
* - `{ default: { register, dispose? } }` (definePlugin)
|
|
11
|
+
* - `{ default: (api) => ... }` (function module)
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePluginExports(mod) {
|
|
14
|
+
if (!mod)
|
|
15
|
+
return {};
|
|
16
|
+
const fromDefault = mod.default && typeof mod.default === 'object'
|
|
17
|
+
? mod.default
|
|
18
|
+
: undefined;
|
|
19
|
+
const registerFn = typeof mod.register === 'function'
|
|
20
|
+
? mod.register
|
|
21
|
+
: typeof fromDefault?.register === 'function'
|
|
22
|
+
? fromDefault.register.bind(fromDefault)
|
|
23
|
+
: typeof mod.default === 'function'
|
|
24
|
+
? mod.default
|
|
25
|
+
: undefined;
|
|
26
|
+
const disposeFn = typeof mod.dispose === 'function'
|
|
27
|
+
? mod.dispose
|
|
28
|
+
: typeof fromDefault?.dispose === 'function'
|
|
29
|
+
? fromDefault.dispose.bind(fromDefault)
|
|
30
|
+
: undefined;
|
|
31
|
+
return { register: registerFn, dispose: disposeFn };
|
|
32
|
+
}
|
|
33
|
+
export async function runDispose(dispose) {
|
|
34
|
+
if (typeof dispose !== 'function')
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
await Promise.resolve(dispose());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* a failing disposer must not block the next fiber mount */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function composeDisposables(...fns) {
|
|
44
|
+
const list = fns.filter((f) => typeof f === 'function');
|
|
45
|
+
return async () => {
|
|
46
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
47
|
+
await runDispose(list[i]);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* True when a Cache API / SW request is a federated frontend asset of `addonKey`.
|
|
53
|
+
* Matches both `/api/addons/<key>/frontend` and `/api/metacore/addons/<key>/frontend`.
|
|
54
|
+
*/
|
|
55
|
+
export function isAddonFrontendCacheUrl(url, addonKey) {
|
|
56
|
+
if (!addonKey || !url)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
const path = new URL(url, 'http://local.invalid').pathname;
|
|
60
|
+
const escaped = addonKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
61
|
+
return new RegExp(`/api/(metacore/)?addons/${escaped}/frontend(/|\\.js)`).test(path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Drop cached federation assets for one addon. Primary path is the page-side
|
|
69
|
+
* Cache API (works even before the SW learns `PURGE_ADDON`). Also posts the
|
|
70
|
+
* message so a current SW can drop its own entries.
|
|
71
|
+
*
|
|
72
|
+
* Never unregisters the SW and never deletes other caches — L1, not L2.
|
|
73
|
+
*/
|
|
74
|
+
export async function purgeAddonFrontendCache(addonKey) {
|
|
75
|
+
if (!addonKey)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof caches !== 'undefined') {
|
|
79
|
+
const names = await caches.keys();
|
|
80
|
+
await Promise.all(names.map(async (name) => {
|
|
81
|
+
if (!name.includes('addon-federation'))
|
|
82
|
+
return;
|
|
83
|
+
const cache = await caches.open(name);
|
|
84
|
+
const requests = await cache.keys();
|
|
85
|
+
await Promise.all(requests
|
|
86
|
+
.filter((req) => isAddonFrontendCacheUrl(req.url, addonKey))
|
|
87
|
+
.map((req) => cache.delete(req)));
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* private mode / no Cache API */
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
96
|
+
const controller = navigator.serviceWorker.controller;
|
|
97
|
+
controller?.postMessage({ type: PURGE_ADDON_MESSAGE, key: addonKey });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* no SW */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Test seam: which remote URL is currently registered per federation scope. */
|
|
105
|
+
export const remoteEntryByScope = new Map();
|
|
106
|
+
export function shouldReregisterRemote(scope, url) {
|
|
107
|
+
return remoteEntryByScope.get(scope) !== url;
|
|
108
|
+
}
|
|
109
|
+
export function markRemoteRegistered(scope, url) {
|
|
110
|
+
remoteEntryByScope.set(scope, url);
|
|
111
|
+
}
|
|
112
|
+
/** @internal tests */
|
|
113
|
+
export function resetRemoteRegistry() {
|
|
114
|
+
remoteEntryByScope.clear();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Safe no-op when `register()` returns void. Used by AddonLoader to treat
|
|
118
|
+
* both historical and fiber-style plugins uniformly.
|
|
119
|
+
*/
|
|
120
|
+
export function disposableFromRegisterResult(result) {
|
|
121
|
+
return typeof result === 'function' ? result : undefined;
|
|
122
|
+
}
|
package/dist/addon-loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk';
|
|
1
|
+
import type { AddonAPI, AddonLayout, Registry } from '@asteby/metacore-sdk';
|
|
2
2
|
export interface AddonLoaderProps {
|
|
3
3
|
/** Unique key of the addon — maps to the federation container name. */
|
|
4
4
|
scope: string;
|
|
@@ -8,9 +8,25 @@ export interface AddonLoaderProps {
|
|
|
8
8
|
module?: string;
|
|
9
9
|
/** Host-provided API passed to the addon's register() call. */
|
|
10
10
|
api: AddonAPI;
|
|
11
|
+
/**
|
|
12
|
+
* Host registry used to {@link Registry.unbind} this addon's contributions
|
|
13
|
+
* on dispose. Optional so legacy hosts keep compiling; without it, fiber
|
|
14
|
+
* remounts leak routes/actions.
|
|
15
|
+
*/
|
|
16
|
+
hostRegistry?: Registry;
|
|
17
|
+
/**
|
|
18
|
+
* Addon key for SW cache purge. Defaults to `api.manifest.key`.
|
|
19
|
+
*/
|
|
20
|
+
addonKey?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Registry owner passed to {@link Registry.unbind}. Defaults to `addonKey`.
|
|
23
|
+
* Immersive `./plugin` fibers use `${key}::view` so they don't wipe the
|
|
24
|
+
* shell `./register` contributions of the same addon.
|
|
25
|
+
*/
|
|
26
|
+
unbindKey?: string;
|
|
11
27
|
/** Optional rendering while loading. */
|
|
12
28
|
fallback?: React.ReactNode;
|
|
13
|
-
/** Called once the addon has successfully registered. */
|
|
29
|
+
/** Called once the addon has successfully registered (including re-register). */
|
|
14
30
|
onReady?: () => void;
|
|
15
31
|
/** Called if loading fails. */
|
|
16
32
|
onError?: (err: Error) => void;
|
|
@@ -28,5 +44,5 @@ export interface AddonLoaderProps {
|
|
|
28
44
|
layout?: AddonLayout;
|
|
29
45
|
children?: React.ReactNode;
|
|
30
46
|
}
|
|
31
|
-
export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react").JSX.Element;
|
|
47
|
+
export declare function AddonLoader({ scope, url, module, api, hostRegistry, addonKey, unbindKey, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react").JSX.Element;
|
|
32
48
|
//# sourceMappingURL=addon-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAc3E,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAA;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AA4DD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,+BA6DlB"}
|