@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 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;
@@ -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).
@@ -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;CACtB;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;;;;;;;;;;;;;;;;GAgBG;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,GAC/B,UAAU,CAAC,CAAC,CAAC,EAAE,CAsCjB"}
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"}
@@ -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
- const known = new Set(layout.sections.map((s) => s.key));
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 layout.sections) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "28.5.0",
3
+ "version": "28.6.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
@@ -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
 
@@ -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
- const known = new Set(layout.sections.map((s) => s.key))
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 layout.sections) {
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({