@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.
@@ -0,0 +1,147 @@
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
+
17
+ import type { VisibleWhen } from './types'
18
+ import { evaluateVisibleWhen, getVisibleWhen } from './dynamic-form-schema'
19
+
20
+ /** One declared section of a model form. `title`/`description` arrive already
21
+ * localized from the kernel and are rendered verbatim. */
22
+ export interface FormSection {
23
+ key: string
24
+ title?: string
25
+ description?: string
26
+ /** Sections mode only: render this section collapsed on first paint. */
27
+ collapsed?: boolean
28
+ /** Section-level visibility gate (kernel v0.84.0). When present and its
29
+ * predicate evaluates false against the live form values, the whole section
30
+ * is omitted (and in steps mode its wizard step drops from the sequence).
31
+ * Tolerates the camelCase alias, same as fields. */
32
+ visible_when?: VisibleWhen
33
+ visibleWhen?: VisibleWhen
34
+ }
35
+
36
+ /** Model-level layout directive. `mode` defaults to `"sections"`. */
37
+ export interface FormLayout {
38
+ mode?: 'sections' | 'steps'
39
+ sections?: FormSection[]
40
+ }
41
+
42
+ /** A resolved group of fields ready to render: a section plus the (already
43
+ * visibility-filtered) fields that belong to it. */
44
+ export interface FieldGroup<F> {
45
+ /** Section key, or `__default__` for the orphan group. */
46
+ key: string
47
+ title?: string
48
+ description?: string
49
+ collapsed?: boolean
50
+ /** True for the synthetic group holding section-less / unknown-section fields. */
51
+ isDefault: boolean
52
+ fields: F[]
53
+ }
54
+
55
+ /** The synthetic key of the orphan group (fields with no / unknown `section`). */
56
+ export const DEFAULT_SECTION_KEY = '__default__'
57
+
58
+ /** Reads a field's `section` reference, tolerating a camelCase alias. */
59
+ export function getFieldSection(
60
+ field: { section?: string; Section?: string } | null | undefined,
61
+ ): string | undefined {
62
+ if (!field) return undefined
63
+ const s = field.section ?? (field as { Section?: string }).Section
64
+ return typeof s === 'string' && s !== '' ? s : undefined
65
+ }
66
+
67
+ /**
68
+ * Groups already-filtered (visible) fields by their `section`, honoring the
69
+ * order of `layout.sections`.
70
+ *
71
+ * Contract:
72
+ * - No `layout` (or no `sections`) → a single default group carrying every
73
+ * field in its original order. Callers render this exactly like the legacy
74
+ * flat list, so the layout-less path is byte-for-byte the current behaviour.
75
+ * - Fields whose `section` is empty or references an UNKNOWN section key are
76
+ * collected into ONE default group placed FIRST (before any declared
77
+ * section) — the consistent, documented choice: general/uncategorized fields
78
+ * lead, declared sections follow in their authored order.
79
+ * - A declared section with zero visible fields is OMITTED entirely, so a
80
+ * section whose only members are hidden by `visible_when` never renders an
81
+ * empty shell (and never yields an empty wizard step). Because the caller
82
+ * passes the already visibility-filtered list, this falls out for free.
83
+ * - A section declaring its OWN `visible_when` is dropped whole (before its
84
+ * fields are even collected) whenever the predicate evaluates false against
85
+ * `values`, reusing the SAME evaluator the fields use. In steps mode the
86
+ * caller derives the wizard sequence from these groups, so a hidden section
87
+ * simply never becomes a step. Section-less / unknown-section fields are
88
+ * never gated by any section predicate. Without a section `visible_when`
89
+ * (or without `values`), behaviour is byte-for-byte the legacy path.
90
+ */
91
+ export function groupFieldsBySection<F extends { section?: string }>(
92
+ fields: F[],
93
+ layout: FormLayout | undefined,
94
+ values?: Record<string, any> | null,
95
+ ): FieldGroup<F>[] {
96
+ if (!layout?.sections?.length) {
97
+ return [{ key: DEFAULT_SECTION_KEY, isDefault: true, fields }]
98
+ }
99
+
100
+ // Sections whose own `visible_when` predicate evaluates false are hidden
101
+ // WHOLESALE: the section never emits a group/step AND its member fields are
102
+ // dropped (they must not leak into the default group). Reuses the same
103
+ // field-level evaluator — no duplicated logic. A `declared` set keeps a
104
+ // field targeting a hidden section from being treated as "unknown section"
105
+ // and orphaned.
106
+ const declared = new Set(layout.sections.map((s) => s.key))
107
+ const visibleSections = layout.sections.filter((s) =>
108
+ evaluateVisibleWhen(getVisibleWhen(s), values),
109
+ )
110
+
111
+ const known = new Set(visibleSections.map((s) => s.key))
112
+ const bySection = new Map<string, F[]>()
113
+ const orphans: F[] = []
114
+
115
+ for (const f of fields) {
116
+ const sec = getFieldSection(f)
117
+ if (sec && known.has(sec)) {
118
+ const arr = bySection.get(sec)
119
+ if (arr) arr.push(f)
120
+ else bySection.set(sec, [f])
121
+ } else if (sec && declared.has(sec)) {
122
+ // Belongs to a declared-but-hidden section → drop the field.
123
+ continue
124
+ } else {
125
+ orphans.push(f)
126
+ }
127
+ }
128
+
129
+ const groups: FieldGroup<F>[] = []
130
+ // Orphan/default group leads (fields with no / unknown section).
131
+ if (orphans.length) {
132
+ groups.push({ key: DEFAULT_SECTION_KEY, isDefault: true, fields: orphans })
133
+ }
134
+ for (const s of visibleSections) {
135
+ const secFields = bySection.get(s.key)
136
+ if (!secFields || secFields.length === 0) continue // hide empty section
137
+ groups.push({
138
+ key: s.key,
139
+ title: s.title,
140
+ description: s.description,
141
+ collapsed: s.collapsed,
142
+ isDefault: false,
143
+ fields: secFields,
144
+ })
145
+ }
146
+ return groups
147
+ }
package/src/index.ts CHANGED
@@ -107,6 +107,8 @@ export {
107
107
  type UseDynamicFiltersResult,
108
108
  } from './use-dynamic-filters'
109
109
  export * from './dynamic-form'
110
+ export * from './form-layout'
111
+ export * from './form-layout-ui'
110
112
  export { FieldGrid, FieldCell, FieldLabel } from './field-grid'
111
113
  export {
112
114
  ActionModalDispatcher,
package/src/types.ts CHANGED
@@ -306,6 +306,12 @@ export interface ColumnDefinition {
306
306
  visible_when?: VisibleWhen
307
307
  /** camelCase alias for `visible_when`. */
308
308
  visibleWhen?: VisibleWhen
309
+ /**
310
+ * Form-layout membership: the key of the `form_layout` section this column's
311
+ * modal field belongs to (kernel PR #230). Absent → the default group. See
312
+ * `groupFieldsBySection`.
313
+ */
314
+ section?: string
309
315
  }
310
316
 
311
317
  /**
@@ -529,6 +535,12 @@ export interface ActionFieldDef {
529
535
  visible_when?: VisibleWhen
530
536
  /** camelCase alias for `visible_when`. */
531
537
  visibleWhen?: VisibleWhen
538
+ /**
539
+ * Form-layout membership: the key of the `form_layout` section this field
540
+ * belongs to (kernel PR #230). Absent → the field lands in the default
541
+ * group. See `groupFieldsBySection`.
542
+ */
543
+ section?: string
532
544
  }
533
545
 
534
546
  /**