@asteby/metacore-runtime-react 28.4.1 → 28.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 28.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 21c5268: Soporta `visible_when` a nivel de SECCIÓN en `form_layout` (kernel v0.84.0). Cuando una sección declara un predicado `visible_when` que evalúa falso contra los valores actuales del formulario, la sección se oculta por completo junto con sus campos; en modo wizard (steps) ese paso desaparece de la secuencia de navegación sin romper Anterior/Siguiente ni el submit del último paso. Reutiliza el mismo evaluador (`getVisibleWhen`/`evaluateVisibleWhen`) que ya usan los campos, tolerando snake_case y camelCase. El `visible_when` de cada campo se sigue respetando dentro de una sección visible, y sin `visible_when` de sección el comportamiento es idéntico al actual.
8
+
9
+ ## 28.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
15
+ ### Patch Changes
16
+
17
+ - b05aded: DynamicRecordDialog ya no envía en el submit los campos ocultos por `visible_when`, en paridad con DynamicForm.
18
+
3
19
  ## 28.4.1
4
20
 
5
21
  ### Patch Changes
@@ -83,6 +83,12 @@ export interface FieldDef {
83
83
  visible_when?: VisibleWhen;
84
84
  /** camelCase alias for `visible_when`. */
85
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;
86
92
  }
87
93
  export interface DynamicRecordDialogProps {
88
94
  open: boolean;
@@ -178,6 +184,7 @@ export declare function isLineItemsField(field: FieldDef, value: any): boolean;
178
184
  export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
179
185
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
180
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>;
181
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;
182
189
  export declare function ReadonlyEditField({ field, value }: {
183
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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"}
@@ -24,6 +24,8 @@ import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-f
24
24
  import { DynamicRelations } from '../dynamic-relations';
25
25
  import { useOptionsResolver } from '../use-options-resolver';
26
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';
@@ -257,6 +259,25 @@ export function filterVisibleFields(fields, mode, formValues) {
257
259
  return true;
258
260
  });
259
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
+ }
260
281
  export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
261
282
  const api = useApi();
262
283
  const { t } = useTranslation();
@@ -271,6 +292,8 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
271
292
  const [loading, setLoading] = useState(false);
272
293
  const [saving, setSaving] = useState(false);
273
294
  const [deleting, setDeleting] = useState(false);
295
+ // Wizard step cursor (form_layout mode:"steps" only).
296
+ const [stepIndex, setStepIndex] = useState(0);
274
297
  const isCreate = mode === 'create';
275
298
  const isView = mode === 'view';
276
299
  const isEditable = mode === 'create' || mode === 'edit';
@@ -281,8 +304,10 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
281
304
  return;
282
305
  if (!isCreate && !recordId)
283
306
  return;
284
- // 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.
285
309
  setFieldErrors({});
310
+ setStepIndex(0);
286
311
  let cancelled = false;
287
312
  // Seed instantly from the row the table already has so view/edit render
288
313
  // without a spinner. The list row carries the pro siblings (resolved
@@ -489,9 +514,15 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
489
514
  }
490
515
  // Required check passed → clear any prior validation errors.
491
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);
492
523
  // Empty reference pickers → null (not "" / nil-UUID) so nullable FK
493
524
  // columns accept them instead of raising a 23503 FK violation.
494
- const payload = normalizeRefFieldsForSubmit(formValues, modalMeta.fields);
525
+ const payload = normalizeRefFieldsForSubmit(submittedValues, modalMeta.fields);
495
526
  setSaving(true);
496
527
  try {
497
528
  if (isCreate && onCreate) {
@@ -563,22 +594,51 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
563
594
  };
564
595
  const title = modalMeta ? config.getTitle(modalMeta, t) : '';
565
596
  const visibleFields = filterVisibleFields(modalMeta?.fields, mode, formValues);
566
- 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 => {
567
- const isFullWidth = field.type === 'textarea' ||
568
- field.widget === 'textarea' ||
569
- field.widget === 'richtext';
570
- return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, error: fieldErrors[field.key], onChange: val => {
571
- setFormValues((prev) => ({ ...prev, [field.key]: val }));
572
- // Clear this field's error as soon as the user edits it.
573
- setFieldErrors(prev => {
574
- if (!prev[field.key])
575
- return prev;
576
- const next = { ...prev };
577
- delete next[field.key];
578
- return next;
579
- });
580
- } }) }, field.key));
581
- }), 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, 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: [_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] }))] })] })] }) }));
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, formValues);
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] }))] })] })] }) }));
582
642
  }
583
643
  function LoadingSkeleton() {
584
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,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,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;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BAyGlB"}
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,6 +3,8 @@ 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
9
  import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, applyOptionWhen, getDependsOn, getVisibleWhen, evaluateVisibleWhen, } from './dynamic-form-schema';
8
10
  import { useOptionsResolver } from './use-options-resolver';
@@ -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);
@@ -47,6 +49,18 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
47
49
  }
48
50
  return false;
49
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, values), [visibleFields, formLayout, values]);
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]);
50
64
  useEffect(() => {
51
65
  const defaults = {};
52
66
  for (const f of editableFields) {
@@ -84,11 +98,43 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
84
98
  setSubmitting(false);
85
99
  }
86
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 ───────────────────────────────────────────────
87
132
  // Layout: scalar header fields flow through a responsive 2-column grid;
88
133
  // line-items grids (and textareas) span the full width so the row table /
89
134
  // memo gets room. Mirrors the pro look of the federated journal modal but
90
- // stays fully declarative — driven only by field shape.
91
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: visibleFields.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 })] })] }));
92
138
  }
93
139
  // One form field row: label + renderer + inline error. Encapsulated as its own
94
140
  // component so a STATIC enum select whose options are all gated out by a sibling
@@ -0,0 +1,25 @@
1
+ import type { FieldGroup } from './form-layout';
2
+ /**
3
+ * Section chrome for `mode: "sections"`. Wraps a group's (already gridded)
4
+ * fields in a titled block.
5
+ * - The default/orphan group renders with NO chrome, so a layout-less form (one
6
+ * default group) is visually identical to the legacy flat list.
7
+ * - A section whose `collapsed` is defined (true/false) is rendered COLLAPSIBLE,
8
+ * starting collapsed when `collapsed === true`. A section with no `collapsed`
9
+ * flag renders as a plain, always-open titled block.
10
+ */
11
+ export declare function FieldSection({ group, children, }: {
12
+ group: FieldGroup<unknown>;
13
+ children: React.ReactNode;
14
+ }): import("react").JSX.Element;
15
+ /**
16
+ * Progress bar for `mode: "steps"`: one filled segment per completed/current
17
+ * step plus a "Paso i/n · <title>" caption. Mirrors the WizardActionModal look
18
+ * so a model wizard and an action wizard read the same.
19
+ */
20
+ export declare function WizardProgress({ groups, stepIndex, stepLabel, }: {
21
+ groups: FieldGroup<unknown>[];
22
+ stepIndex: number;
23
+ stepLabel?: string;
24
+ }): import("react").JSX.Element;
25
+ //# sourceMappingURL=form-layout-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-layout-ui.d.ts","sourceRoot":"","sources":["../src/form-layout-ui.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,GACX,EAAE;IACC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B,+BA6CA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,MAAM,EACN,SAAS,EACT,SAAkB,GACrB,EAAE;IACC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,+BA2BA"}
@@ -0,0 +1,39 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Presentational chrome for declarative form layouts (see `form-layout.ts`).
3
+ // Kept apart from the grouping logic so the pure helper stays React-free and the
4
+ // two renderers (`dynamic-form.tsx`, `dialogs/dynamic-record.tsx`) share one
5
+ // look for sections and the wizard progress bar.
6
+ import { useState } from 'react';
7
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, Button, } from '@asteby/metacore-ui/primitives';
8
+ import { ChevronDown } from 'lucide-react';
9
+ /**
10
+ * Section chrome for `mode: "sections"`. Wraps a group's (already gridded)
11
+ * fields in a titled block.
12
+ * - The default/orphan group renders with NO chrome, so a layout-less form (one
13
+ * default group) is visually identical to the legacy flat list.
14
+ * - A section whose `collapsed` is defined (true/false) is rendered COLLAPSIBLE,
15
+ * starting collapsed when `collapsed === true`. A section with no `collapsed`
16
+ * flag renders as a plain, always-open titled block.
17
+ */
18
+ export function FieldSection({ group, children, }) {
19
+ const collapsible = group.collapsed !== undefined;
20
+ const [open, setOpen] = useState(!group.collapsed);
21
+ if (group.isDefault)
22
+ return _jsx(_Fragment, { children: children });
23
+ const header = (_jsxs("div", { className: "min-w-0 text-left", children: [group.title && (_jsx("h3", { className: "text-sm font-semibold leading-none", children: group.title })), group.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: group.description }))] }));
24
+ if (!collapsible) {
25
+ return (_jsxs("section", { className: "grid gap-3", children: [(group.title || group.description) && header, children] }));
26
+ }
27
+ return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, className: "grid gap-3", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", className: "h-auto w-full justify-between px-0 py-1 hover:bg-transparent", children: [header, _jsx(ChevronDown, { className: 'h-4 w-4 shrink-0 transition-transform ' + (open ? 'rotate-180' : '') })] }) }), _jsx(CollapsibleContent, { className: "grid gap-3", children: children })] }));
28
+ }
29
+ /**
30
+ * Progress bar for `mode: "steps"`: one filled segment per completed/current
31
+ * step plus a "Paso i/n · <title>" caption. Mirrors the WizardActionModal look
32
+ * so a model wizard and an action wizard read the same.
33
+ */
34
+ export function WizardProgress({ groups, stepIndex, stepLabel = 'Paso', }) {
35
+ const current = groups[stepIndex];
36
+ return (_jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "flex items-center gap-1.5", role: "list", "aria-label": "progress", children: groups.map((g, i) => (_jsx("div", { role: "listitem", "aria-current": i === stepIndex ? 'step' : undefined, className: "h-1.5 flex-1 rounded-full", style: {
37
+ backgroundColor: i <= stepIndex ? 'hsl(var(--primary))' : 'hsl(var(--muted))',
38
+ } }, g.key))) }), _jsxs("p", { className: "pt-2 text-sm text-muted-foreground", children: [stepLabel, " ", stepIndex + 1, "/", groups.length, current?.title ? ` · ${current.title}` : ''] }), current?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: current.description }))] }));
39
+ }
@@ -0,0 +1,68 @@
1
+ import type { VisibleWhen } from './types';
2
+ /** One declared section of a model form. `title`/`description` arrive already
3
+ * localized from the kernel and are rendered verbatim. */
4
+ export interface FormSection {
5
+ key: string;
6
+ title?: string;
7
+ description?: string;
8
+ /** Sections mode only: render this section collapsed on first paint. */
9
+ collapsed?: boolean;
10
+ /** Section-level visibility gate (kernel v0.84.0). When present and its
11
+ * predicate evaluates false against the live form values, the whole section
12
+ * is omitted (and in steps mode its wizard step drops from the sequence).
13
+ * Tolerates the camelCase alias, same as fields. */
14
+ visible_when?: VisibleWhen;
15
+ visibleWhen?: VisibleWhen;
16
+ }
17
+ /** Model-level layout directive. `mode` defaults to `"sections"`. */
18
+ export interface FormLayout {
19
+ mode?: 'sections' | 'steps';
20
+ sections?: FormSection[];
21
+ }
22
+ /** A resolved group of fields ready to render: a section plus the (already
23
+ * visibility-filtered) fields that belong to it. */
24
+ export interface FieldGroup<F> {
25
+ /** Section key, or `__default__` for the orphan group. */
26
+ key: string;
27
+ title?: string;
28
+ description?: string;
29
+ collapsed?: boolean;
30
+ /** True for the synthetic group holding section-less / unknown-section fields. */
31
+ isDefault: boolean;
32
+ fields: F[];
33
+ }
34
+ /** The synthetic key of the orphan group (fields with no / unknown `section`). */
35
+ export declare const DEFAULT_SECTION_KEY = "__default__";
36
+ /** Reads a field's `section` reference, tolerating a camelCase alias. */
37
+ export declare function getFieldSection(field: {
38
+ section?: string;
39
+ Section?: string;
40
+ } | null | undefined): string | undefined;
41
+ /**
42
+ * Groups already-filtered (visible) fields by their `section`, honoring the
43
+ * order of `layout.sections`.
44
+ *
45
+ * Contract:
46
+ * - No `layout` (or no `sections`) → a single default group carrying every
47
+ * field in its original order. Callers render this exactly like the legacy
48
+ * flat list, so the layout-less path is byte-for-byte the current behaviour.
49
+ * - Fields whose `section` is empty or references an UNKNOWN section key are
50
+ * collected into ONE default group placed FIRST (before any declared
51
+ * section) — the consistent, documented choice: general/uncategorized fields
52
+ * lead, declared sections follow in their authored order.
53
+ * - A declared section with zero visible fields is OMITTED entirely, so a
54
+ * section whose only members are hidden by `visible_when` never renders an
55
+ * empty shell (and never yields an empty wizard step). Because the caller
56
+ * passes the already visibility-filtered list, this falls out for free.
57
+ * - A section declaring its OWN `visible_when` is dropped whole (before its
58
+ * fields are even collected) whenever the predicate evaluates false against
59
+ * `values`, reusing the SAME evaluator the fields use. In steps mode the
60
+ * caller derives the wizard sequence from these groups, so a hidden section
61
+ * simply never becomes a step. Section-less / unknown-section fields are
62
+ * never gated by any section predicate. Without a section `visible_when`
63
+ * (or without `values`), behaviour is byte-for-byte the legacy path.
64
+ */
65
+ export declare function groupFieldsBySection<F extends {
66
+ section?: string;
67
+ }>(fields: F[], layout: FormLayout | undefined, values?: Record<string, any> | null): FieldGroup<F>[];
68
+ //# sourceMappingURL=form-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-layout.d.ts","sourceRoot":"","sources":["../src/form-layout.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAG1C;0DAC0D;AAC1D,MAAM,WAAW,WAAW;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;wDAGoD;IACpD,YAAY,CAAC,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;IAC3B,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;CAC3B;AAED;oDACoD;AACpD,MAAM,WAAW,UAAU,CAAC,CAAC;IACzB,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,kFAAkF;IAClF,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,CAAC,EAAE,CAAA;CACd;AAED,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,gBAAgB,CAAA;AAEhD,yEAAyE;AACzE,wBAAgB,eAAe,CAC3B,KAAK,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GACjE,MAAM,GAAG,SAAS,CAIpB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/D,MAAM,EAAE,CAAC,EAAE,EACX,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GACpC,UAAU,CAAC,CAAC,CAAC,EAAE,CAoDjB"}
@@ -0,0 +1,101 @@
1
+ // Declarative form layout (kernel PR #230): a model's create/edit form can
2
+ // declare `form_layout` to group its fields into named sections, rendered either
3
+ // as stacked (optionally collapsible) SECTIONS or as a multi-step WIZARD.
4
+ //
5
+ // Wire shape served on the table/modal metadata:
6
+ // form_layout: {
7
+ // mode: "sections" | "steps", // default "sections"
8
+ // sections: [{ key, title?, description?, collapsed? }]
9
+ // }
10
+ // and, per field/column: `section: "<key>"` referencing a section key.
11
+ //
12
+ // This module owns the PURE grouping logic (no React), so the two renderers
13
+ // (`dynamic-form.tsx`, `dialogs/dynamic-record.tsx`) share one tested
14
+ // implementation and the modal-render machinery never has to be booted under
15
+ // happy-dom just to assert the grouping (same approach as PR #673).
16
+ import { evaluateVisibleWhen, getVisibleWhen } from './dynamic-form-schema';
17
+ /** The synthetic key of the orphan group (fields with no / unknown `section`). */
18
+ export const DEFAULT_SECTION_KEY = '__default__';
19
+ /** Reads a field's `section` reference, tolerating a camelCase alias. */
20
+ export function getFieldSection(field) {
21
+ if (!field)
22
+ return undefined;
23
+ const s = field.section ?? field.Section;
24
+ return typeof s === 'string' && s !== '' ? s : undefined;
25
+ }
26
+ /**
27
+ * Groups already-filtered (visible) fields by their `section`, honoring the
28
+ * order of `layout.sections`.
29
+ *
30
+ * Contract:
31
+ * - No `layout` (or no `sections`) → a single default group carrying every
32
+ * field in its original order. Callers render this exactly like the legacy
33
+ * flat list, so the layout-less path is byte-for-byte the current behaviour.
34
+ * - Fields whose `section` is empty or references an UNKNOWN section key are
35
+ * collected into ONE default group placed FIRST (before any declared
36
+ * section) — the consistent, documented choice: general/uncategorized fields
37
+ * lead, declared sections follow in their authored order.
38
+ * - A declared section with zero visible fields is OMITTED entirely, so a
39
+ * section whose only members are hidden by `visible_when` never renders an
40
+ * empty shell (and never yields an empty wizard step). Because the caller
41
+ * passes the already visibility-filtered list, this falls out for free.
42
+ * - A section declaring its OWN `visible_when` is dropped whole (before its
43
+ * fields are even collected) whenever the predicate evaluates false against
44
+ * `values`, reusing the SAME evaluator the fields use. In steps mode the
45
+ * caller derives the wizard sequence from these groups, so a hidden section
46
+ * simply never becomes a step. Section-less / unknown-section fields are
47
+ * never gated by any section predicate. Without a section `visible_when`
48
+ * (or without `values`), behaviour is byte-for-byte the legacy path.
49
+ */
50
+ export function groupFieldsBySection(fields, layout, values) {
51
+ if (!layout?.sections?.length) {
52
+ return [{ key: DEFAULT_SECTION_KEY, isDefault: true, fields }];
53
+ }
54
+ // Sections whose own `visible_when` predicate evaluates false are hidden
55
+ // WHOLESALE: the section never emits a group/step AND its member fields are
56
+ // dropped (they must not leak into the default group). Reuses the same
57
+ // field-level evaluator — no duplicated logic. A `declared` set keeps a
58
+ // field targeting a hidden section from being treated as "unknown section"
59
+ // and orphaned.
60
+ const declared = new Set(layout.sections.map((s) => s.key));
61
+ const visibleSections = layout.sections.filter((s) => evaluateVisibleWhen(getVisibleWhen(s), values));
62
+ const known = new Set(visibleSections.map((s) => s.key));
63
+ const bySection = new Map();
64
+ const orphans = [];
65
+ for (const f of fields) {
66
+ const sec = getFieldSection(f);
67
+ if (sec && known.has(sec)) {
68
+ const arr = bySection.get(sec);
69
+ if (arr)
70
+ arr.push(f);
71
+ else
72
+ bySection.set(sec, [f]);
73
+ }
74
+ else if (sec && declared.has(sec)) {
75
+ // Belongs to a declared-but-hidden section → drop the field.
76
+ continue;
77
+ }
78
+ else {
79
+ orphans.push(f);
80
+ }
81
+ }
82
+ const groups = [];
83
+ // Orphan/default group leads (fields with no / unknown section).
84
+ if (orphans.length) {
85
+ groups.push({ key: DEFAULT_SECTION_KEY, isDefault: true, fields: orphans });
86
+ }
87
+ for (const s of visibleSections) {
88
+ const secFields = bySection.get(s.key);
89
+ if (!secFields || secFields.length === 0)
90
+ continue; // hide empty section
91
+ groups.push({
92
+ key: s.key,
93
+ title: s.title,
94
+ description: s.description,
95
+ collapsed: s.collapsed,
96
+ isDefault: false,
97
+ fields: secFields,
98
+ });
99
+ }
100
+ return groups;
101
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export { useStageOverrides, type StageOverridePatch, type UseStageOverridesResul
11
11
  export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
12
12
  export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
13
13
  export * from './dynamic-form';
14
+ export * from './form-layout';
15
+ export * from './form-layout-ui';
14
16
  export { FieldGrid, FieldCell, FieldLabel } from './field-grid';
15
17
  export { ActionModalDispatcher, type ActionModalProps, } from './action-modal-dispatcher';
16
18
  export { ModelActionToolbar, useModelActions, type ModelActionToolbarProps, type ActionPlacement, } from './model-action-toolbar';