@asteby/metacore-runtime-react 28.5.0 → 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 +6 -0
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-form.js +1 -1
- package/dist/form-layout.d.ts +15 -1
- package/dist/form-layout.d.ts.map +1 -1
- package/dist/form-layout.js +23 -3
- package/package.json +1 -1
- package/src/__tests__/form-layout.test.ts +88 -0
- package/src/dialogs/dynamic-record.tsx +1 -1
- package/src/dynamic-form.tsx +2 -2
- package/src/form-layout.ts +33 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 28.5.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -598,7 +598,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
598
598
|
// their section. Empty sections drop out for free. Steps mode only drives a
|
|
599
599
|
// wizard in editable modes — view mode always stacks the sections.
|
|
600
600
|
const formLayout = modalMeta?.form_layout ?? modalMeta?.formLayout;
|
|
601
|
-
const groups = groupFieldsBySection(visibleFields, formLayout);
|
|
601
|
+
const groups = groupFieldsBySection(visibleFields, formLayout, formValues);
|
|
602
602
|
const isSteps = isEditable && formLayout?.mode === 'steps' && groups.length > 1;
|
|
603
603
|
const clampedStep = Math.min(stepIndex, Math.max(groups.length - 1, 0));
|
|
604
604
|
const isLastStep = clampedStep === groups.length - 1;
|
package/dist/dynamic-form.js
CHANGED
|
@@ -53,7 +53,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
53
53
|
// because visibleFields is already visibility-filtered). Without a layout
|
|
54
54
|
// this is a single default group → the render below collapses to the legacy
|
|
55
55
|
// flat grid, byte-for-byte.
|
|
56
|
-
const groups = useMemo(() => groupFieldsBySection(visibleFields, formLayout), [visibleFields, formLayout]);
|
|
56
|
+
const groups = useMemo(() => groupFieldsBySection(visibleFields, formLayout, values), [visibleFields, formLayout, values]);
|
|
57
57
|
const isSteps = formLayout?.mode === 'steps' && groups.length > 1;
|
|
58
58
|
// Wizard step cursor (steps mode only). Clamped whenever the group count
|
|
59
59
|
// shrinks (a visible_when flip can empty a trailing step's section).
|
package/dist/form-layout.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VisibleWhen } from './types';
|
|
1
2
|
/** One declared section of a model form. `title`/`description` arrive already
|
|
2
3
|
* localized from the kernel and are rendered verbatim. */
|
|
3
4
|
export interface FormSection {
|
|
@@ -6,6 +7,12 @@ export interface FormSection {
|
|
|
6
7
|
description?: string;
|
|
7
8
|
/** Sections mode only: render this section collapsed on first paint. */
|
|
8
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;
|
|
9
16
|
}
|
|
10
17
|
/** Model-level layout directive. `mode` defaults to `"sections"`. */
|
|
11
18
|
export interface FormLayout {
|
|
@@ -47,8 +54,15 @@ export declare function getFieldSection(field: {
|
|
|
47
54
|
* section whose only members are hidden by `visible_when` never renders an
|
|
48
55
|
* empty shell (and never yields an empty wizard step). Because the caller
|
|
49
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.
|
|
50
64
|
*/
|
|
51
65
|
export declare function groupFieldsBySection<F extends {
|
|
52
66
|
section?: string;
|
|
53
|
-
}>(fields: F[], layout: FormLayout | undefined): FieldGroup<F>[];
|
|
67
|
+
}>(fields: F[], layout: FormLayout | undefined, values?: Record<string, any> | null): FieldGroup<F>[];
|
|
54
68
|
//# sourceMappingURL=form-layout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-layout.d.ts","sourceRoot":"","sources":["../src/form-layout.ts"],"names":[],"mappings":"AAgBA;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;
|
|
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"}
|
package/dist/form-layout.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// (`dynamic-form.tsx`, `dialogs/dynamic-record.tsx`) share one tested
|
|
14
14
|
// implementation and the modal-render machinery never has to be booted under
|
|
15
15
|
// happy-dom just to assert the grouping (same approach as PR #673).
|
|
16
|
+
import { evaluateVisibleWhen, getVisibleWhen } from './dynamic-form-schema';
|
|
16
17
|
/** The synthetic key of the orphan group (fields with no / unknown `section`). */
|
|
17
18
|
export const DEFAULT_SECTION_KEY = '__default__';
|
|
18
19
|
/** Reads a field's `section` reference, tolerating a camelCase alias. */
|
|
@@ -38,12 +39,27 @@ export function getFieldSection(field) {
|
|
|
38
39
|
* section whose only members are hidden by `visible_when` never renders an
|
|
39
40
|
* empty shell (and never yields an empty wizard step). Because the caller
|
|
40
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.
|
|
41
49
|
*/
|
|
42
|
-
export function groupFieldsBySection(fields, layout) {
|
|
50
|
+
export function groupFieldsBySection(fields, layout, values) {
|
|
43
51
|
if (!layout?.sections?.length) {
|
|
44
52
|
return [{ key: DEFAULT_SECTION_KEY, isDefault: true, fields }];
|
|
45
53
|
}
|
|
46
|
-
|
|
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));
|
|
47
63
|
const bySection = new Map();
|
|
48
64
|
const orphans = [];
|
|
49
65
|
for (const f of fields) {
|
|
@@ -55,6 +71,10 @@ export function groupFieldsBySection(fields, layout) {
|
|
|
55
71
|
else
|
|
56
72
|
bySection.set(sec, [f]);
|
|
57
73
|
}
|
|
74
|
+
else if (sec && declared.has(sec)) {
|
|
75
|
+
// Belongs to a declared-but-hidden section → drop the field.
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
58
78
|
else {
|
|
59
79
|
orphans.push(f);
|
|
60
80
|
}
|
|
@@ -64,7 +84,7 @@ export function groupFieldsBySection(fields, layout) {
|
|
|
64
84
|
if (orphans.length) {
|
|
65
85
|
groups.push({ key: DEFAULT_SECTION_KEY, isDefault: true, fields: orphans });
|
|
66
86
|
}
|
|
67
|
-
for (const s of
|
|
87
|
+
for (const s of visibleSections) {
|
|
68
88
|
const secFields = bySection.get(s.key);
|
|
69
89
|
if (!secFields || secFields.length === 0)
|
|
70
90
|
continue; // hide empty section
|
package/package.json
CHANGED
|
@@ -111,3 +111,91 @@ describe('groupFieldsBySection — steps', () => {
|
|
|
111
111
|
expect(groups.map(g => g.key)).toEqual(['who', 'pay'])
|
|
112
112
|
})
|
|
113
113
|
})
|
|
114
|
+
|
|
115
|
+
describe('groupFieldsBySection — section-level visible_when (kernel v0.84.0)', () => {
|
|
116
|
+
// A discriminator field `kind` gates the `company` section: only visible for
|
|
117
|
+
// business customers. Tolerate both snake_case and camelCase authoring.
|
|
118
|
+
const gatedLayout: FormLayout = {
|
|
119
|
+
mode: 'sections',
|
|
120
|
+
sections: [
|
|
121
|
+
{ key: 'general', title: 'General' },
|
|
122
|
+
{
|
|
123
|
+
key: 'company',
|
|
124
|
+
title: 'Empresa',
|
|
125
|
+
visible_when: { field: 'kind', equals: 'business' },
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const fields: F[] = [
|
|
131
|
+
{ key: 'name', section: 'general' },
|
|
132
|
+
{ key: 'tax_id', section: 'company' },
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
it('hides the section AND its fields when the predicate is false', () => {
|
|
136
|
+
const groups = groupFieldsBySection(fields, gatedLayout, { kind: 'person' })
|
|
137
|
+
// `company` never appears, and `tax_id` is not leaked into any group.
|
|
138
|
+
expect(groups.map(g => g.key)).toEqual(['general'])
|
|
139
|
+
const allFields = groups.flatMap(g => g.fields.map(f => f.key))
|
|
140
|
+
expect(allFields).toEqual(['name'])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('shows the section when the predicate is satisfied', () => {
|
|
144
|
+
const groups = groupFieldsBySection(fields, gatedLayout, { kind: 'business' })
|
|
145
|
+
expect(groups.map(g => g.key)).toEqual(['general', 'company'])
|
|
146
|
+
expect(groups[1].fields.map(f => f.key)).toEqual(['tax_id'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('tolerates the camelCase visibleWhen alias', () => {
|
|
150
|
+
const camelLayout: FormLayout = {
|
|
151
|
+
mode: 'sections',
|
|
152
|
+
sections: [
|
|
153
|
+
{ key: 'general', title: 'General' },
|
|
154
|
+
{ key: 'company', visibleWhen: { field: 'kind', in: ['business'] } },
|
|
155
|
+
],
|
|
156
|
+
}
|
|
157
|
+
expect(groupFieldsBySection(fields, camelLayout, { kind: 'person' }).map(g => g.key)).toEqual([
|
|
158
|
+
'general',
|
|
159
|
+
])
|
|
160
|
+
expect(groupFieldsBySection(fields, camelLayout, { kind: 'business' }).map(g => g.key)).toEqual([
|
|
161
|
+
'general',
|
|
162
|
+
'company',
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('without values (or without a section predicate) behaves exactly like before', () => {
|
|
167
|
+
// No values passed → a predicated section reads its gate field as '' and
|
|
168
|
+
// stays hidden (equals miss), while an ungated layout is untouched.
|
|
169
|
+
const plain = groupFieldsBySection(fields, {
|
|
170
|
+
mode: 'sections',
|
|
171
|
+
sections: [{ key: 'general' }, { key: 'company' }],
|
|
172
|
+
})
|
|
173
|
+
expect(plain.map(g => g.key)).toEqual(['general', 'company'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('drops a wizard step whose section is gated off, preserving navigable order', () => {
|
|
177
|
+
const stepsGated: FormLayout = {
|
|
178
|
+
mode: 'steps',
|
|
179
|
+
sections: [
|
|
180
|
+
{ key: 'who', title: 'Cliente' },
|
|
181
|
+
{
|
|
182
|
+
key: 'company',
|
|
183
|
+
title: 'Empresa',
|
|
184
|
+
visible_when: { field: 'kind', equals: 'business' },
|
|
185
|
+
},
|
|
186
|
+
{ key: 'pay', title: 'Pago' },
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
const stepFields: F[] = [
|
|
190
|
+
{ key: 'customer_id', section: 'who' },
|
|
191
|
+
{ key: 'tax_id', section: 'company' },
|
|
192
|
+
{ key: 'amount', section: 'pay' },
|
|
193
|
+
]
|
|
194
|
+
// person → company step removed: wizard is who → pay, submit still on last.
|
|
195
|
+
const hidden = groupFieldsBySection(stepFields, stepsGated, { kind: 'person' })
|
|
196
|
+
expect(hidden.map(g => g.key)).toEqual(['who', 'pay'])
|
|
197
|
+
// business → all three steps in order.
|
|
198
|
+
const shown = groupFieldsBySection(stepFields, stepsGated, { kind: 'business' })
|
|
199
|
+
expect(shown.map(g => g.key)).toEqual(['who', 'company', 'pay'])
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -884,7 +884,7 @@ export function DynamicRecordDialog({
|
|
|
884
884
|
// their section. Empty sections drop out for free. Steps mode only drives a
|
|
885
885
|
// wizard in editable modes — view mode always stacks the sections.
|
|
886
886
|
const formLayout = modalMeta?.form_layout ?? modalMeta?.formLayout
|
|
887
|
-
const groups = groupFieldsBySection(visibleFields, formLayout)
|
|
887
|
+
const groups = groupFieldsBySection(visibleFields, formLayout, formValues)
|
|
888
888
|
const isSteps = isEditable && formLayout?.mode === 'steps' && groups.length > 1
|
|
889
889
|
const clampedStep = Math.min(stepIndex, Math.max(groups.length - 1, 0))
|
|
890
890
|
const isLastStep = clampedStep === groups.length - 1
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -113,8 +113,8 @@ export function DynamicForm({
|
|
|
113
113
|
// this is a single default group → the render below collapses to the legacy
|
|
114
114
|
// flat grid, byte-for-byte.
|
|
115
115
|
const groups = useMemo(
|
|
116
|
-
() => groupFieldsBySection(visibleFields, formLayout),
|
|
117
|
-
[visibleFields, formLayout],
|
|
116
|
+
() => groupFieldsBySection(visibleFields, formLayout, values),
|
|
117
|
+
[visibleFields, formLayout, values],
|
|
118
118
|
)
|
|
119
119
|
const isSteps = formLayout?.mode === 'steps' && groups.length > 1
|
|
120
120
|
|
package/src/form-layout.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// implementation and the modal-render machinery never has to be booted under
|
|
15
15
|
// happy-dom just to assert the grouping (same approach as PR #673).
|
|
16
16
|
|
|
17
|
+
import type { VisibleWhen } from './types'
|
|
18
|
+
import { evaluateVisibleWhen, getVisibleWhen } from './dynamic-form-schema'
|
|
19
|
+
|
|
17
20
|
/** One declared section of a model form. `title`/`description` arrive already
|
|
18
21
|
* localized from the kernel and are rendered verbatim. */
|
|
19
22
|
export interface FormSection {
|
|
@@ -22,6 +25,12 @@ export interface FormSection {
|
|
|
22
25
|
description?: string
|
|
23
26
|
/** Sections mode only: render this section collapsed on first paint. */
|
|
24
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
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
/** Model-level layout directive. `mode` defaults to `"sections"`. */
|
|
@@ -71,16 +80,35 @@ export function getFieldSection(
|
|
|
71
80
|
* section whose only members are hidden by `visible_when` never renders an
|
|
72
81
|
* empty shell (and never yields an empty wizard step). Because the caller
|
|
73
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.
|
|
74
90
|
*/
|
|
75
91
|
export function groupFieldsBySection<F extends { section?: string }>(
|
|
76
92
|
fields: F[],
|
|
77
93
|
layout: FormLayout | undefined,
|
|
94
|
+
values?: Record<string, any> | null,
|
|
78
95
|
): FieldGroup<F>[] {
|
|
79
96
|
if (!layout?.sections?.length) {
|
|
80
97
|
return [{ key: DEFAULT_SECTION_KEY, isDefault: true, fields }]
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
|
|
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))
|
|
84
112
|
const bySection = new Map<string, F[]>()
|
|
85
113
|
const orphans: F[] = []
|
|
86
114
|
|
|
@@ -90,6 +118,9 @@ export function groupFieldsBySection<F extends { section?: string }>(
|
|
|
90
118
|
const arr = bySection.get(sec)
|
|
91
119
|
if (arr) arr.push(f)
|
|
92
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
|
|
93
124
|
} else {
|
|
94
125
|
orphans.push(f)
|
|
95
126
|
}
|
|
@@ -100,7 +131,7 @@ export function groupFieldsBySection<F extends { section?: string }>(
|
|
|
100
131
|
if (orphans.length) {
|
|
101
132
|
groups.push({ key: DEFAULT_SECTION_KEY, isDefault: true, fields: orphans })
|
|
102
133
|
}
|
|
103
|
-
for (const s of
|
|
134
|
+
for (const s of visibleSections) {
|
|
104
135
|
const secFields = bySection.get(s.key)
|
|
105
136
|
if (!secFields || secFields.length === 0) continue // hide empty section
|
|
106
137
|
groups.push({
|