@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 +16 -0
- package/dist/dialogs/dynamic-record.d.ts +7 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +78 -18
- package/dist/dynamic-form.d.ts +10 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +49 -3
- package/dist/form-layout-ui.d.ts +25 -0
- package/dist/form-layout-ui.d.ts.map +1 -0
- package/dist/form-layout-ui.js +39 -0
- package/dist/form-layout.d.ts +68 -0
- package/dist/form-layout.d.ts.map +1 -0
- package/dist/form-layout.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/dynamic-record-hidden-submit.test.tsx +78 -0
- package/src/__tests__/form-layout.test.ts +201 -0
- package/src/dialogs/dynamic-record.tsx +160 -46
- package/src/dynamic-form.tsx +110 -14
- package/src/form-layout-ui.tsx +117 -0
- package/src/form-layout.ts +147 -0
- package/src/index.ts +2 -0
- package/src/types.ts +12 -0
|
@@ -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
|
/**
|