@asteby/metacore-runtime-react 28.3.7 → 28.5.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/action-modal-dispatcher.js +1 -1
  3. package/dist/dashboard-grid.d.ts.map +1 -1
  4. package/dist/dashboard-grid.js +5 -1
  5. package/dist/dialogs/dynamic-record.d.ts +20 -1
  6. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  7. package/dist/dialogs/dynamic-record.js +95 -23
  8. package/dist/dynamic-form-schema.d.ts +26 -1
  9. package/dist/dynamic-form-schema.d.ts.map +1 -1
  10. package/dist/dynamic-form-schema.js +41 -0
  11. package/dist/dynamic-form.d.ts +10 -1
  12. package/dist/dynamic-form.d.ts.map +1 -1
  13. package/dist/dynamic-form.js +60 -7
  14. package/dist/dynamic-relation.d.ts +12 -0
  15. package/dist/dynamic-relation.d.ts.map +1 -1
  16. package/dist/dynamic-relation.js +12 -6
  17. package/dist/dynamic-relations.d.ts +9 -1
  18. package/dist/dynamic-relations.d.ts.map +1 -1
  19. package/dist/dynamic-relations.js +5 -4
  20. package/dist/form-layout-ui.d.ts +25 -0
  21. package/dist/form-layout-ui.d.ts.map +1 -0
  22. package/dist/form-layout-ui.js +39 -0
  23. package/dist/form-layout.d.ts +54 -0
  24. package/dist/form-layout.d.ts.map +1 -0
  25. package/dist/form-layout.js +81 -0
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/types.d.ts +47 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/__tests__/dynamic-record-hidden-submit.test.tsx +78 -0
  33. package/src/__tests__/dynamic-relation-subtable-context.test.tsx +74 -0
  34. package/src/__tests__/form-layout.test.ts +113 -0
  35. package/src/__tests__/visible-when.test.ts +63 -0
  36. package/src/action-modal-dispatcher.tsx +1 -1
  37. package/src/dashboard-grid.tsx +5 -1
  38. package/src/dialogs/dynamic-record.tsx +189 -50
  39. package/src/dynamic-form-schema.ts +47 -1
  40. package/src/dynamic-form.tsx +126 -17
  41. package/src/dynamic-relation.tsx +25 -8
  42. package/src/dynamic-relations.tsx +16 -1
  43. package/src/form-layout-ui.tsx +117 -0
  44. package/src/form-layout.ts +116 -0
  45. package/src/index.ts +4 -0
  46. package/src/types.ts +48 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 28.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 47f6543: Los renderers de formulario (`DynamicForm` y `DynamicRecordDialog`) ahora consumen el primitivo `form_layout` del modelo (kernel PR #230): agrupan los campos por su `section` respetando el orden de `sections`. En `mode: "sections"` cada grupo se dibuja como una sección con `title`/`description` y soporte de `collapsed` (colapsable); en `mode: "steps"` se dibuja un wizard multi-paso con navegación Anterior/Siguiente y submit en el último paso. Los campos sin `section` (o con sección desconocida) caen en un grupo por defecto al inicio, y una sección sin campos visibles se oculta. Sin `form_layout` el comportamiento (lista plana) queda idéntico; `visible_when`, el gate de required y el strip de ocultos en el submit se respetan.
8
+
9
+ ### Patch Changes
10
+
11
+ - b05aded: DynamicRecordDialog ya no envía en el submit los campos ocultos por `visible_when`, en paridad con DynamicForm.
12
+
13
+ ## 28.4.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 0075ae0: Pulido: (1) el mockup animado del tablero vacío ya no lleva borde punteado ni
18
+ padding — las skeleton cards flotan como las reales; (2) las secciones de
19
+ relaciones apiladas en el modal de vista (ej. "Líneas del pedido" y "Facturas")
20
+ tienen separación (space-y-6) en vez de ir pegadas.
21
+
22
+ ## 28.4.0
23
+
24
+ ### Minor Changes
25
+
26
+ - f1f66cb: feat: `visible_when` conditional field visibility in both form renderers
27
+
28
+ Add support for the kernel's new `visible_when` primitive: a create/edit form
29
+ field is rendered — and validated — only while a sibling field's current value
30
+ matches the declared predicate (`{ field, equals | in }`). A hidden field never
31
+ gates submit (its required-check is dropped along with it).
32
+
33
+ Wired into BOTH form renderers off a shared helper `evaluateVisibleWhen`
34
+ (exported alongside `getVisibleWhen`):
35
+
36
+ - `DynamicForm` (`dynamic-form.tsx`) — drives the zod schema, the balance gate
37
+ and the render off a `visibleFields` list filtered on the live values.
38
+ - `DynamicRecordDialog` (`dialogs/dynamic-record.tsx`) — `filterVisibleFields`
39
+ gains an optional `formValues` arg applying the predicate; the render and the
40
+ required-gate loop both use it.
41
+
42
+ `ActionFieldDef` and `ColumnDefinition` gain `visible_when` / `visibleWhen`
43
+ (snake + camel aliases). Fields without `visible_when` are always visible
44
+ (retro-compatible). Requires a kernel/ops bump that serves the field.
45
+
46
+ ### Patch Changes
47
+
48
+ - 9bc8bd1: runtime-react: scope audit-column hiding in `DynamicRelation` to the view-modal line-subtable context
49
+
50
+ PR #659 started dropping audit/system columns (`created_by`, timestamps,
51
+ `organization_id`) and `visibility: "table"` columns in every one-to-many
52
+ `DynamicRelation` panel — including standalone detail pages
53
+ (`/m/<model>/<id>`, `m/invoices/<id>`), where those columns are useful. Add an
54
+ explicit `lineSubtable` prop (default `false`) that gates
55
+ `isColumnVisibleInLineSubtable`; outside that context the panel keeps the
56
+ previous behaviour and only hides the FK, scope and `hidden` columns. The view
57
+ modal (`DynamicRecordDialog`, action-modal dispatcher) opts in via
58
+ `<DynamicRelations lineSubtable>`.
59
+
3
60
  ## 28.3.7
4
61
 
5
62
  ### Patch 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 }) }))] }) }), _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 }) }))] }) }), _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":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAmJpB"}
1
+ {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAuJpB"}
@@ -131,7 +131,11 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
131
131
  // box auto-height and the tiles collapse into a strip — so we own
132
132
  // the height here instead of hoping the dashboard layout supplies
133
133
  // one.
134
- 'h-[clamp(420px,60vh,720px)] w-full rounded-xl border border-dashed border-border/60 p-4', className), children: _jsx(DashboardEmptyMockup, { className: "h-full" }) }));
134
+ // No dashed border / padding wrapper: the skeleton tiles ARE
135
+ // cards (same anatomy as the real widgets), so the mockup
136
+ // should float exactly like the loaded dashboard, not sit
137
+ // inside a boxed placeholder. Keep only the definite height.
138
+ 'h-[clamp(420px,60vh,720px)] w-full', className), children: _jsx(DashboardEmptyMockup, { className: "h-full" }) }));
135
139
  }
136
140
  return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
137
141
  // Masonry: balanced CSS columns. Cards take their natural height
@@ -1,5 +1,6 @@
1
1
  import type { ModelSchema } from './types';
2
2
  import { type ResolvedOption } from '../use-options-resolver';
3
+ import type { VisibleWhen } from '../types';
3
4
  import { type ItemField } from '../collection-cell';
4
5
  import { type GetImageUrl } from '../image-url-context';
5
6
  export type { GetImageUrl };
@@ -71,6 +72,23 @@ export interface FieldDef {
71
72
  itemFields?: ItemField[];
72
73
  /** snake_case alias served by the kernel for `itemFields`. */
73
74
  item_fields?: ItemField[];
75
+ /**
76
+ * Conditional visibility: render — and required-check — this field only
77
+ * while a sibling field's current value matches the predicate. Mirrors the
78
+ * kernel v3 `visible_when` (projected onto the served modal FieldDef).
79
+ * Tolerates the camelCase alias. Absent = always visible; a hidden field is
80
+ * dropped from the required-gate so it never blocks submit. Evaluated by
81
+ * `evaluateVisibleWhen` against the live form values.
82
+ */
83
+ visible_when?: VisibleWhen;
84
+ /** camelCase alias for `visible_when`. */
85
+ visibleWhen?: VisibleWhen;
86
+ /**
87
+ * Form-layout membership: the key of the `form_layout` section this field
88
+ * belongs to (kernel PR #230). Absent → the default group. See
89
+ * `groupFieldsBySection`.
90
+ */
91
+ section?: string;
74
92
  }
75
93
  export interface DynamicRecordDialogProps {
76
94
  open: boolean;
@@ -165,7 +183,8 @@ export declare function fieldItemFields(field: FieldDef): ItemField[] | undefine
165
183
  export declare function isLineItemsField(field: FieldDef, value: any): boolean;
166
184
  export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
167
185
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
168
- export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): FieldDef[];
186
+ export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create', formValues?: Record<string, any>): FieldDef[];
187
+ export declare function stripHiddenFieldValues(values: Record<string, any>, fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): Record<string, any>;
169
188
  export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
170
189
  export declare function ReadonlyEditField({ field, value }: {
171
190
  field: FieldDef;
@@ -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":"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"}
@@ -23,7 +23,9 @@ import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../ser
23
23
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
24
24
  import { DynamicRelations } from '../dynamic-relations';
25
25
  import { useOptionsResolver } from '../use-options-resolver';
26
- import { getFieldRef } from '../dynamic-form-schema';
26
+ import { getFieldRef, getVisibleWhen, evaluateVisibleWhen } from '../dynamic-form-schema';
27
+ import { groupFieldsBySection } from '../form-layout';
28
+ import { FieldSection, WizardProgress } from '../form-layout-ui';
27
29
  import { FieldCell } from '../field-grid';
28
30
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
29
31
  import { normalizeRefFieldsForSubmit } from './normalize-submit';
@@ -239,15 +241,43 @@ export function isMoneyField(field, value) {
239
241
  // given mode. `hidden` fields never render. A `readonly` (server/system-
240
242
  // generated) field is EXCLUDED on create — the user can't set a value the
241
243
  // server will overwrite — but stays visible on edit/view (rendered disabled).
242
- export function filterVisibleFields(fields, mode) {
244
+ //
245
+ // `formValues`, when provided, additionally applies each field's `visible_when`
246
+ // predicate against the live form values (conditional visibility): a field is
247
+ // dropped while the referenced sibling's value does not match. Driving both the
248
+ // render and the required-gate off this same list means a hidden field never
249
+ // blocks submit. Omitting `formValues` keeps the legacy (always-visible)
250
+ // behaviour for callers that only gate on mode.
251
+ export function filterVisibleFields(fields, mode, formValues) {
243
252
  return (fields ?? []).filter(f => {
244
253
  if (f.hidden)
245
254
  return false;
246
255
  if (mode === 'create' && f.readonly)
247
256
  return false;
257
+ if (formValues && !evaluateVisibleWhen(getVisibleWhen(f), formValues))
258
+ return false;
248
259
  return true;
249
260
  });
250
261
  }
262
+ // stripHiddenFieldValues drops from a flat form-values object the keys of any
263
+ // declared field currently hidden by its `visible_when` predicate, so the
264
+ // submit never POSTs a value the form isn't showing (e.g. a DiscountRule with
265
+ // rule_scope=category must not send the product_id / customer_id it hides).
266
+ // This mirrors dynamic-form.tsx, which builds its Zod schema only over the
267
+ // visible fields — hidden fields are dropped from BOTH the render/required-gate
268
+ // AND the submitted values. Keys with no matching declared field, or whose
269
+ // field carries no `visible_when`, always pass through (retrocompat).
270
+ export function stripHiddenFieldValues(values, fields, mode) {
271
+ const visibleKeys = new Set(filterVisibleFields(fields, mode, values).map(f => f.key));
272
+ const out = {};
273
+ for (const [key, value] of Object.entries(values)) {
274
+ const field = (fields ?? []).find(f => f.key === key);
275
+ if (field && !visibleKeys.has(key) && getVisibleWhen(field))
276
+ continue;
277
+ out[key] = value;
278
+ }
279
+ return out;
280
+ }
251
281
  export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
252
282
  const api = useApi();
253
283
  const { t } = useTranslation();
@@ -262,6 +292,8 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
262
292
  const [loading, setLoading] = useState(false);
263
293
  const [saving, setSaving] = useState(false);
264
294
  const [deleting, setDeleting] = useState(false);
295
+ // Wizard step cursor (form_layout mode:"steps" only).
296
+ const [stepIndex, setStepIndex] = useState(0);
265
297
  const isCreate = mode === 'create';
266
298
  const isView = mode === 'view';
267
299
  const isEditable = mode === 'create' || mode === 'edit';
@@ -272,8 +304,10 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
272
304
  return;
273
305
  if (!isCreate && !recordId)
274
306
  return;
275
- // Fresh open → drop any validation errors from a prior submit.
307
+ // Fresh open → drop any validation errors from a prior submit and reset
308
+ // the wizard to its first step.
276
309
  setFieldErrors({});
310
+ setStepIndex(0);
277
311
  let cancelled = false;
278
312
  // Seed instantly from the row the table already has so view/edit render
279
313
  // without a spinner. The list row carries the pro siblings (resolved
@@ -462,9 +496,12 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
462
496
  return;
463
497
  if (isEditable) {
464
498
  // Collect ALL missing required fields (not just the first) and mark
465
- // each inline instead of a single toast.
499
+ // each inline instead of a single toast. Only CURRENTLY-VISIBLE
500
+ // fields are gated: a field hidden by its `visible_when` predicate
501
+ // must not block submit even when it is declared required (matching
502
+ // the render, which drops it via the same filter).
466
503
  const missing = {};
467
- for (const field of modalMeta.fields ?? []) {
504
+ for (const field of filterVisibleFields(modalMeta.fields, mode, formValues)) {
468
505
  if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
469
506
  missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
470
507
  }
@@ -477,9 +514,15 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
477
514
  }
478
515
  // Required check passed → clear any prior validation errors.
479
516
  setFieldErrors({});
517
+ // Fields hidden by their `visible_when` predicate must not be POSTed:
518
+ // the render, the required-gate and the payload all drive off the same
519
+ // filter, so a DiscountRule with scope=category never submits the
520
+ // product_id / customer_id it isn't showing. Mirrors dynamic-form.tsx,
521
+ // which builds its Zod only over visibleFields.
522
+ const submittedValues = stripHiddenFieldValues(formValues, modalMeta.fields, mode);
480
523
  // Empty reference pickers → null (not "" / nil-UUID) so nullable FK
481
524
  // columns accept them instead of raising a 23503 FK violation.
482
- const payload = normalizeRefFieldsForSubmit(formValues, modalMeta.fields);
525
+ const payload = normalizeRefFieldsForSubmit(submittedValues, modalMeta.fields);
483
526
  setSaving(true);
484
527
  try {
485
528
  if (isCreate && onCreate) {
@@ -550,23 +593,52 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
550
593
  }
551
594
  };
552
595
  const title = modalMeta ? config.getTitle(modalMeta, t) : '';
553
- const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
554
- 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 grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: [visibleFields.map(field => {
555
- const isFullWidth = field.type === 'textarea' ||
556
- field.widget === 'textarea' ||
557
- field.widget === 'richtext';
558
- return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, error: fieldErrors[field.key], onChange: val => {
559
- setFormValues((prev) => ({ ...prev, [field.key]: val }));
560
- // Clear this field's error as soon as the user edits it.
561
- setFieldErrors(prev => {
562
- if (!prev[field.key])
563
- return prev;
564
- const next = { ...prev };
565
- delete next[field.key];
566
- return next;
567
- });
568
- } }) }, field.key));
569
- }), record?.external_url && (_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, 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: [_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 && (_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] }))] })] })] }) }));
596
+ const visibleFields = filterVisibleFields(modalMeta?.fields, mode, formValues);
597
+ // Declarative form layout: group the (already visibility-filtered) fields by
598
+ // their section. Empty sections drop out for free. Steps mode only drives a
599
+ // wizard in editable modes — view mode always stacks the sections.
600
+ const formLayout = modalMeta?.form_layout ?? modalMeta?.formLayout;
601
+ const groups = groupFieldsBySection(visibleFields, formLayout);
602
+ const isSteps = isEditable && formLayout?.mode === 'steps' && groups.length > 1;
603
+ const clampedStep = Math.min(stepIndex, Math.max(groups.length - 1, 0));
604
+ const isLastStep = clampedStep === groups.length - 1;
605
+ // Renders a list of fields into the shared two-column grid (each FieldCell
606
+ // gives min-w-0 so long values can't blow the columns past the dialog).
607
+ const renderFields = (groupFields) => groupFields.map(field => {
608
+ const isFullWidth = field.type === 'textarea' ||
609
+ field.widget === 'textarea' ||
610
+ field.widget === 'richtext';
611
+ return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, error: fieldErrors[field.key], onChange: val => {
612
+ setFormValues((prev) => ({ ...prev, [field.key]: val }));
613
+ setFieldErrors(prev => {
614
+ if (!prev[field.key])
615
+ return prev;
616
+ const next = { ...prev };
617
+ delete next[field.key];
618
+ return next;
619
+ });
620
+ } }) }, field.key));
621
+ });
622
+ // Wizard "Siguiente": gate only the CURRENT step's required (visible) fields,
623
+ // then advance. Mirrors handleSubmit's required check but scoped to the step.
624
+ const goNextStep = () => {
625
+ const step = groups[clampedStep];
626
+ const missing = {};
627
+ for (const field of step?.fields ?? []) {
628
+ if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
629
+ missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
630
+ }
631
+ }
632
+ if (Object.keys(missing).length) {
633
+ setFieldErrors(missing);
634
+ toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
635
+ return;
636
+ }
637
+ setFieldErrors({});
638
+ setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
639
+ };
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] }))] })] })] }) }));
570
642
  }
571
643
  function LoadingSkeleton() {
572
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,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { ActionFieldDef, FieldOptionsConfig, OptionDef } from './types';
2
+ import type { ActionFieldDef, FieldOptionsConfig, OptionDef, VisibleWhen } from './types';
3
3
  /**
4
4
  * Apps register validator implementations by slug. The slug is the value
5
5
  * `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
@@ -97,6 +97,31 @@ export declare function resolveDependsValue(field: ActionFieldDef, formValues?:
97
97
  * used as the default gating field.
98
98
  */
99
99
  export declare function applyOptionWhen(options: OptionDef[] | undefined, formValues: Record<string, any> | null | undefined, dependsOn?: string): OptionDef[];
100
+ /**
101
+ * Reads a field's `visible_when` predicate, tolerating the camelCase alias
102
+ * (`visibleWhen`) an app may author and the snake_case (`visible_when`) the
103
+ * kernel serves. Returns `undefined` when the field declares neither.
104
+ */
105
+ export declare function getVisibleWhen(field: {
106
+ visible_when?: VisibleWhen;
107
+ visibleWhen?: VisibleWhen;
108
+ } | null | undefined): VisibleWhen | undefined;
109
+ /**
110
+ * Evaluates a `visible_when` predicate against the current flat form values.
111
+ * Pure — no React, no side effects.
112
+ *
113
+ * - No predicate → `true` (the field is always visible; retrocompat).
114
+ * - With a predicate: read the value of the sibling `cond.field` from
115
+ * `formValues` (as string, null/undefined → ''). Visible when it is a member
116
+ * of `cond.in` (any-of, wins when present) OR equals `cond.equals`. A
117
+ * predicate with an empty `field` is a no-op (visible). A predicate that
118
+ * names a field but declares neither `in` nor `equals` hides nothing
119
+ * (visible) — nothing to gate on.
120
+ *
121
+ * `cond` may be the raw block off either the snake_case or camelCase slot; use
122
+ * `getVisibleWhen` to normalize first.
123
+ */
124
+ export declare function evaluateVisibleWhen(cond: VisibleWhen | null | undefined, formValues: Record<string, any> | null | undefined): boolean;
100
125
  /**
101
126
  * Reads a field's enriched options-resolution config, tolerating the camelCase
102
127
  * `optionsConfig` (authored SDK shape) and the snake_case `options_config` the
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAiB1G;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAiC3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,cAAc,EACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACvC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GACvC,MAAM,CAQR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC3B,OAAO,EAAE,SAAS,EAAE,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,SAAS,CAAC,EAAE,MAAM,GACnB,SAAS,EAAE,CAqBb;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE;IAAE,YAAY,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GACpF,WAAW,GAAG,SAAS,CAIzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,GACnD,OAAO,CAaT;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,kBAAkB,GAAG,SAAS,CAGtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACnB,CAQA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB,CAaA"}
@@ -313,6 +313,47 @@ export function applyOptionWhen(options, formValues, dependsOn) {
313
313
  return true;
314
314
  });
315
315
  }
316
+ /**
317
+ * Reads a field's `visible_when` predicate, tolerating the camelCase alias
318
+ * (`visibleWhen`) an app may author and the snake_case (`visible_when`) the
319
+ * kernel serves. Returns `undefined` when the field declares neither.
320
+ */
321
+ export function getVisibleWhen(field) {
322
+ if (!field)
323
+ return undefined;
324
+ const vw = field.visible_when ?? field.visibleWhen;
325
+ return vw && typeof vw === 'object' && typeof vw.field === 'string' ? vw : undefined;
326
+ }
327
+ /**
328
+ * Evaluates a `visible_when` predicate against the current flat form values.
329
+ * Pure — no React, no side effects.
330
+ *
331
+ * - No predicate → `true` (the field is always visible; retrocompat).
332
+ * - With a predicate: read the value of the sibling `cond.field` from
333
+ * `formValues` (as string, null/undefined → ''). Visible when it is a member
334
+ * of `cond.in` (any-of, wins when present) OR equals `cond.equals`. A
335
+ * predicate with an empty `field` is a no-op (visible). A predicate that
336
+ * names a field but declares neither `in` nor `equals` hides nothing
337
+ * (visible) — nothing to gate on.
338
+ *
339
+ * `cond` may be the raw block off either the snake_case or camelCase slot; use
340
+ * `getVisibleWhen` to normalize first.
341
+ */
342
+ export function evaluateVisibleWhen(cond, formValues) {
343
+ if (!cond || typeof cond.field !== 'string' || cond.field.trim() === '')
344
+ return true;
345
+ const raw = formValues ? formValues[cond.field.trim()] : undefined;
346
+ const current = raw == null ? '' : String(raw);
347
+ const inList = cond.in;
348
+ if (Array.isArray(inList) && inList.length > 0) {
349
+ return inList.some((v) => String(v) === current);
350
+ }
351
+ if (typeof cond.equals === 'string') {
352
+ return cond.equals === current;
353
+ }
354
+ // Named a field but declared no comparison → nothing to gate on.
355
+ return true;
356
+ }
316
357
  /**
317
358
  * Reads a field's enriched options-resolution config, tolerating the camelCase
318
359
  * `optionsConfig` (authored SDK shape) and the snake_case `options_config` the
@@ -1,3 +1,4 @@
1
+ import { type FormLayout } from './form-layout';
1
2
  import type { ActionFieldDef } from './types';
2
3
  import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
3
4
  export { buildZodSchema, resolveWidget };
@@ -14,6 +15,14 @@ export interface DynamicFormProps {
14
15
  submitLabel?: string;
15
16
  cancelLabel?: string;
16
17
  disabled?: boolean;
18
+ /**
19
+ * Declarative form layout served on the model metadata (kernel PR #230).
20
+ * When present the visible fields are grouped by their `section` respecting
21
+ * `sections` order: `mode:"sections"` stacks (optionally collapsible)
22
+ * sections; `mode:"steps"` renders a Anterior/Siguiente wizard. Absent → the
23
+ * legacy flat list (unchanged).
24
+ */
25
+ formLayout?: FormLayout;
17
26
  }
18
- export declare function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel, cancelLabel, disabled, }: DynamicFormProps): import("react").JSX.Element;
27
+ export declare function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel, cancelLabel, disabled, formLayout, }: DynamicFormProps): import("react").JSX.Element;
19
28
  //# sourceMappingURL=dynamic-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAQ9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BA8FlB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAcrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAOhB,MAAM,uBAAuB,CAAA;AAQ9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,EAChB,UAAU,GACb,EAAE,gBAAgB,+BA8LlB"}
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // pattern + ActionFieldDef renderer so callers can reuse the form layout
4
4
  // outside the full record-edit modal.
5
5
  import { useEffect, useMemo, useState } from 'react';
6
+ import { groupFieldsBySection } from './form-layout';
7
+ import { FieldSection, WizardProgress } from './form-layout-ui';
6
8
  import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
7
- import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, applyOptionWhen, getDependsOn, } from './dynamic-form-schema';
9
+ import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, applyOptionWhen, getDependsOn, getVisibleWhen, evaluateVisibleWhen, } from './dynamic-form-schema';
8
10
  import { useOptionsResolver } from './use-options-resolver';
9
11
  import { DynamicLineItems } from './dynamic-line-items';
10
12
  import { DynamicSelectField } from './dynamic-select-field';
@@ -17,7 +19,7 @@ export { DynamicSelectField } from './dynamic-select-field';
17
19
  export { DynamicDateField } from './dynamic-date-field';
18
20
  export { UploadField } from './upload-field';
19
21
  export { IconPickerField } from './icon-picker-field';
20
- export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
22
+ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, formLayout, }) {
21
23
  const [values, setValues] = useState({});
22
24
  const [errors, setErrors] = useState({});
23
25
  const [submitting, setSubmitting] = useState(false);
@@ -27,19 +29,38 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
27
29
  // (ColumnDef.Readonly). They are also excluded from defaults/submit so a
28
30
  // create never posts e.g. `status: ""` over the column default.
29
31
  const editableFields = useMemo(() => fields.filter((f) => !f.readonly), [fields]);
30
- const schema = useMemo(() => buildZodSchema(editableFields), [editableFields]);
32
+ // Conditional visibility: a field carrying `visible_when` is rendered — and
33
+ // validated — only while the referenced sibling field's current value
34
+ // matches the predicate. Driving BOTH the schema and the render off the same
35
+ // filtered list means a hidden field never blocks submit (its required-gate
36
+ // is dropped with it), matching the primitive's contract. Fields with no
37
+ // `visible_when` are always kept (retrocompat).
38
+ const visibleFields = useMemo(() => editableFields.filter((f) => evaluateVisibleWhen(getVisibleWhen(f), values)), [editableFields, values]);
39
+ const schema = useMemo(() => buildZodSchema(visibleFields), [visibleFields]);
31
40
  // Line-items fields carrying a balance rule gate submit: an unbalanced entry
32
41
  // (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
33
42
  // is fully declarative — `evaluateBalance` returns undefined for fields with
34
43
  // no rule, so non-balanced forms are unaffected.
35
44
  const balanceBlocked = useMemo(() => {
36
- for (const f of editableFields) {
45
+ for (const f of visibleFields) {
37
46
  const state = evaluateBalance(f, values[f.key]);
38
47
  if (state && !state.balanced)
39
48
  return true;
40
49
  }
41
50
  return false;
42
- }, [editableFields, values]);
51
+ }, [visibleFields, values]);
52
+ // Group visible fields by their form_layout section (empty sections drop out
53
+ // because visibleFields is already visibility-filtered). Without a layout
54
+ // this is a single default group → the render below collapses to the legacy
55
+ // flat grid, byte-for-byte.
56
+ const groups = useMemo(() => groupFieldsBySection(visibleFields, formLayout), [visibleFields, formLayout]);
57
+ const isSteps = formLayout?.mode === 'steps' && groups.length > 1;
58
+ // Wizard step cursor (steps mode only). Clamped whenever the group count
59
+ // shrinks (a visible_when flip can empty a trailing step's section).
60
+ const [stepIndex, setStepIndex] = useState(0);
61
+ useEffect(() => {
62
+ setStepIndex((i) => Math.min(i, Math.max(groups.length - 1, 0)));
63
+ }, [groups.length]);
43
64
  useEffect(() => {
44
65
  const defaults = {};
45
66
  for (const f of editableFields) {
@@ -77,11 +98,43 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
77
98
  setSubmitting(false);
78
99
  }
79
100
  };
101
+ // Renders one group's fields into the responsive 2-column grid: scalar
102
+ // header fields flow through it; line-items grids (and textareas) span full
103
+ // width so the row table / memo gets room. Shared by every layout mode.
104
+ const renderGrid = (groupFields) => (_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: groupFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }));
105
+ // ── Steps (wizard) mode ────────────────────────────────────────────────
106
+ // One step per section; Anterior/Siguiente navigate, submit only on the
107
+ // last step. Advancing validates just the current step's fields so a later
108
+ // step can't be blocked by an earlier untouched one and vice-versa.
109
+ if (isSteps) {
110
+ const step = groups[stepIndex];
111
+ const isLast = stepIndex === groups.length - 1;
112
+ const goNext = () => {
113
+ const stepSchema = buildZodSchema(step.fields);
114
+ const result = stepSchema.safeParse(values);
115
+ if (!result.success) {
116
+ const next = {};
117
+ for (const issue of result.error.issues) {
118
+ const key = issue.path[0];
119
+ if (typeof key === 'string' && !next[key])
120
+ next[key] = issue.message;
121
+ }
122
+ setErrors(next);
123
+ return;
124
+ }
125
+ setErrors({});
126
+ setStepIndex((i) => Math.min(i + 1, groups.length - 1));
127
+ };
128
+ const goBack = () => setStepIndex((i) => Math.max(i - 1, 0));
129
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx(WizardProgress, { groups: groups, stepIndex: stepIndex }), renderGrid(step.fields), _jsxs("div", { className: "flex justify-between gap-2 pt-2", children: [stepIndex > 0 ? (_jsx(Button, { type: "button", variant: "outline", onClick: goBack, disabled: submitting || disabled, children: "Anterior" })) : onCancel ? (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })) : (_jsx("span", {})), isLast ? (_jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })) : (_jsx(Button, { type: "button", onClick: goNext, disabled: submitting || disabled, children: "Siguiente" }))] })] }));
130
+ }
131
+ // ── Sections / flat mode ───────────────────────────────────────────────
80
132
  // Layout: scalar header fields flow through a responsive 2-column grid;
81
133
  // line-items grids (and textareas) span the full width so the row table /
82
134
  // memo gets room. Mirrors the pro look of the federated journal modal but
83
- // stays fully declarative — driven only by field shape.
84
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: editableFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
135
+ // stays fully declarative — driven only by field shape. With no layout this
136
+ // is a single default group rendered without section chrome (unchanged).
137
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [groups.map((group) => (_jsx(FieldSection, { group: group, children: renderGrid(group.fields) }, group.key))), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
85
138
  }
86
139
  // One form field row: label + renderer + inline error. Encapsulated as its own
87
140
  // component so a STATIC enum select whose options are all gated out by a sibling
@@ -27,6 +27,18 @@ interface CommonProps {
27
27
  filters?: Record<string, string>;
28
28
  /** Hidden columns; el FK siempre se oculta automáticamente. */
29
29
  hiddenColumns?: string[];
30
+ /**
31
+ * Contexto de sub-tabla de líneas dentro del MODAL de vista de un registro
32
+ * padre. Cuando es true se aplican las reglas de {@link isColumnVisibleInLineSubtable}
33
+ * — se ocultan por defecto las columnas de auditoría/sistema (created_by,
34
+ * timestamps, organization_id) y las scopeadas a `visibility: "table"`, que
35
+ * son ruido redundante bajo el registro padre.
36
+ *
37
+ * Default false: en una página de detalle autónoma (`/m/<model>/<id>`) el
38
+ * panel de relación conserva el comportamiento previo (solo oculta FK, scope
39
+ * y columnas `hidden`), donde esas columnas SÍ son útiles.
40
+ */
41
+ lineSubtable?: boolean;
30
42
  /** Permisos visibles. Default true. */
31
43
  canCreate?: boolean;
32
44
  canDelete?: boolean;