@asteby/metacore-runtime-react 34.1.0 → 35.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 +7 -86
- package/dist/action-modal-dispatcher.d.ts +4 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +102 -23
- package/dist/addon-loader.d.ts +1 -1
- package/dist/addon-loader.d.ts.map +1 -1
- package/dist/addon-loader.js +8 -1
- package/dist/color-picker-field.d.ts +13 -0
- package/dist/color-picker-field.d.ts.map +1 -0
- package/dist/color-picker-field.js +181 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +99 -61
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +23 -8
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +3 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +6 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/permissions-manager.d.ts +5 -0
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +67 -23
- package/dist/use-print-document.d.ts +10 -1
- package/dist/use-print-document.d.ts.map +1 -1
- package/dist/use-print-document.js +28 -1
- package/package.json +1 -1
- package/src/__tests__/color-picker-field.test.tsx +18 -0
- package/src/__tests__/filename-from-disposition.test.ts +30 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +37 -0
- package/src/__tests__/record-detail-display.test.tsx +30 -0
- package/src/action-modal-dispatcher.tsx +110 -22
- package/src/addon-loader.tsx +7 -1
- package/src/color-picker-field.tsx +296 -0
- package/src/dialogs/dynamic-record.tsx +139 -65
- package/src/dynamic-columns.tsx +28 -12
- package/src/dynamic-form.tsx +9 -1
- package/src/dynamic-select-field.tsx +7 -3
- package/src/index.ts +2 -0
- package/src/permissions-manager.tsx +98 -39
- package/src/use-print-document.ts +38 -2
|
@@ -30,6 +30,8 @@ import { toast } from 'sonner';
|
|
|
30
30
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
31
31
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Separator, Skeleton, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
|
|
32
32
|
import { DynamicIcon } from './dynamic-icon';
|
|
33
|
+
import { IconPickerField } from './icon-picker-field';
|
|
34
|
+
import { ColorPickerField, DEFAULT_ROLE_COLOR } from './color-picker-field';
|
|
33
35
|
// ---------------------------------------------------------------------------
|
|
34
36
|
// Pure helpers (exported for hosts/tests)
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -133,17 +135,29 @@ function slugify(label) {
|
|
|
133
135
|
.replace(/[^a-z0-9]+/g, '_')
|
|
134
136
|
.replace(/^_+|_+$/g, '');
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
]
|
|
138
|
+
/** Suggest a Lucide icon from the role label (create-dialog default). */
|
|
139
|
+
export function suggestRoleIcon(label) {
|
|
140
|
+
const s = fold(label);
|
|
141
|
+
const rules = [
|
|
142
|
+
[/cajer|cash|teller|bank/, 'Banknote'],
|
|
143
|
+
[/vend|seller|sales|shop|mostrador/, 'ShoppingBag'],
|
|
144
|
+
[/mecanic|technic|wrench|taller/, 'Wrench'],
|
|
145
|
+
[/jefe|lead|manager|gerente|supervisor/, 'HardHat'],
|
|
146
|
+
[/almacen|ware|stock|invent/, 'Warehouse'],
|
|
147
|
+
[/compr|purch|buyer/, 'Truck'],
|
|
148
|
+
[/contad|account|ledger/, 'Calculator'],
|
|
149
|
+
[/nomina|payroll|sueldo/, 'Wallet'],
|
|
150
|
+
[/rh|hr|human|emplead|people/, 'Users'],
|
|
151
|
+
[/fleet|flota|vehic/, 'Car'],
|
|
152
|
+
[/admin|dueño|owner/, 'ShieldCheck'],
|
|
153
|
+
[/view|observa|lectura|read/, 'Eye'],
|
|
154
|
+
];
|
|
155
|
+
for (const [re, icon] of rules) {
|
|
156
|
+
if (re.test(s))
|
|
157
|
+
return icon;
|
|
158
|
+
}
|
|
159
|
+
return 'Shield';
|
|
160
|
+
}
|
|
147
161
|
/**
|
|
148
162
|
* Normalize whatever `loadModules` returned into the canonical grouped shape.
|
|
149
163
|
*
|
|
@@ -237,7 +251,14 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
237
251
|
const [moduleOpen, setModuleOpen] = React.useState(false);
|
|
238
252
|
// Pending role switch while there are unsaved changes.
|
|
239
253
|
const [pendingRoleId, setPendingRoleId] = React.useState(null);
|
|
240
|
-
const [roleDialog, setRoleDialog] = React.useState({
|
|
254
|
+
const [roleDialog, setRoleDialog] = React.useState({
|
|
255
|
+
open: false,
|
|
256
|
+
mode: 'create',
|
|
257
|
+
label: '',
|
|
258
|
+
color: DEFAULT_ROLE_COLOR,
|
|
259
|
+
icon: 'Shield',
|
|
260
|
+
grantAll: false,
|
|
261
|
+
});
|
|
241
262
|
const [roleSaving, setRoleSaving] = React.useState(false);
|
|
242
263
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
243
264
|
const [deleting, setDeleting] = React.useState(false);
|
|
@@ -389,6 +410,7 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
389
410
|
name: slugify(label),
|
|
390
411
|
label,
|
|
391
412
|
color: roleDialog.color,
|
|
413
|
+
icon: roleDialog.icon || suggestRoleIcon(label),
|
|
392
414
|
});
|
|
393
415
|
const rs = await loadRoles();
|
|
394
416
|
setRoles(rs);
|
|
@@ -412,6 +434,7 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
412
434
|
name: activeRole.name,
|
|
413
435
|
label,
|
|
414
436
|
color: roleDialog.color,
|
|
437
|
+
icon: roleDialog.icon || suggestRoleIcon(label),
|
|
415
438
|
});
|
|
416
439
|
await refreshRoles(activeRole.id);
|
|
417
440
|
toast.success('Rol actualizado');
|
|
@@ -451,7 +474,8 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
451
474
|
open: true,
|
|
452
475
|
mode: 'edit',
|
|
453
476
|
label: activeRole.label || activeRole.name,
|
|
454
|
-
color: activeRole.color ||
|
|
477
|
+
color: activeRole.color || DEFAULT_ROLE_COLOR,
|
|
478
|
+
icon: activeRole.icon || suggestRoleIcon(activeRole.label || activeRole.name),
|
|
455
479
|
grantAll: false,
|
|
456
480
|
});
|
|
457
481
|
};
|
|
@@ -476,18 +500,21 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
476
500
|
open: true,
|
|
477
501
|
mode: 'create',
|
|
478
502
|
label: '',
|
|
479
|
-
color:
|
|
503
|
+
color: DEFAULT_ROLE_COLOR,
|
|
504
|
+
icon: 'Shield',
|
|
480
505
|
grantAll: false,
|
|
481
|
-
}), children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), " Nuevo rol"] })), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(true), disabled: checksDisabled || !allCatalogCapabilities.length, title: "Marca todos los permisos de todos los m\u00F3dulos", children: [_jsx(CheckCheck, { className: "mr-1.5 h-4 w-4" }), " Otorgar todo"] }), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(false), disabled: checksDisabled || !draft?.size, title: "Quita todos los permisos del rol", children: [_jsx(Eraser, { className: "mr-1.5 h-4 w-4" }), " Quitar todo"] }), _jsxs(Button, { onClick: handleSave, disabled: !dirty || saving || !activeRole, className: "bg-emerald-600 text-white hover:bg-emerald-700", children: [_jsx(Save, { className: "mr-1.5 h-4 w-4" }), saving ? 'Guardando…' : 'Guardar permisos'] })] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Rol" }), _jsx(CardDescription, { children: "Selecciona el rol a configurar." })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: roleOpen, onOpenChange: setRoleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": roleOpen, className: "min-w-0 flex-1 justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeRole && (_jsx("span", { className: "h-
|
|
482
|
-
background: activeRole.color || '#
|
|
483
|
-
|
|
506
|
+
}), children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), " Nuevo rol"] })), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(true), disabled: checksDisabled || !allCatalogCapabilities.length, title: "Marca todos los permisos de todos los m\u00F3dulos", children: [_jsx(CheckCheck, { className: "mr-1.5 h-4 w-4" }), " Otorgar todo"] }), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(false), disabled: checksDisabled || !draft?.size, title: "Quita todos los permisos del rol", children: [_jsx(Eraser, { className: "mr-1.5 h-4 w-4" }), " Quitar todo"] }), _jsxs(Button, { onClick: handleSave, disabled: !dirty || saving || !activeRole, className: "bg-emerald-600 text-white hover:bg-emerald-700", children: [_jsx(Save, { className: "mr-1.5 h-4 w-4" }), saving ? 'Guardando…' : 'Guardar permisos'] })] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Rol" }), _jsx(CardDescription, { children: "Selecciona el rol a configurar." })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: roleOpen, onOpenChange: setRoleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": roleOpen, className: "min-w-0 flex-1 justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeRole && (_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
|
|
507
|
+
background: `${activeRole.color || '#64748b'}22`,
|
|
508
|
+
color: activeRole.color || '#64748b',
|
|
509
|
+
}, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: activeRole.icon || 'Shield', className: "h-3.5 w-3.5" }) })), _jsx("span", { className: "truncate", children: activeRole
|
|
484
510
|
? activeRole.label || activeRole.name
|
|
485
511
|
: 'Seleccionar rol…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar rol\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "Sin resultados." }), _jsx(CommandGroup, { children: (roles ?? []).map((role) => (_jsxs(CommandItem, { value: `${role.label || ''} ${role.name}`, onSelect: () => {
|
|
486
512
|
requestRoleSwitch(role.id);
|
|
487
513
|
setRoleOpen(false);
|
|
488
|
-
}, children: [_jsx("span", { className: "mr-2 h-
|
|
489
|
-
background: role.color || '#
|
|
490
|
-
|
|
514
|
+
}, children: [_jsx("span", { className: "mr-2 flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
|
|
515
|
+
background: `${role.color || '#64748b'}22`,
|
|
516
|
+
color: role.color || '#64748b',
|
|
517
|
+
}, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: role.icon || 'Shield', className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "truncate", children: role.label || role.name }), role.id === activeRoleId && (_jsx(Check, { className: "ml-auto h-4 w-4" }))] }, role.id))) })] })] }) })] }), updateRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0", "aria-label": "Editar rol", disabled: !activeRole, onClick: openEditRole, children: _jsx(Pencil, { className: "h-4 w-4" }) })), deleteRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 text-destructive hover:text-destructive", "aria-label": "Eliminar rol", disabled: !activeRole, onClick: () => setDeleteOpen(true), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), (general?.length ?? 0) > 0 && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-semibold", children: "Permisos Generales" }), _jsx("div", { className: "flex flex-col gap-2", children: general.map((g) => (_jsx(CapabilityCheck, { checked: draft?.has(g.key) ?? false, disabled: checksDisabled, onToggle: () => toggleCapability(g.key), label: g.label, description: g.description }, g.key))) })] })] }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "M\u00F3dulo" }), _jsx(CardDescription, { children: "Elige el m\u00F3dulo cuyas acciones quieres configurar." })] }), _jsx(CardContent, { children: _jsxs(Popover, { open: moduleOpen, onOpenChange: setModuleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": moduleOpen, className: "w-full justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon ||
|
|
491
518
|
(activeModule.kind === 'screen'
|
|
492
519
|
? 'Eye'
|
|
493
520
|
: 'Square'), className: "h-4 w-4 shrink-0 opacity-70" })), _jsx("span", { className: "truncate", children: activeModule
|
|
@@ -506,9 +533,26 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
506
533
|
: 'Configura los permisos del módulo seleccionado.' })] }), activeRole && activeModule && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Badge, { variant: "secondary", className: "tabular-nums", children: [moduleGranted, "/", moduleTotal] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === moduleTotal, onClick: () => setModuleAll(true), children: [_jsx(CheckCheck, { className: "mr-1.5 h-3.5 w-3.5" }), " Marcar todo"] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === 0, onClick: () => setModuleAll(false), children: [_jsx(Eraser, { className: "mr-1.5 h-3.5 w-3.5" }), " Limpiar"] })] }))] }) }), _jsx(CardContent, { children: !activeRole ? (_jsx(EmptyHint, { text: "Selecciona un rol para configurar sus permisos." })) : loadingPerms ? (_jsx("div", { className: "grid gap-2 sm:grid-cols-2", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Skeleton, { className: "h-14 w-full" }, i))) })) : !activeModule ? (_jsx(EmptyHint, { text: "Selecciona un m\u00F3dulo de la lista para ver sus acciones." })) : (_jsx(ModuleActionsPanel, { module: activeModule, draft: draft, checksDisabled: checksDisabled, onToggle: toggleCapability })) })] })] }), _jsx(AlertDialog, { open: pendingRoleId !== null, onOpenChange: (open) => !open && setPendingRoleId(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Cambios sin guardar" }), _jsx(AlertDialogDescription, { children: "Tienes cambios sin guardar en este rol. Si cambias de rol se descartar\u00E1n." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancelar" }), _jsx(AlertDialogAction, { onClick: () => {
|
|
507
534
|
setActiveRoleId(pendingRoleId);
|
|
508
535
|
setPendingRoleId(null);
|
|
509
|
-
}, children: "Descartar y cambiar" })] })] }) }), _jsx(Dialog, { open: roleDialog.open, onOpenChange: (open) => setRoleDialog((d) => ({ ...d, open })), children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: roleDialog.mode === 'create' ? 'Nuevo rol' : 'Editar rol' }) }), _jsxs("div", { className: "flex flex-col gap-4 py-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "pm-role-name", children: "Nombre del rol" }), _jsx(Input, { id: "pm-role-name", value: roleDialog.label, placeholder: "Ej. Cajero", onChange: (e) =>
|
|
510
|
-
|
|
511
|
-
|
|
536
|
+
}, children: "Descartar y cambiar" })] })] }) }), _jsx(Dialog, { open: roleDialog.open, onOpenChange: (open) => setRoleDialog((d) => ({ ...d, open })), children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: roleDialog.mode === 'create' ? 'Nuevo rol' : 'Editar rol' }) }), _jsxs("div", { className: "flex flex-col gap-4 py-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "pm-role-name", children: "Nombre del rol" }), _jsx(Input, { id: "pm-role-name", value: roleDialog.label, placeholder: "Ej. Cajero", onChange: (e) => {
|
|
537
|
+
const label = e.target.value;
|
|
538
|
+
setRoleDialog((d) => ({
|
|
539
|
+
...d,
|
|
540
|
+
label,
|
|
541
|
+
icon: d.icon === suggestRoleIcon(d.label) ||
|
|
542
|
+
d.icon === 'Shield' ||
|
|
543
|
+
!d.icon
|
|
544
|
+
? suggestRoleIcon(label)
|
|
545
|
+
: d.icon,
|
|
546
|
+
}));
|
|
547
|
+
} })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "\u00CDcono" }), _jsx(IconPickerField, { field: {
|
|
548
|
+
key: 'icon',
|
|
549
|
+
label: 'Ícono',
|
|
550
|
+
type: 'text',
|
|
551
|
+
widget: 'icon',
|
|
552
|
+
}, value: roleDialog.icon, onChange: (v) => setRoleDialog((d) => ({
|
|
553
|
+
...d,
|
|
554
|
+
icon: typeof v === 'string' && v ? v : 'Shield',
|
|
555
|
+
})) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Color" }), _jsx(ColorPickerField, { "aria-label": "Color del rol", value: roleDialog.color, onChange: (color) => setRoleDialog((d) => ({ ...d, color })) })] }), roleDialog.mode === 'create' && (_jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded-lg border p-3 text-sm hover:bg-muted/40", children: [_jsx("input", { type: "checkbox", className: "mt-0.5 size-4 accent-emerald-600", checked: roleDialog.grantAll, onChange: (e) => setRoleDialog((d) => ({
|
|
512
556
|
...d,
|
|
513
557
|
grantAll: e.target.checked,
|
|
514
558
|
})) }), _jsxs("span", { children: [_jsx("span", { className: "font-medium", children: "Otorgar todos los permisos" }), _jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: "Acceso completo a todos los m\u00F3dulos del cat\u00E1logo (como un admin). Puedes quitar permisos despu\u00E9s." })] })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRoleDialog((d) => ({ ...d, open: false })), disabled: roleSaving, children: "Cancelar" }), _jsx(Button, { onClick: handleRoleSubmit, disabled: roleSaving || !roleDialog.label.trim(), children: roleSaving
|
|
@@ -12,9 +12,18 @@ export interface PrintDocumentArgs {
|
|
|
12
12
|
* open → open the PDF in a new tab (user prints from the viewer).
|
|
13
13
|
*/
|
|
14
14
|
mode?: 'print' | 'download' | 'open';
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Hint filename for download mode. Prefer leaving this unset: the server
|
|
17
|
+
* expands `{{record.*}}` into Content-Disposition. A raw template string
|
|
18
|
+
* (e.g. "cfdi-{{record.number}}.pdf") must NOT be used as a.download —
|
|
19
|
+
* that is how downloads end up literally named with mustache braces.
|
|
20
|
+
*/
|
|
16
21
|
filename?: string;
|
|
17
22
|
}
|
|
23
|
+
/** Parse filename from Content-Disposition (RFC 5987 / quoted). */
|
|
24
|
+
export declare function filenameFromContentDisposition(header: string | undefined | null): string | undefined;
|
|
25
|
+
/** True when a caller passed an unexpanded mustache template as filename. */
|
|
26
|
+
export declare function looksLikeFilenameTemplate(name: string | undefined): boolean;
|
|
18
27
|
/**
|
|
19
28
|
* Returns a `printDocument(args)` callback. Resolves once the PDF has been
|
|
20
29
|
* fetched and the browser action (print/download/open) has been kicked off;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-print-document.d.ts","sourceRoot":"","sources":["../src/use-print-document.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAA;IACpC
|
|
1
|
+
{"version":3,"file":"use-print-document.d.ts","sourceRoot":"","sources":["../src/use-print-document.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAA;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,mEAAmE;AACnE,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAapG;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,0CASrB,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC,CAmE7C"}
|
|
@@ -18,6 +18,27 @@
|
|
|
18
18
|
// this hook constructs no client of its own.
|
|
19
19
|
import { useCallback } from 'react';
|
|
20
20
|
import { useApi } from './api-context';
|
|
21
|
+
/** Parse filename from Content-Disposition (RFC 5987 / quoted). */
|
|
22
|
+
export function filenameFromContentDisposition(header) {
|
|
23
|
+
if (!header)
|
|
24
|
+
return undefined;
|
|
25
|
+
const star = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header);
|
|
26
|
+
if (star?.[1]) {
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(star[1].trim().replace(/^"|"$/g, ''));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return star[1].trim().replace(/^"|"$/g, '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const plain = /filename\s*=\s*"([^"]+)"|filename\s*=\s*([^;]+)/i.exec(header);
|
|
35
|
+
const raw = (plain?.[1] ?? plain?.[2] ?? '').trim();
|
|
36
|
+
return raw || undefined;
|
|
37
|
+
}
|
|
38
|
+
/** True when a caller passed an unexpanded mustache template as filename. */
|
|
39
|
+
export function looksLikeFilenameTemplate(name) {
|
|
40
|
+
return !!name && /\{\{/.test(name);
|
|
41
|
+
}
|
|
21
42
|
/**
|
|
22
43
|
* Returns a `printDocument(args)` callback. Resolves once the PDF has been
|
|
23
44
|
* fetched and the browser action (print/download/open) has been kicked off;
|
|
@@ -35,9 +56,15 @@ export function usePrintDocument() {
|
|
|
35
56
|
const blobUrl = URL.createObjectURL(blob);
|
|
36
57
|
const cleanup = () => setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
|
37
58
|
if (mode === 'download') {
|
|
59
|
+
const headers = res.headers || {};
|
|
60
|
+
const fromHeader = filenameFromContentDisposition(headers['content-disposition'] || headers['Content-Disposition']) || undefined;
|
|
61
|
+
// Prefer server-expanded name; never use a raw {{record.*}} template.
|
|
62
|
+
const downloadName = fromHeader ||
|
|
63
|
+
(!looksLikeFilenameTemplate(filename) ? filename : undefined) ||
|
|
64
|
+
`${key}.pdf`;
|
|
38
65
|
const a = document.createElement('a');
|
|
39
66
|
a.href = blobUrl;
|
|
40
|
-
a.download =
|
|
67
|
+
a.download = downloadName;
|
|
41
68
|
document.body.appendChild(a);
|
|
42
69
|
a.click();
|
|
43
70
|
a.remove();
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { normalizeHex, DEFAULT_ROLE_COLOR } from '../color-picker-field'
|
|
3
|
+
|
|
4
|
+
describe('normalizeHex', () => {
|
|
5
|
+
it('accepts #rgb and expands', () => {
|
|
6
|
+
expect(normalizeHex('#0af')).toBe('#00aaff')
|
|
7
|
+
})
|
|
8
|
+
it('lowercases #rrggbb', () => {
|
|
9
|
+
expect(normalizeHex('#3B82F6')).toBe('#3b82f6')
|
|
10
|
+
})
|
|
11
|
+
it('rejects garbage', () => {
|
|
12
|
+
expect(normalizeHex('blue')).toBe('')
|
|
13
|
+
expect(normalizeHex('#gg0000')).toBe('')
|
|
14
|
+
})
|
|
15
|
+
it('default constant is valid', () => {
|
|
16
|
+
expect(normalizeHex(DEFAULT_ROLE_COLOR)).toBe('#3b82f6')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
filenameFromContentDisposition,
|
|
4
|
+
looksLikeFilenameTemplate,
|
|
5
|
+
} from '../use-print-document'
|
|
6
|
+
|
|
7
|
+
describe('filenameFromContentDisposition', () => {
|
|
8
|
+
it('parses quoted filename', () => {
|
|
9
|
+
expect(filenameFromContentDisposition('inline; filename="cfdi-F-950.pdf"')).toBe(
|
|
10
|
+
'cfdi-F-950.pdf',
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('parses RFC 5987 filename*', () => {
|
|
15
|
+
expect(
|
|
16
|
+
filenameFromContentDisposition("attachment; filename*=UTF-8''cfdi-F%20950.pdf"),
|
|
17
|
+
).toBe('cfdi-F 950.pdf')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns undefined for empty', () => {
|
|
21
|
+
expect(filenameFromContentDisposition(undefined)).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('looksLikeFilenameTemplate', () => {
|
|
26
|
+
it('detects mustache', () => {
|
|
27
|
+
expect(looksLikeFilenameTemplate('cfdi-{{record.number}}.pdf')).toBe(true)
|
|
28
|
+
expect(looksLikeFilenameTemplate('cfdi-F-950.pdf')).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readRecordPath, scalarDefaultFromRecord, unwrapRecordScalar } from '../action-modal-dispatcher'
|
|
3
|
+
import type { ActionFieldDef } from '../types'
|
|
4
|
+
|
|
5
|
+
describe('scalarDefaultFromRecord', () => {
|
|
6
|
+
it('unwraps FK cells with value/label', () => {
|
|
7
|
+
expect(unwrapRecordScalar({ value: 'abc', label: 'Cliente SA' })).toBe('abc')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('reads dotted paths', () => {
|
|
11
|
+
const record = { fiscal_data: { forma_pago: '03' } }
|
|
12
|
+
expect(readRecordPath(record, 'fiscal_data.forma_pago')).toBe('03')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('uses defaultFromRecord string', () => {
|
|
16
|
+
const field = { key: 'forma_pago', defaultFromRecord: 'fiscal_data.forma_pago' } as ActionFieldDef & {
|
|
17
|
+
defaultFromRecord: string
|
|
18
|
+
}
|
|
19
|
+
const record = { fiscal_data: { forma_pago: '01' }, forma_pago: '99' }
|
|
20
|
+
expect(scalarDefaultFromRecord(field, record)).toBe('01')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('tries defaultFromRecord array in order', () => {
|
|
24
|
+
const field = {
|
|
25
|
+
key: 'uso_cfdi',
|
|
26
|
+
defaultFromRecord: ['fiscal_data.uso_cfdi', 'uso_cfdi'],
|
|
27
|
+
} as ActionFieldDef & { defaultFromRecord: string[] }
|
|
28
|
+
const record = { uso_cfdi: 'G03' }
|
|
29
|
+
expect(scalarDefaultFromRecord(field, record)).toBe('G03')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('falls back to record[field.key]', () => {
|
|
33
|
+
const field = { key: 'customer_id' } as ActionFieldDef
|
|
34
|
+
const record = { customer_id: '11111111-1111-4111-8111-111111111111' }
|
|
35
|
+
expect(scalarDefaultFromRecord(field, record)).toBe('11111111-1111-4111-8111-111111111111')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -110,4 +110,34 @@ describe('ViewValue — detail dialog display mapping', () => {
|
|
|
110
110
|
)
|
|
111
111
|
expect(container.textContent).toContain('—')
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
it('renders plain text external_id without a relation initials avatar', () => {
|
|
115
|
+
const { container } = render(
|
|
116
|
+
<ViewValue
|
|
117
|
+
field={{ key: 'external_id', label: 'ID externo', type: 'text' }}
|
|
118
|
+
value="6a8c931523ea7"
|
|
119
|
+
record={{}}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
expect(screen.getByText('6a8c931523ea7')).toBeTruthy()
|
|
123
|
+
// Must NOT render the InitialsAvatar lead ("6" chip) used for FKs.
|
|
124
|
+
expect(container.querySelector('[data-slot="avatar"]')).toBeNull()
|
|
125
|
+
expect(container.textContent).not.toMatch(/^6\s*6a8c/)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders nested provider_data objects as pretty JSON, not key: {…}', () => {
|
|
129
|
+
render(
|
|
130
|
+
<ViewValue
|
|
131
|
+
field={{ key: 'provider_data', label: 'Datos del proveedor', type: 'json' }}
|
|
132
|
+
value={{
|
|
133
|
+
forma_pago: '03',
|
|
134
|
+
INV: { Folio: 950, Serie: 'F' },
|
|
135
|
+
}}
|
|
136
|
+
record={{}}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
expect(screen.getByText('03')).toBeTruthy()
|
|
140
|
+
expect(screen.getByText(/"Folio": 950/)).toBeTruthy()
|
|
141
|
+
expect(screen.queryByText(/Inv:\s*\{\.\.\.\}/i)).toBeNull()
|
|
142
|
+
})
|
|
113
143
|
})
|
|
@@ -156,6 +156,65 @@ export function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<s
|
|
|
156
156
|
return rows
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// ---- scalar prefill from the acted-on record --------------------------------
|
|
160
|
+
//
|
|
161
|
+
// Row actions (stamp / refactura / cancel-with-reason) should open with the
|
|
162
|
+
// current record's values, not empty selects. Manifest declares explicit paths
|
|
163
|
+
// via `defaultFromRecord` (string or string[] fallback chain). When omitted,
|
|
164
|
+
// the field seeds from record[field.key] if present.
|
|
165
|
+
|
|
166
|
+
export function unwrapRecordScalar(value: unknown): unknown {
|
|
167
|
+
if (value === null || value === undefined) return value
|
|
168
|
+
if (typeof value !== 'object' || value instanceof Date) return value
|
|
169
|
+
if (Array.isArray(value)) return value
|
|
170
|
+
const o = value as Record<string, unknown>
|
|
171
|
+
if ('value' in o && (typeof o.value === 'string' || typeof o.value === 'number')) {
|
|
172
|
+
return o.value
|
|
173
|
+
}
|
|
174
|
+
if ('id' in o && (typeof o.id === 'string' || typeof o.id === 'number')) {
|
|
175
|
+
return o.id
|
|
176
|
+
}
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function readRecordPath(record: any, path: string): unknown {
|
|
181
|
+
if (!record || !path) return undefined
|
|
182
|
+
const parts = path.split('.')
|
|
183
|
+
let cur: any = record
|
|
184
|
+
for (const p of parts) {
|
|
185
|
+
if (cur == null || typeof cur !== 'object') return undefined
|
|
186
|
+
cur = cur[p]
|
|
187
|
+
}
|
|
188
|
+
return unwrapRecordScalar(cur)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function defaultFromRecordSpec(field: ActionFieldDef): string | string[] | undefined {
|
|
192
|
+
const f = field as ActionFieldDef & {
|
|
193
|
+
defaultFromRecord?: string | string[]
|
|
194
|
+
default_from_record?: string | string[]
|
|
195
|
+
}
|
|
196
|
+
return f.defaultFromRecord ?? f.default_from_record
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Scalar seed for one action field from the row being acted on. */
|
|
200
|
+
export function scalarDefaultFromRecord(field: ActionFieldDef, record: any): unknown {
|
|
201
|
+
if (!record) return undefined
|
|
202
|
+
const spec = defaultFromRecordSpec(field)
|
|
203
|
+
if (typeof spec === 'string') {
|
|
204
|
+
const v = readRecordPath(record, spec)
|
|
205
|
+
if (v !== undefined && v !== null && v !== '') return v
|
|
206
|
+
} else if (Array.isArray(spec)) {
|
|
207
|
+
for (const path of spec) {
|
|
208
|
+
const v = readRecordPath(record, path)
|
|
209
|
+
if (v !== undefined && v !== null && v !== '') return v
|
|
210
|
+
}
|
|
211
|
+
} else if (field.key) {
|
|
212
|
+
const v = readRecordPath(record, field.key)
|
|
213
|
+
if (v !== undefined && v !== null && v !== '') return v
|
|
214
|
+
}
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
159
218
|
export function ActionModalDispatcher({
|
|
160
219
|
open,
|
|
161
220
|
onOpenChange,
|
|
@@ -322,14 +381,17 @@ function selectPreviewColumns(columns: ColumnDefinition[] | undefined, record: a
|
|
|
322
381
|
continue
|
|
323
382
|
}
|
|
324
383
|
|
|
325
|
-
// Relación (ref / search / dynamic_select / *_id): solo si el sibling
|
|
326
|
-
// resolvió a un label legible;
|
|
384
|
+
// Relación (ref / search / dynamic_select / uuid *_id): solo si el sibling
|
|
385
|
+
// resolvió a un label legible; text `*_id` (external_id) no es FK.
|
|
386
|
+
const t = String(col.type || '').toLowerCase()
|
|
327
387
|
const isRelation =
|
|
328
388
|
!!getFieldRef(col as ActionFieldDef) ||
|
|
329
389
|
col.type === 'search' ||
|
|
330
390
|
col.type === 'relation' ||
|
|
331
391
|
(col as { widget?: string }).widget === 'dynamic_select' ||
|
|
332
|
-
(typeof col.key === 'string' &&
|
|
392
|
+
(typeof col.key === 'string' &&
|
|
393
|
+
col.key.endsWith('_id') &&
|
|
394
|
+
(t === 'uuid' || t === 'search' || t === 'relation' || t === 'dynamic_select' || t === 'belongs_to'))
|
|
333
395
|
if (isRelation) {
|
|
334
396
|
const sib = relationSiblingValue(col as any, record)
|
|
335
397
|
const label = typeof sib === 'string' ? sib : objectLabel(sib)
|
|
@@ -552,20 +614,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
552
614
|
|
|
553
615
|
useEffect(() => {
|
|
554
616
|
if (open && action.fields) {
|
|
555
|
-
|
|
556
|
-
for (const field of action.fields) {
|
|
557
|
-
if (isLineItemsField(field)) {
|
|
558
|
-
const dv = lineItemsDefault(field)
|
|
559
|
-
defaults[field.key] = isPrefillSpec(dv)
|
|
560
|
-
? buildPrefillRows(dv, record)
|
|
561
|
-
: Array.isArray(dv)
|
|
562
|
-
? dv
|
|
563
|
-
: []
|
|
564
|
-
continue
|
|
565
|
-
}
|
|
566
|
-
defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '')
|
|
567
|
-
}
|
|
568
|
-
setFormData(defaults)
|
|
617
|
+
setFormData(buildFieldDefaults(action.fields, record))
|
|
569
618
|
setFieldErrors({})
|
|
570
619
|
}
|
|
571
620
|
}, [open, action.fields, record])
|
|
@@ -632,6 +681,8 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
632
681
|
() => (action.fields ?? []).some(isLineItemsField),
|
|
633
682
|
[action.fields],
|
|
634
683
|
)
|
|
684
|
+
const embedRelations =
|
|
685
|
+
hasLineItems || !!(action as ActionMetadata & { embedRelations?: boolean }).embedRelations
|
|
635
686
|
const explicitWidth = (action as unknown as { modalWidth?: number | string }).modalWidth
|
|
636
687
|
const widthPx =
|
|
637
688
|
explicitWidth != null
|
|
@@ -657,7 +708,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
657
708
|
<DynamicIcon name={action.icon} className="h-5 w-5" />
|
|
658
709
|
{tl(action.label)}
|
|
659
710
|
</DialogTitle>
|
|
660
|
-
{action.confirmMessage && <DialogDescription>{action.confirmMessage}</DialogDescription>}
|
|
711
|
+
{action.confirmMessage && <DialogDescription>{tl(action.confirmMessage)}</DialogDescription>}
|
|
661
712
|
</DialogHeader>
|
|
662
713
|
{/* Scrollable body. The shared FieldGrid lays scalar fields out
|
|
663
714
|
in two responsive columns (single column on phones); line-items
|
|
@@ -676,14 +727,14 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
676
727
|
<FieldLabel htmlFor={field.key} required={field.required}>
|
|
677
728
|
{tl(field.label)}
|
|
678
729
|
</FieldLabel>
|
|
679
|
-
{renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData)}
|
|
730
|
+
{renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData, record)}
|
|
680
731
|
{fieldErrors[field.key] && (
|
|
681
732
|
<p className="text-destructive text-xs mt-1">{fieldErrors[field.key]}</p>
|
|
682
733
|
)}
|
|
683
734
|
</FieldCell>
|
|
684
735
|
)
|
|
685
736
|
})}
|
|
686
|
-
{relations.length > 0 && (
|
|
737
|
+
{embedRelations && relations.length > 0 && (
|
|
687
738
|
<FieldCell fullWidth>
|
|
688
739
|
{/* Igual que el modal de registro: solo las
|
|
689
740
|
relaciones de composición se embeben. */}
|
|
@@ -725,6 +776,11 @@ function buildFieldDefaults(fields: ActionFieldDef[], record: any): Record<strin
|
|
|
725
776
|
: []
|
|
726
777
|
continue
|
|
727
778
|
}
|
|
779
|
+
const fromRecord = scalarDefaultFromRecord(field, record)
|
|
780
|
+
if (fromRecord !== undefined && fromRecord !== null && fromRecord !== '') {
|
|
781
|
+
defaults[field.key] = fromRecord
|
|
782
|
+
continue
|
|
783
|
+
}
|
|
728
784
|
defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '')
|
|
729
785
|
}
|
|
730
786
|
return defaults
|
|
@@ -881,7 +937,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
881
937
|
<FieldLabel htmlFor={field.key} required={field.required}>
|
|
882
938
|
{tl(field.label)}
|
|
883
939
|
</FieldLabel>
|
|
884
|
-
{renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData)}
|
|
940
|
+
{renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData, record)}
|
|
885
941
|
</FieldCell>
|
|
886
942
|
)
|
|
887
943
|
})}
|
|
@@ -922,6 +978,29 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
922
978
|
)
|
|
923
979
|
}
|
|
924
980
|
|
|
981
|
+
function seedOptionFromRecord(
|
|
982
|
+
field: ActionFieldDef,
|
|
983
|
+
value: any,
|
|
984
|
+
record?: Record<string, any>,
|
|
985
|
+
): import('./use-options-resolver').ResolvedOption | undefined {
|
|
986
|
+
if (!record || !field.key.endsWith('_id')) return undefined
|
|
987
|
+
const siblingKey = field.key.replace(/_id$/, '')
|
|
988
|
+
const sib = record[siblingKey]
|
|
989
|
+
if (!sib || typeof sib !== 'object') return undefined
|
|
990
|
+
const label = (sib as any).label ?? (sib as any).name ?? ''
|
|
991
|
+
if (!label && !(sib as any).image) return undefined
|
|
992
|
+
const id = String((sib as any).value ?? (sib as any).id ?? value ?? '')
|
|
993
|
+
return {
|
|
994
|
+
id,
|
|
995
|
+
value: id,
|
|
996
|
+
label: String(label),
|
|
997
|
+
name: String(label),
|
|
998
|
+
image: (sib as any).image,
|
|
999
|
+
color: (sib as any).color,
|
|
1000
|
+
icon: (sib as any).icon,
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
925
1004
|
function renderField(
|
|
926
1005
|
field: ActionFieldDef,
|
|
927
1006
|
value: any,
|
|
@@ -931,6 +1010,7 @@ function renderField(
|
|
|
931
1010
|
// fields. Omitted by callers that have no surrounding form (the field is
|
|
932
1011
|
// then treated as having no resolvable dependency).
|
|
933
1012
|
formValues?: Record<string, any>,
|
|
1013
|
+
record?: Record<string, any>,
|
|
934
1014
|
) {
|
|
935
1015
|
// Repeatable line-items group → row grid (value is an array of row objects).
|
|
936
1016
|
// The header form values flow in so a cell can depend on a header field.
|
|
@@ -948,7 +1028,15 @@ function renderField(
|
|
|
948
1028
|
const dependsValue = getDependsOn(field)
|
|
949
1029
|
? resolveDependsValue(field, formValues)
|
|
950
1030
|
: undefined
|
|
951
|
-
return
|
|
1031
|
+
return (
|
|
1032
|
+
<DynamicSelectField
|
|
1033
|
+
field={field}
|
|
1034
|
+
value={value}
|
|
1035
|
+
onChange={onChange}
|
|
1036
|
+
dependsValue={dependsValue}
|
|
1037
|
+
seedOption={seedOptionFromRecord(field, value, record)}
|
|
1038
|
+
/>
|
|
1039
|
+
)
|
|
952
1040
|
}
|
|
953
1041
|
// File upload → themed picker that POSTs the file to the host upload
|
|
954
1042
|
// endpoint and stores the returned url/path. Kept in sync with DynamicForm.
|
package/src/addon-loader.tsx
CHANGED
|
@@ -199,7 +199,13 @@ export function AddonLoader({
|
|
|
199
199
|
}, [scope, url, module, addonKey, unbindKey, hostRegistry])
|
|
200
200
|
|
|
201
201
|
if (status === 'loading') return <>{fallback}</>
|
|
202
|
-
if (status === 'error')
|
|
202
|
+
if (status === 'error') {
|
|
203
|
+
// Hosts that pass `onError` (toast + telemetry) should not also paint
|
|
204
|
+
// a second, persistent inline banner — DynamicAddonLoaders mounts one
|
|
205
|
+
// fiber per installed addon and a visible error stack reads as a broken
|
|
206
|
+
// shell even when only one remote failed transiently.
|
|
207
|
+
if (onError) return null
|
|
203
208
|
return <div className="text-sm text-red-500">Addon load error: {error?.message}</div>
|
|
209
|
+
}
|
|
204
210
|
return <>{children}</>
|
|
205
211
|
}
|