@asteby/metacore-runtime-react 28.7.0 → 29.0.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 +43 -0
- package/dist/action-modal-dispatcher.js +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +13 -1
- package/dist/dynamic-relation.d.ts +4 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +80 -16
- package/dist/dynamic-relations.d.ts +22 -1
- package/dist/dynamic-relations.d.ts.map +1 -1
- package/dist/dynamic-relations.js +16 -3
- package/dist/entity-select.d.ts +43 -0
- package/dist/entity-select.d.ts.map +1 -0
- package/dist/entity-select.js +124 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/print-document-button.d.ts +21 -0
- package/dist/print-document-button.d.ts.map +1 -0
- package/dist/print-document-button.js +32 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-print-document.d.ts +25 -0
- package/dist/use-print-document.d.ts.map +1 -0
- package/dist/use-print-document.js +79 -0
- package/package.json +3 -3
- package/src/__tests__/relation-embed-gate.test.tsx +110 -0
- package/src/action-modal-dispatcher.tsx +3 -1
- package/src/dialogs/dynamic-record.tsx +6 -1
- package/src/dynamic-columns.tsx +14 -1
- package/src/dynamic-relation.tsx +121 -23
- package/src/dynamic-relations.tsx +34 -2
- package/src/entity-select.tsx +316 -0
- package/src/index.ts +5 -0
- package/src/print-document-button.tsx +75 -0
- package/src/types.ts +9 -0
- package/src/use-print-document.ts +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 29.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 13ef5ed: Subtablas de relación: embebido opt-in (`embed`) + paginación y búsqueda
|
|
8
|
+
|
|
9
|
+
El modal de registro embebía TODAS las relaciones `one_to_many` del modelo, y cada subtabla pedía el modelo hijo completo (`/data/<hijo>?f_<fk>=eq:<id>`, sin `page`/`per_page`). Abrir "Editar Almacén" cargaba miles de existencias y traspasos dentro de un formulario.
|
|
10
|
+
|
|
11
|
+
- `DynamicRelations` acepta `embedOnly`: filtra por `RelationMeta.embed` (nuevo, servido por el kernel). Lo pasan los dos modales — el de registro y el de acción. La página de detalle autónoma sigue mostrando todas las relaciones.
|
|
12
|
+
- `RelationMeta.embed?: boolean` en los tipos; ausente = no embebe (tolera kernels que aún no lo sirven).
|
|
13
|
+
- `OneToManyRelation` pagina server-side (`page`/`per_page`, 25 por página) con scroll infinito, buscador server-side (`search`, con debounce), contador "cargadas de total" y scroll propio acotado en alto.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [e801041]
|
|
18
|
+
- @asteby/metacore-ui@2.13.0
|
|
19
|
+
|
|
20
|
+
## 28.8.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 3db2705: Nuevo primitivo `EntitySelect`: combobox async de un modelo relacionado con las dos affordances del modal dinámico — "+" para CREAR (sin selección) que auto-selecciona el creado, y lápiz para EDITAR el seleccionado. Ambas gateadas por permisos del kernel (useCan: `<model>.create`/`.update`) y 100% dinámicas (el form sale de `/metadata/modal/:model` vía CreateRecordDialog). Reemplaza los pickers bespoke duplicados en POS/purchases por una sola implementación reusable.
|
|
25
|
+
- 8346d8d: PrintDocumentButton: botón reusable de impresión de documentos
|
|
26
|
+
|
|
27
|
+
Componente drop-in sobre usePrintDocument: renderiza un <button> (estilable con
|
|
28
|
+
className) que imprime/descarga/abre un documento del servidor, se deshabilita
|
|
29
|
+
mientras baja el PDF (aria-busy) y reporta errores por onError. Evita recablear
|
|
30
|
+
el hook + estado de carga en cada addon.
|
|
31
|
+
|
|
32
|
+
- 05bc470: usePrintDocument: primitivo reusable para imprimir documentos del servidor
|
|
33
|
+
|
|
34
|
+
Nuevo hook que cualquier addon federado usa para imprimir/descargar/abrir un
|
|
35
|
+
documento renderizado por el motor de docs de ops (ticket, comprobante, orden):
|
|
36
|
+
GET /api/data/:model/:id/documents/:key.pdf. Fetch con auth vía el ApiClient
|
|
37
|
+
inyectado (el endpoint es Bearer-gated, no se puede window.open directo), blob →
|
|
38
|
+
iframe print (modo default, ideal para ticket térmico), download u open. Era el
|
|
39
|
+
primitivo de impresión que faltaba en el SDK (había 0). Habilita el auto-print del
|
|
40
|
+
POS al cerrar venta.
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- 233d63b: DynamicTable: una celda `creator` sin actor resuelto (registro creado por el sistema, `created_by` null) muestra "Sistema" en vez de "N/A", consistente con el fallback del historial de actividad. `user`/`avatar`/`search` mantienen "N/A" (vacío = sin asignar).
|
|
45
|
+
|
|
3
46
|
## 28.7.0
|
|
4
47
|
|
|
5
48
|
### Minor Changes
|
|
@@ -458,7 +458,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
458
458
|
resolveWidget(field) === 'textarea' ||
|
|
459
459
|
resolveWidget(field) === 'richtext';
|
|
460
460
|
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
|
|
461
|
-
}), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : 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" }), tl(action.label)] })] })] }) }));
|
|
461
|
+
}), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : 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" }), tl(action.label)] })] })] }) }));
|
|
462
462
|
}
|
|
463
463
|
// buildFieldDefaults seeds formData for a set of action fields, honoring the
|
|
464
464
|
// same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAwC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAkB3C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;IACzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,WAAW,CAAA;IAC1B,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AA0CD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CASrE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA6FD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAaD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,QAAQ,EAAE,CAOZ;AAUD,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAuhB1B;AA+DD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAWlF;AAyDD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BAgQA;AAsID,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;IAC1D,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;CACf,+BA0MA"}
|
|
@@ -638,7 +638,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
638
638
|
setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
|
|
639
639
|
};
|
|
640
640
|
const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0));
|
|
641
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid gap-y-4", children: [isSteps && (_jsx(WizardProgress, { groups: groups, stepIndex: clampedStep })), (isSteps ? [groups[clampedStep]] : groups).map(group => (_jsx(FieldSection, { group: group, children: _jsx("div", { className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: renderFields(group.fields) }) }, group.key))), record?.external_url && (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2", children: _jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [isSteps && clampedStep > 0 ? (_jsx(Button, { variant: "outline", onClick: goBackStep, disabled: saving || deleting, children: "Anterior" })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel })), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && isSteps && !isLastStep && (_jsx(Button, { type: "button", onClick: goNextStep, disabled: saving || loading, children: "Siguiente" })), isEditable && (!isSteps || isLastStep) && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
|
|
641
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid gap-y-4", children: [isSteps && (_jsx(WizardProgress, { groups: groups, stepIndex: clampedStep })), (isSteps ? [groups[clampedStep]] : groups).map(group => (_jsx(FieldSection, { group: group, children: _jsx("div", { className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: renderFields(group.fields) }) }, group.key))), record?.external_url && (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2", children: _jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [isSteps && clampedStep > 0 ? (_jsx(Button, { variant: "outline", onClick: goBackStep, disabled: saving || deleting, children: "Anterior" })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel })), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && isSteps && !isLastStep && (_jsx(Button, { type: "button", onClick: goNextStep, disabled: saving || loading, children: "Siguiente" })), isEditable && (!isSteps || isLastStep) && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
|
|
642
642
|
}
|
|
643
643
|
function LoadingSkeleton() {
|
|
644
644
|
return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -622,7 +622,19 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
622
622
|
col.tooltip ||
|
|
623
623
|
col.displayField ||
|
|
624
624
|
col.key;
|
|
625
|
-
|
|
625
|
+
// A `creator` cell with no resolved actor means the
|
|
626
|
+
// record was created by the SYSTEM (e.g. an owner /
|
|
627
|
+
// seed row whose `created_by` is null), not a missing
|
|
628
|
+
// value — show "Sistema" rather than "N/A". Matches
|
|
629
|
+
// the record-history actor fallback. `user`/`avatar`/
|
|
630
|
+
// `search` keep "N/A" (empty there means unassigned).
|
|
631
|
+
const resolvedName = getNestedValue(row.original, namePath);
|
|
632
|
+
const name = resolvedName ||
|
|
633
|
+
(renderAs === 'creator'
|
|
634
|
+
? t
|
|
635
|
+
? t('common.system', { defaultValue: 'Sistema' })
|
|
636
|
+
: 'Sistema'
|
|
637
|
+
: 'N/A');
|
|
626
638
|
const desc = getNestedValue(row.original, col.description || '');
|
|
627
639
|
const avatarSrc = resolveAvatarSrc(col, row.original, value, apiBaseUrl);
|
|
628
640
|
return (_jsx(AvatarCell, { name: String(name), desc: desc ? String(desc) : undefined, avatarSrc: avatarSrc, getImageUrl: getImageUrl }));
|
|
@@ -13,6 +13,10 @@ export interface DynamicRelationStrings {
|
|
|
13
13
|
selectPlaceholder: string;
|
|
14
14
|
selectSearchPlaceholder: string;
|
|
15
15
|
selectEmpty: string;
|
|
16
|
+
/** Placeholder del buscador de la sub-tabla 1:N. */
|
|
17
|
+
searchPlaceholder: string;
|
|
18
|
+
/** Pie de la sub-tabla: "{{loaded}} de {{total}}". */
|
|
19
|
+
countLabel: string;
|
|
16
20
|
}
|
|
17
21
|
interface CommonProps {
|
|
18
22
|
/** id del registro padre. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AAkEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CACrB;AAwBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -4,12 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// - "one_to_many": lista inline editable que cuelga del registro padre.
|
|
5
5
|
// - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
|
|
6
6
|
// La RFC completa vive en `packages/runtime-react/docs/relations.md`.
|
|
7
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
9
|
import { flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table';
|
|
10
10
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
11
|
-
import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
|
|
12
|
-
import { Plus, Trash2, Pencil } from 'lucide-react';
|
|
11
|
+
import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, Input, MultiSelect, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
|
|
12
|
+
import { Plus, Trash2, Pencil, Search } from 'lucide-react';
|
|
13
13
|
import { useApi } from './api-context';
|
|
14
14
|
import { useMetadataCache } from './metadata-cache';
|
|
15
15
|
import { DynamicForm } from './dynamic-form';
|
|
@@ -18,6 +18,7 @@ import { useTimeZone, useCurrency } from './org-runtime-context';
|
|
|
18
18
|
import { makeDefaultGetDynamicColumns } from './dynamic-columns';
|
|
19
19
|
import { isColumnVisibleInLineSubtable } from './column-visibility';
|
|
20
20
|
import { useOptionsResolver } from './use-options-resolver';
|
|
21
|
+
import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
|
|
21
22
|
import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
22
23
|
export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, formatRelationCell, objectLabel, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
23
24
|
const DEFAULT_STRINGS = {
|
|
@@ -33,7 +34,13 @@ const DEFAULT_STRINGS = {
|
|
|
33
34
|
selectPlaceholder: 'Seleccionar…',
|
|
34
35
|
selectSearchPlaceholder: 'Buscar…',
|
|
35
36
|
selectEmpty: 'Sin resultados.',
|
|
37
|
+
searchPlaceholder: 'Buscar…',
|
|
38
|
+
countLabel: '{{loaded}} de {{total}}',
|
|
36
39
|
};
|
|
40
|
+
// Tamaño de página de la sub-tabla 1:N. Antes la lista pedía el hijo COMPLETO
|
|
41
|
+
// (sin page/per_page), así que abrir un almacén traía todas sus existencias y
|
|
42
|
+
// traspasos. Se pagina de a 25 y el resto entra por scroll infinito.
|
|
43
|
+
const REL_PAGE_SIZE = 25;
|
|
37
44
|
export function DynamicRelation(props) {
|
|
38
45
|
if (props.kind === 'many_to_many') {
|
|
39
46
|
return _jsx(ManyToManyRelation, { ...props });
|
|
@@ -60,6 +67,15 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
60
67
|
const [metadata, setMetadata] = useState(cachedMeta || null);
|
|
61
68
|
const [rows, setRows] = useState([]);
|
|
62
69
|
const [loading, setLoading] = useState(true);
|
|
70
|
+
// Paginación server-side + scroll infinito, igual que <DynamicTable>.
|
|
71
|
+
const [total, setTotal] = useState(0);
|
|
72
|
+
const [page, setPage] = useState(1);
|
|
73
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
74
|
+
// El backend devolvió una página corta: no hay más filas aunque meta.total
|
|
75
|
+
// diga otra cosa (drift count/list dispararía el sentinel para siempre).
|
|
76
|
+
const [exhausted, setExhausted] = useState(false);
|
|
77
|
+
const [search, setSearch] = useState('');
|
|
78
|
+
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
63
79
|
const [formOpen, setFormOpen] = useState(false);
|
|
64
80
|
const [editingRow, setEditingRow] = useState(null);
|
|
65
81
|
const [rowToDelete, setRowToDelete] = useState(null);
|
|
@@ -69,12 +85,32 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
69
85
|
// literal each render). Keeps fetchAll from re-firing on identity churn while
|
|
70
86
|
// still reacting to real scope changes.
|
|
71
87
|
const filtersKey = useMemo(() => (filters ? JSON.stringify(filters) : ''), [filters]);
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
// La metadata se lee por ref dentro del fetch: si entrara como dependencia,
|
|
89
|
+
// resolverla dispararía un segundo fetch de datos por cada montaje.
|
|
90
|
+
const metadataRef = useRef(metadata);
|
|
91
|
+
metadataRef.current = metadata;
|
|
92
|
+
// Debounce del buscador: cada tecla no puede pegarle al servidor.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const id = setTimeout(() => setDebouncedSearch(search.trim()), 300);
|
|
95
|
+
return () => clearTimeout(id);
|
|
96
|
+
}, [search]);
|
|
97
|
+
// fetchPage REEMPLAZA (página 1) o ANEXA (scroll infinito). Antes esta lista
|
|
98
|
+
// pedía el modelo hijo entero sin page/per_page.
|
|
99
|
+
const fetchPage = useCallback(async (nextPage, append) => {
|
|
100
|
+
if (append)
|
|
101
|
+
setLoadingMore(true);
|
|
102
|
+
else
|
|
103
|
+
setLoading(true);
|
|
74
104
|
try {
|
|
75
|
-
const params =
|
|
105
|
+
const params = {
|
|
106
|
+
...buildRelationFilterParams(foreignKey, parentId, filters),
|
|
107
|
+
page: nextPage,
|
|
108
|
+
per_page: REL_PAGE_SIZE,
|
|
109
|
+
};
|
|
110
|
+
if (debouncedSearch)
|
|
111
|
+
params.search = debouncedSearch;
|
|
76
112
|
const [metaRes, dataRes] = await Promise.all([
|
|
77
|
-
|
|
113
|
+
metadataRef.current ? Promise.resolve(null) : api.get(`/metadata/table/${model}`),
|
|
78
114
|
api.get(dataEndpoint, { params }),
|
|
79
115
|
]);
|
|
80
116
|
if (metaRes && metaRes.data?.success) {
|
|
@@ -83,18 +119,44 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
83
119
|
cacheMetadata(model, fresh);
|
|
84
120
|
}
|
|
85
121
|
const list = dataRes.data;
|
|
86
|
-
if (list.success)
|
|
87
|
-
|
|
122
|
+
if (list.success) {
|
|
123
|
+
const fetched = list.data || [];
|
|
124
|
+
setRows((prev) => (append ? dedupeById(prev, fetched) : fetched));
|
|
125
|
+
setPage(nextPage);
|
|
126
|
+
if (list.meta?.total !== undefined)
|
|
127
|
+
setTotal(list.meta.total);
|
|
128
|
+
else if (!append)
|
|
129
|
+
setTotal(fetched.length);
|
|
130
|
+
setExhausted(fetched.length < REL_PAGE_SIZE);
|
|
131
|
+
}
|
|
88
132
|
}
|
|
89
133
|
catch (err) {
|
|
90
134
|
console.error('DynamicRelation fetch error', err);
|
|
91
135
|
}
|
|
92
136
|
finally {
|
|
93
|
-
|
|
137
|
+
if (append)
|
|
138
|
+
setLoadingMore(false);
|
|
139
|
+
else
|
|
140
|
+
setLoading(false);
|
|
94
141
|
}
|
|
95
142
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
-
}, [api, dataEndpoint, foreignKey, parentId, filtersKey,
|
|
97
|
-
|
|
143
|
+
}, [api, dataEndpoint, foreignKey, parentId, filtersKey, model, cacheMetadata, debouncedSearch]);
|
|
144
|
+
// Recarga desde la primera página. Es lo que corren create/edit/delete.
|
|
145
|
+
const fetchAll = useCallback(async () => {
|
|
146
|
+
await fetchPage(1, false);
|
|
147
|
+
}, [fetchPage]);
|
|
148
|
+
// Un cambio de padre/scope/búsqueda vuelve a la página 1.
|
|
149
|
+
useEffect(() => { fetchPage(1, false); }, [fetchPage]);
|
|
150
|
+
const canLoadMore = !loading && !loadingMore && !exhausted && rows.length < total;
|
|
151
|
+
const { rootRef, sentinelRef } = useInfiniteScrollSentinel({
|
|
152
|
+
onLoadMore: () => { if (canLoadMore)
|
|
153
|
+
fetchPage(page + 1, true); },
|
|
154
|
+
disabled: !canLoadMore,
|
|
155
|
+
});
|
|
156
|
+
// El buscador solo aparece cuando hay algo que buscar: una sub-tabla de 3
|
|
157
|
+
// líneas no necesita chrome extra. Se queda visible mientras haya término
|
|
158
|
+
// activo para no dejar al usuario sin forma de limpiarlo.
|
|
159
|
+
const showSearch = debouncedSearch !== '' || search !== '' || total > REL_PAGE_SIZE;
|
|
98
160
|
const formFields = useMemo(() => deriveRelationFormFields(metadata, foreignKey), [metadata, foreignKey]);
|
|
99
161
|
const visibleColumns = useMemo(() => {
|
|
100
162
|
if (!metadata?.columns)
|
|
@@ -199,10 +261,12 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
199
261
|
setSubmitting(false);
|
|
200
262
|
}
|
|
201
263
|
}, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
|
|
202
|
-
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
264
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate || showSearch) && (_jsxs("div", { className: "flex items-center justify-between gap-2 pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [showSearch && (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: labels.searchPlaceholder, className: "h-8 w-40 ps-7 sm:w-56" })] })), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsxs("div", { ref: rootRef, className: "overflow-auto max-h-[60vh] border rounded-md bg-card", children: [_jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
|
|
265
|
+
const isActions = header.id === 'actions';
|
|
266
|
+
return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActions && 'text-right'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
267
|
+
}) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }), loadingMore && (_jsx("div", { className: "p-2", children: _jsx(Skeleton, { className: "h-8 w-full" }) })), _jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "h-px w-full" })] })), !loading && rows.length > 0 && total > rows.length && (_jsx("div", { className: "pt-2 text-end text-xs text-muted-foreground", children: labels.countLabel
|
|
268
|
+
.replace('{{loaded}}', String(rows.length))
|
|
269
|
+
.replace('{{total}}', String(total)) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
|
|
206
270
|
setEditingRow(null); }, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting })] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
|
|
207
271
|
}
|
|
208
272
|
function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, readonly, readOnly, strings, className, onChange, }) {
|
|
@@ -37,6 +37,19 @@ export interface DynamicRelationsProps {
|
|
|
37
37
|
* autónoma conserva todas las columnas.
|
|
38
38
|
*/
|
|
39
39
|
lineSubtable?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Solo renderiza las relaciones de COMPOSICIÓN — las que el kernel marca
|
|
42
|
+
* con `embed: true` (las líneas de un documento). Es lo que usan los MODALES
|
|
43
|
+
* de registro: antes embebían TODAS las relaciones one_to_many del modelo,
|
|
44
|
+
* así que abrir "Editar Almacén" arrastraba miles de existencias y traspasos
|
|
45
|
+
* al formulario. Las relaciones no embebidas siguen accesibles desde su
|
|
46
|
+
* propia página / la vista de detalle, que renderiza el listado completo.
|
|
47
|
+
*
|
|
48
|
+
* Default false: una página de detalle autónoma sigue mostrando todas.
|
|
49
|
+
* Una relación sin `embed` (kernel viejo) NO se embebe — el gate falla del
|
|
50
|
+
* lado seguro.
|
|
51
|
+
*/
|
|
52
|
+
embedOnly?: boolean;
|
|
40
53
|
/** Bubble up when any panel's data changes (create/delete/attach/detach). */
|
|
41
54
|
onChange?: (relation: RelationMeta) => void;
|
|
42
55
|
}
|
|
@@ -55,5 +68,13 @@ export declare function resolveParentId(record: {
|
|
|
55
68
|
* both places is safe.
|
|
56
69
|
*/
|
|
57
70
|
export declare function buildRelationFilters(relation: Pick<RelationMeta, 'foreign_key' | 'scope'>, parentId: string | number): Record<string, string>;
|
|
58
|
-
|
|
71
|
+
/**
|
|
72
|
+
* ¿La relación es de COMPOSICIÓN (embebible en un modal)? Solo `embed: true`
|
|
73
|
+
* califica: la ausencia del flag — un kernel viejo que todavía no lo sirve —
|
|
74
|
+
* significa NO embeber, que es el lado seguro (el costo de un falso negativo
|
|
75
|
+
* es un panel de menos en el modal; el de un falso positivo, miles de filas
|
|
76
|
+
* dentro de un formulario).
|
|
77
|
+
*/
|
|
78
|
+
export declare function isEmbedded(rel: Pick<RelationMeta, 'embed'>): boolean;
|
|
79
|
+
export declare function DynamicRelations({ record, relations, parentIdKey, className, panelClassName, canCreate, canDelete, canEdit, strings, lineSubtable, embedOnly, onChange, }: DynamicRelationsProps): import("react").JSX.Element | null;
|
|
59
80
|
//# sourceMappingURL=dynamic-relations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relations.d.ts","sourceRoot":"","sources":["../src/dynamic-relations.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IACzE,gEAAgE;IAChE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;CAC9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,MAAM,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnD,WAAW,SAAO,GACnB,MAAM,GAAG,MAAM,GAAG,SAAS,CAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,OAAO,CAAC,EACrD,QAAQ,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAOD,wBAAgB,gBAAgB,CAAC,EAC7B,MAAM,EACN,SAAS,EACT,WAAkB,EAClB,SAAS,EACT,cAAc,EACd,SAAgB,EAChB,SAAgB,EAChB,OAAc,EACd,OAAO,EACP,YAAoB,EACpB,QAAQ,GACX,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"dynamic-relations.d.ts","sourceRoot":"","sources":["../src/dynamic-relations.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IACzE,gEAAgE;IAChE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;CAC9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,MAAM,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnD,WAAW,SAAO,GACnB,MAAM,GAAG,MAAM,GAAG,SAAS,CAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,OAAO,CAAC,EACrD,QAAQ,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,OAAO,CAEpE;AAOD,wBAAgB,gBAAgB,CAAC,EAC7B,MAAM,EACN,SAAS,EACT,WAAkB,EAClB,SAAS,EACT,cAAc,EACd,SAAgB,EAChB,SAAgB,EAChB,OAAc,EACd,OAAO,EACP,YAAoB,EACpB,SAAiB,EACjB,QAAQ,GACX,EAAE,qBAAqB,sCA8EvB"}
|
|
@@ -48,16 +48,29 @@ export function buildRelationFilters(relation, parentId) {
|
|
|
48
48
|
out[relation.foreign_key] = String(parentId);
|
|
49
49
|
return out;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* ¿La relación es de COMPOSICIÓN (embebible en un modal)? Solo `embed: true`
|
|
53
|
+
* califica: la ausencia del flag — un kernel viejo que todavía no lo sirve —
|
|
54
|
+
* significa NO embeber, que es el lado seguro (el costo de un falso negativo
|
|
55
|
+
* es un panel de menos en el modal; el de un falso positivo, miles de filas
|
|
56
|
+
* dentro de un formulario).
|
|
57
|
+
*/
|
|
58
|
+
export function isEmbedded(rel) {
|
|
59
|
+
return rel.embed === true;
|
|
60
|
+
}
|
|
51
61
|
/** Stable React key for a relation panel. */
|
|
52
62
|
function relationKey(rel, idx) {
|
|
53
63
|
return rel.name || `${rel.through}-${rel.foreign_key}-${idx}`;
|
|
54
64
|
}
|
|
55
|
-
export function DynamicRelations({ record, relations, parentIdKey = 'id', className, panelClassName, canCreate = true, canDelete = true, canEdit = true, strings, lineSubtable = false, onChange, }) {
|
|
65
|
+
export function DynamicRelations({ record, relations, parentIdKey = 'id', className, panelClassName, canCreate = true, canDelete = true, canEdit = true, strings, lineSubtable = false, embedOnly = false, onChange, }) {
|
|
56
66
|
const parentId = useMemo(() => resolveParentId(record, parentIdKey), [record, parentIdKey]);
|
|
57
|
-
|
|
67
|
+
// Gate de composición: en un modal solo entran las relaciones marcadas
|
|
68
|
+
// `embed` por el kernel. Fuera del modal la lista pasa entera.
|
|
69
|
+
const visible = useMemo(() => (embedOnly ? (relations || []).filter(isEmbedded) : relations || []), [relations, embedOnly]);
|
|
70
|
+
if (parentId === undefined || visible.length === 0) {
|
|
58
71
|
return null;
|
|
59
72
|
}
|
|
60
|
-
return (_jsx("div", { className: cn('space-y-6', className), "data-dynamic-relations": "", children:
|
|
73
|
+
return (_jsx("div", { className: cn('space-y-6', className), "data-dynamic-relations": "", children: visible.map((rel, idx) => {
|
|
61
74
|
const filters = buildRelationFilters(rel, parentId);
|
|
62
75
|
const panelStrings = {
|
|
63
76
|
...(strings || {}),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type LucideIcon } from 'lucide-react';
|
|
2
|
+
/** One searchable option: `value` is the id, `label` the display text. */
|
|
3
|
+
export interface EntitySelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EntitySelectProps {
|
|
9
|
+
/** Kernel model key (e.g. "Supplier", "Warehouse", "Category"). */
|
|
10
|
+
model: string;
|
|
11
|
+
/** Currently selected id (or null). */
|
|
12
|
+
value: string | null;
|
|
13
|
+
/** Label of the selected record (rendered without a re-fetch). */
|
|
14
|
+
label: string | null;
|
|
15
|
+
/** Called with (id, label) on select/create/clear. */
|
|
16
|
+
onSelect: (id: string | null, label: string | null) => void;
|
|
17
|
+
/** Async search over the model. Callers pass their `/api/options/<model>` fetcher. */
|
|
18
|
+
fetcher: (q: string, signal: AbortSignal) => Promise<EntitySelectOption[]>;
|
|
19
|
+
icon?: LucideIcon;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
searchPlaceholder?: string;
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
/** Preload first results on open and drop the 2-char gate. */
|
|
24
|
+
preload?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Permission overrides. By default create/edit are gated by the kernel
|
|
27
|
+
* permissions `<model>.create` / `<model>.update` (useCan). Pass explicit
|
|
28
|
+
* booleans to force them (e.g. a read-only surface).
|
|
29
|
+
*/
|
|
30
|
+
canCreate?: boolean;
|
|
31
|
+
canEdit?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* CRUD endpoint base for the create/edit dialog. Defaults to the standard
|
|
34
|
+
* org-scoped `/data/<model>/me`, with edit at `/data/<model>/me/<id>`.
|
|
35
|
+
*/
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
/** Record field used as the label after create/edit (default "name"). */
|
|
38
|
+
labelField?: string;
|
|
39
|
+
/** Disable the whole control. */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, }: EntitySelectProps): import("react").JSX.Element;
|
|
43
|
+
//# sourceMappingURL=entity-select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAiBvE,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAOD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EAAE,IAAI,EACV,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,GACnB,EAAE,iBAAiB,+BAuNnB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// EntitySelect — the shared, permission-aware single-select for a related model.
|
|
3
|
+
//
|
|
4
|
+
// A searchable async combobox over a kernel model's records, plus the two
|
|
5
|
+
// affordances every "pick a related record" control should have, exactly like
|
|
6
|
+
// the dynamic create modal's relation fields (Categoría/Marca) do:
|
|
7
|
+
//
|
|
8
|
+
// - nothing selected → a "+" that opens the model's CREATE dialog, and
|
|
9
|
+
// auto-selects the record it creates;
|
|
10
|
+
// - a record selected → a pencil that opens that record's EDIT dialog.
|
|
11
|
+
//
|
|
12
|
+
// Both affordances are gated by the kernel permissions (useCan): the "+" only
|
|
13
|
+
// shows when the user can create the model, the pencil only when they can edit
|
|
14
|
+
// it. Everything is DYNAMIC — the create/edit form comes from the model's
|
|
15
|
+
// `/metadata/modal/:model` schema via <CreateRecordDialog>, so no per-model form
|
|
16
|
+
// code is needed. This lives in the SDK so POS, purchases and any future addon
|
|
17
|
+
// share ONE implementation instead of each re-porting a bespoke picker.
|
|
18
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
19
|
+
import { Search, X, Plus, Pencil } from 'lucide-react';
|
|
20
|
+
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui';
|
|
21
|
+
import { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
22
|
+
import { useCan } from './permissions-context';
|
|
23
|
+
import { useApi } from './api-context';
|
|
24
|
+
/** Lowercase model → permission capability namespace (Supplier → supplier). */
|
|
25
|
+
function capabilityNamespace(model) {
|
|
26
|
+
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, }) {
|
|
29
|
+
const can = useCan();
|
|
30
|
+
const api = useApi();
|
|
31
|
+
const ns = capabilityNamespace(model);
|
|
32
|
+
const mayCreate = canCreate ?? can(`${ns}.create`);
|
|
33
|
+
const mayEdit = canEdit ?? can(`${ns}.update`);
|
|
34
|
+
const base = endpoint ?? `/data/${model}/me`;
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
37
|
+
const [dialogRecordId, setDialogRecordId] = useState(undefined);
|
|
38
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
39
|
+
const [results, setResults] = useState([]);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
41
|
+
const minChars = preload ? 0 : 2;
|
|
42
|
+
const run = useCallback(async (q, signal) => {
|
|
43
|
+
if (q.length < minChars) {
|
|
44
|
+
setResults([]);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setIsLoading(true);
|
|
48
|
+
try {
|
|
49
|
+
const rows = await fetcher(q, signal);
|
|
50
|
+
if (!signal.aborted)
|
|
51
|
+
setResults(rows);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (!signal.aborted)
|
|
55
|
+
setResults([]);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (!signal.aborted)
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [fetcher, minChars]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!open)
|
|
64
|
+
return;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => run(searchTerm, controller.signal), 250);
|
|
67
|
+
return () => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
controller.abort();
|
|
70
|
+
};
|
|
71
|
+
}, [searchTerm, run, open]);
|
|
72
|
+
const pick = (row) => {
|
|
73
|
+
onSelect(row.value, row.label);
|
|
74
|
+
setOpen(false);
|
|
75
|
+
setSearchTerm('');
|
|
76
|
+
};
|
|
77
|
+
const clear = (e) => {
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
onSelect(null, null);
|
|
80
|
+
};
|
|
81
|
+
const openCreate = (e) => {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
setDialogRecordId(undefined);
|
|
84
|
+
setDialogOpen(true);
|
|
85
|
+
};
|
|
86
|
+
const openEdit = (e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (!value)
|
|
89
|
+
return;
|
|
90
|
+
setDialogRecordId(value);
|
|
91
|
+
setDialogOpen(true);
|
|
92
|
+
};
|
|
93
|
+
// Read the {id,label} off a saved record and select it. The transport
|
|
94
|
+
// matches the standard org-scoped CRUD the dynamic modal uses, so create/edit
|
|
95
|
+
// stay consistent with the rest of the app.
|
|
96
|
+
const selectSaved = (rec) => {
|
|
97
|
+
if (!rec)
|
|
98
|
+
return;
|
|
99
|
+
const id = rec.id != null ? String(rec.id) : value ?? '';
|
|
100
|
+
const lbl = (rec[labelField] != null && String(rec[labelField])) ||
|
|
101
|
+
(rec.name != null && String(rec.name)) ||
|
|
102
|
+
label ||
|
|
103
|
+
id;
|
|
104
|
+
onSelect(id, lbl);
|
|
105
|
+
};
|
|
106
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: disabled ? undefined : setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", disabled: disabled, className: "w-full flex-1 justify-start gap-2 font-normal", children: [Icon && _jsx(Icon, { className: "text-muted-foreground size-4 shrink-0" }), _jsx("span", { className: "flex-1 truncate text-left", children: label ?? placeholder }), value && (_jsx("span", { role: "button", tabIndex: 0, onClick: clear, onKeyDown: (e) => {
|
|
107
|
+
if (e.key === 'Enter' || e.key === ' ')
|
|
108
|
+
clear(e);
|
|
109
|
+
}, className: "hover:bg-accent ml-auto shrink-0 rounded-sm p-0.5", children: _jsx(X, { className: "size-3.5" }) }))] }) }), _jsx(PopoverContent, { className: "p-0", align: "start", style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: searchPlaceholder, value: searchTerm, onValueChange: setSearchTerm }), _jsxs(CommandList, { children: [isLoading && (_jsx("div", { className: "text-muted-foreground py-4 text-center text-sm", children: "Buscando\u2026" })), !isLoading &&
|
|
110
|
+
searchTerm.length >= minChars &&
|
|
111
|
+
results.length === 0 && _jsx(CommandEmpty, { children: emptyText }), !isLoading && results.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: results.map((row) => (_jsxs(CommandItem, { value: row.value, onSelect: () => pick(row), className: "flex flex-col items-start gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: row.label }), row.description && (_jsx("span", { className: "text-muted-foreground text-xs", children: row.description }))] }, row.value))) })), !isLoading && !preload && searchTerm.length < minChars && (_jsxs("div", { className: "text-muted-foreground flex flex-col items-center gap-1 py-6", children: [_jsx(Search, { className: "size-5" }), _jsx("span", { className: "text-xs", children: "Escribe al menos 2 caracteres" })] }))] })] }) })] }), value
|
|
112
|
+
? mayEdit && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openEdit, "aria-label": "Editar", title: "Editar", className: "shrink-0", children: _jsx(Pencil, { className: "size-4" }) }))
|
|
113
|
+
: mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openCreate, "aria-label": "Crear", title: "Crear", className: "shrink-0", children: _jsx(Plus, { className: "size-4" }) })), dialogOpen && (_jsx(CreateRecordDialog, { modelKey: model, open: dialogOpen, onOpenChange: setDialogOpen, recordId: dialogRecordId, endpoint: base, onCreate: async (data) => {
|
|
114
|
+
const res = await api.post(base, data);
|
|
115
|
+
const rec = (res.data?.data ?? res.data);
|
|
116
|
+
selectSaved(rec);
|
|
117
|
+
return rec.id != null ? { id: String(rec.id) } : undefined;
|
|
118
|
+
}, onUpdate: async (id, data) => {
|
|
119
|
+
const res = await api.put(`${base}/${id}`, data);
|
|
120
|
+
const rec = (res.data?.data ?? res.data);
|
|
121
|
+
selectSaved({ id, ...rec });
|
|
122
|
+
return { id: String(id) };
|
|
123
|
+
} }))] }));
|
|
124
|
+
}
|