@asteby/metacore-runtime-react 28.4.1 → 28.5.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 +10 -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 +54 -0
- package/dist/form-layout.d.ts.map +1 -0
- package/dist/form-layout.js +81 -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 +113 -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 +116 -0
- package/src/index.ts +2 -0
- package/src/types.ts +12 -0
package/src/dynamic-form.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// pattern + ActionFieldDef renderer so callers can reuse the form layout
|
|
3
3
|
// outside the full record-edit modal.
|
|
4
4
|
import { useEffect, useMemo, useState } from 'react'
|
|
5
|
+
import { groupFieldsBySection, type FormLayout } from './form-layout'
|
|
6
|
+
import { FieldSection, WizardProgress } from './form-layout-ui'
|
|
5
7
|
import {
|
|
6
8
|
Input,
|
|
7
9
|
Textarea,
|
|
@@ -47,6 +49,14 @@ export interface DynamicFormProps {
|
|
|
47
49
|
submitLabel?: string
|
|
48
50
|
cancelLabel?: string
|
|
49
51
|
disabled?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Declarative form layout served on the model metadata (kernel PR #230).
|
|
54
|
+
* When present the visible fields are grouped by their `section` respecting
|
|
55
|
+
* `sections` order: `mode:"sections"` stacks (optionally collapsible)
|
|
56
|
+
* sections; `mode:"steps"` renders a Anterior/Siguiente wizard. Absent → the
|
|
57
|
+
* legacy flat list (unchanged).
|
|
58
|
+
*/
|
|
59
|
+
formLayout?: FormLayout
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export function DynamicForm({
|
|
@@ -57,6 +67,7 @@ export function DynamicForm({
|
|
|
57
67
|
submitLabel = 'Guardar',
|
|
58
68
|
cancelLabel = 'Cancelar',
|
|
59
69
|
disabled = false,
|
|
70
|
+
formLayout,
|
|
60
71
|
}: DynamicFormProps) {
|
|
61
72
|
const [values, setValues] = useState<Record<string, any>>({})
|
|
62
73
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
@@ -97,6 +108,23 @@ export function DynamicForm({
|
|
|
97
108
|
return false
|
|
98
109
|
}, [visibleFields, values])
|
|
99
110
|
|
|
111
|
+
// Group visible fields by their form_layout section (empty sections drop out
|
|
112
|
+
// because visibleFields is already visibility-filtered). Without a layout
|
|
113
|
+
// this is a single default group → the render below collapses to the legacy
|
|
114
|
+
// flat grid, byte-for-byte.
|
|
115
|
+
const groups = useMemo(
|
|
116
|
+
() => groupFieldsBySection(visibleFields, formLayout),
|
|
117
|
+
[visibleFields, formLayout],
|
|
118
|
+
)
|
|
119
|
+
const isSteps = formLayout?.mode === 'steps' && groups.length > 1
|
|
120
|
+
|
|
121
|
+
// Wizard step cursor (steps mode only). Clamped whenever the group count
|
|
122
|
+
// shrinks (a visible_when flip can empty a trailing step's section).
|
|
123
|
+
const [stepIndex, setStepIndex] = useState(0)
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
setStepIndex((i) => Math.min(i, Math.max(groups.length - 1, 0)))
|
|
126
|
+
}, [groups.length])
|
|
127
|
+
|
|
100
128
|
useEffect(() => {
|
|
101
129
|
const defaults: Record<string, any> = {}
|
|
102
130
|
for (const f of editableFields) {
|
|
@@ -131,25 +159,93 @@ export function DynamicForm({
|
|
|
131
159
|
try { await onSubmit(result.data as Record<string, any>) } finally { setSubmitting(false) }
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
// Renders one group's fields into the responsive 2-column grid: scalar
|
|
163
|
+
// header fields flow through it; line-items grids (and textareas) span full
|
|
164
|
+
// width so the row table / memo gets room. Shared by every layout mode.
|
|
165
|
+
const renderGrid = (groupFields: ActionFieldDef[]) => (
|
|
166
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
167
|
+
{groupFields.map((field) => (
|
|
168
|
+
<FieldRow
|
|
169
|
+
key={field.key}
|
|
170
|
+
field={field}
|
|
171
|
+
value={values[field.key]}
|
|
172
|
+
onChange={(v: any) => update(field.key, v)}
|
|
173
|
+
values={values}
|
|
174
|
+
error={errors[field.key]}
|
|
175
|
+
initialValues={initialValues}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// ── Steps (wizard) mode ────────────────────────────────────────────────
|
|
182
|
+
// One step per section; Anterior/Siguiente navigate, submit only on the
|
|
183
|
+
// last step. Advancing validates just the current step's fields so a later
|
|
184
|
+
// step can't be blocked by an earlier untouched one and vice-versa.
|
|
185
|
+
if (isSteps) {
|
|
186
|
+
const step = groups[stepIndex]
|
|
187
|
+
const isLast = stepIndex === groups.length - 1
|
|
188
|
+
|
|
189
|
+
const goNext = () => {
|
|
190
|
+
const stepSchema = buildZodSchema(step.fields)
|
|
191
|
+
const result = stepSchema.safeParse(values)
|
|
192
|
+
if (!result.success) {
|
|
193
|
+
const next: Record<string, string> = {}
|
|
194
|
+
for (const issue of result.error.issues) {
|
|
195
|
+
const key = issue.path[0]
|
|
196
|
+
if (typeof key === 'string' && !next[key]) next[key] = issue.message
|
|
197
|
+
}
|
|
198
|
+
setErrors(next)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
setErrors({})
|
|
202
|
+
setStepIndex((i) => Math.min(i + 1, groups.length - 1))
|
|
203
|
+
}
|
|
204
|
+
const goBack = () => setStepIndex((i) => Math.max(i - 1, 0))
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<form onSubmit={handleSubmit} className="grid gap-4">
|
|
208
|
+
<WizardProgress groups={groups} stepIndex={stepIndex} />
|
|
209
|
+
{renderGrid(step.fields)}
|
|
210
|
+
<div className="flex justify-between gap-2 pt-2">
|
|
211
|
+
{stepIndex > 0 ? (
|
|
212
|
+
<Button type="button" variant="outline" onClick={goBack} disabled={submitting || disabled}>
|
|
213
|
+
Anterior
|
|
214
|
+
</Button>
|
|
215
|
+
) : onCancel ? (
|
|
216
|
+
<Button type="button" variant="outline" onClick={onCancel} disabled={submitting || disabled}>
|
|
217
|
+
{cancelLabel}
|
|
218
|
+
</Button>
|
|
219
|
+
) : (
|
|
220
|
+
<span />
|
|
221
|
+
)}
|
|
222
|
+
{isLast ? (
|
|
223
|
+
<Button type="submit" disabled={submitting || disabled || balanceBlocked}>
|
|
224
|
+
{submitLabel}
|
|
225
|
+
</Button>
|
|
226
|
+
) : (
|
|
227
|
+
<Button type="button" onClick={goNext} disabled={submitting || disabled}>
|
|
228
|
+
Siguiente
|
|
229
|
+
</Button>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</form>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Sections / flat mode ───────────────────────────────────────────────
|
|
134
237
|
// Layout: scalar header fields flow through a responsive 2-column grid;
|
|
135
238
|
// line-items grids (and textareas) span the full width so the row table /
|
|
136
239
|
// memo gets room. Mirrors the pro look of the federated journal modal but
|
|
137
|
-
// stays fully declarative — driven only by field shape.
|
|
240
|
+
// stays fully declarative — driven only by field shape. With no layout this
|
|
241
|
+
// is a single default group rendered without section chrome (unchanged).
|
|
138
242
|
return (
|
|
139
243
|
<form onSubmit={handleSubmit} className="grid gap-4">
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
value={values[field.key]}
|
|
146
|
-
onChange={(v: any) => update(field.key, v)}
|
|
147
|
-
values={values}
|
|
148
|
-
error={errors[field.key]}
|
|
149
|
-
initialValues={initialValues}
|
|
150
|
-
/>
|
|
151
|
-
))}
|
|
152
|
-
</div>
|
|
244
|
+
{groups.map((group) => (
|
|
245
|
+
<FieldSection key={group.key} group={group}>
|
|
246
|
+
{renderGrid(group.fields)}
|
|
247
|
+
</FieldSection>
|
|
248
|
+
))}
|
|
153
249
|
<div className="flex justify-end gap-2 pt-2">
|
|
154
250
|
{onCancel && (
|
|
155
251
|
<Button type="button" variant="outline" onClick={onCancel} disabled={submitting || disabled}>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Presentational chrome for declarative form layouts (see `form-layout.ts`).
|
|
2
|
+
// Kept apart from the grouping logic so the pure helper stays React-free and the
|
|
3
|
+
// two renderers (`dynamic-form.tsx`, `dialogs/dynamic-record.tsx`) share one
|
|
4
|
+
// look for sections and the wizard progress bar.
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
Collapsible,
|
|
8
|
+
CollapsibleContent,
|
|
9
|
+
CollapsibleTrigger,
|
|
10
|
+
Button,
|
|
11
|
+
} from '@asteby/metacore-ui/primitives'
|
|
12
|
+
import { ChevronDown } from 'lucide-react'
|
|
13
|
+
import type { FieldGroup } from './form-layout'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Section chrome for `mode: "sections"`. Wraps a group's (already gridded)
|
|
17
|
+
* fields in a titled block.
|
|
18
|
+
* - The default/orphan group renders with NO chrome, so a layout-less form (one
|
|
19
|
+
* default group) is visually identical to the legacy flat list.
|
|
20
|
+
* - A section whose `collapsed` is defined (true/false) is rendered COLLAPSIBLE,
|
|
21
|
+
* starting collapsed when `collapsed === true`. A section with no `collapsed`
|
|
22
|
+
* flag renders as a plain, always-open titled block.
|
|
23
|
+
*/
|
|
24
|
+
export function FieldSection({
|
|
25
|
+
group,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
group: FieldGroup<unknown>
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
}) {
|
|
31
|
+
const collapsible = group.collapsed !== undefined
|
|
32
|
+
const [open, setOpen] = useState(!group.collapsed)
|
|
33
|
+
|
|
34
|
+
if (group.isDefault) return <>{children}</>
|
|
35
|
+
|
|
36
|
+
const header = (
|
|
37
|
+
<div className="min-w-0 text-left">
|
|
38
|
+
{group.title && (
|
|
39
|
+
<h3 className="text-sm font-semibold leading-none">{group.title}</h3>
|
|
40
|
+
)}
|
|
41
|
+
{group.description && (
|
|
42
|
+
<p className="pt-1 text-sm text-muted-foreground">{group.description}</p>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (!collapsible) {
|
|
48
|
+
return (
|
|
49
|
+
<section className="grid gap-3">
|
|
50
|
+
{(group.title || group.description) && header}
|
|
51
|
+
{children}
|
|
52
|
+
</section>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Collapsible open={open} onOpenChange={setOpen} className="grid gap-3">
|
|
58
|
+
<CollapsibleTrigger asChild>
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
className="h-auto w-full justify-between px-0 py-1 hover:bg-transparent"
|
|
63
|
+
>
|
|
64
|
+
{header}
|
|
65
|
+
<ChevronDown
|
|
66
|
+
className={
|
|
67
|
+
'h-4 w-4 shrink-0 transition-transform ' + (open ? 'rotate-180' : '')
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
</Button>
|
|
71
|
+
</CollapsibleTrigger>
|
|
72
|
+
<CollapsibleContent className="grid gap-3">{children}</CollapsibleContent>
|
|
73
|
+
</Collapsible>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Progress bar for `mode: "steps"`: one filled segment per completed/current
|
|
79
|
+
* step plus a "Paso i/n · <title>" caption. Mirrors the WizardActionModal look
|
|
80
|
+
* so a model wizard and an action wizard read the same.
|
|
81
|
+
*/
|
|
82
|
+
export function WizardProgress({
|
|
83
|
+
groups,
|
|
84
|
+
stepIndex,
|
|
85
|
+
stepLabel = 'Paso',
|
|
86
|
+
}: {
|
|
87
|
+
groups: FieldGroup<unknown>[]
|
|
88
|
+
stepIndex: number
|
|
89
|
+
stepLabel?: string
|
|
90
|
+
}) {
|
|
91
|
+
const current = groups[stepIndex]
|
|
92
|
+
return (
|
|
93
|
+
<div className="pt-2">
|
|
94
|
+
<div className="flex items-center gap-1.5" role="list" aria-label="progress">
|
|
95
|
+
{groups.map((g, i) => (
|
|
96
|
+
<div
|
|
97
|
+
key={g.key}
|
|
98
|
+
role="listitem"
|
|
99
|
+
aria-current={i === stepIndex ? 'step' : undefined}
|
|
100
|
+
className="h-1.5 flex-1 rounded-full"
|
|
101
|
+
style={{
|
|
102
|
+
backgroundColor:
|
|
103
|
+
i <= stepIndex ? 'hsl(var(--primary))' : 'hsl(var(--muted))',
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
<p className="pt-2 text-sm text-muted-foreground">
|
|
109
|
+
{stepLabel} {stepIndex + 1}/{groups.length}
|
|
110
|
+
{current?.title ? ` · ${current.title}` : ''}
|
|
111
|
+
</p>
|
|
112
|
+
{current?.description && (
|
|
113
|
+
<p className="pt-1 text-sm text-muted-foreground">{current.description}</p>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
/** One declared section of a model form. `title`/`description` arrive already
|
|
18
|
+
* localized from the kernel and are rendered verbatim. */
|
|
19
|
+
export interface FormSection {
|
|
20
|
+
key: string
|
|
21
|
+
title?: string
|
|
22
|
+
description?: string
|
|
23
|
+
/** Sections mode only: render this section collapsed on first paint. */
|
|
24
|
+
collapsed?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Model-level layout directive. `mode` defaults to `"sections"`. */
|
|
28
|
+
export interface FormLayout {
|
|
29
|
+
mode?: 'sections' | 'steps'
|
|
30
|
+
sections?: FormSection[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A resolved group of fields ready to render: a section plus the (already
|
|
34
|
+
* visibility-filtered) fields that belong to it. */
|
|
35
|
+
export interface FieldGroup<F> {
|
|
36
|
+
/** Section key, or `__default__` for the orphan group. */
|
|
37
|
+
key: string
|
|
38
|
+
title?: string
|
|
39
|
+
description?: string
|
|
40
|
+
collapsed?: boolean
|
|
41
|
+
/** True for the synthetic group holding section-less / unknown-section fields. */
|
|
42
|
+
isDefault: boolean
|
|
43
|
+
fields: F[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The synthetic key of the orphan group (fields with no / unknown `section`). */
|
|
47
|
+
export const DEFAULT_SECTION_KEY = '__default__'
|
|
48
|
+
|
|
49
|
+
/** Reads a field's `section` reference, tolerating a camelCase alias. */
|
|
50
|
+
export function getFieldSection(
|
|
51
|
+
field: { section?: string; Section?: string } | null | undefined,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
if (!field) return undefined
|
|
54
|
+
const s = field.section ?? (field as { Section?: string }).Section
|
|
55
|
+
return typeof s === 'string' && s !== '' ? s : undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Groups already-filtered (visible) fields by their `section`, honoring the
|
|
60
|
+
* order of `layout.sections`.
|
|
61
|
+
*
|
|
62
|
+
* Contract:
|
|
63
|
+
* - No `layout` (or no `sections`) → a single default group carrying every
|
|
64
|
+
* field in its original order. Callers render this exactly like the legacy
|
|
65
|
+
* flat list, so the layout-less path is byte-for-byte the current behaviour.
|
|
66
|
+
* - Fields whose `section` is empty or references an UNKNOWN section key are
|
|
67
|
+
* collected into ONE default group placed FIRST (before any declared
|
|
68
|
+
* section) — the consistent, documented choice: general/uncategorized fields
|
|
69
|
+
* lead, declared sections follow in their authored order.
|
|
70
|
+
* - A declared section with zero visible fields is OMITTED entirely, so a
|
|
71
|
+
* section whose only members are hidden by `visible_when` never renders an
|
|
72
|
+
* empty shell (and never yields an empty wizard step). Because the caller
|
|
73
|
+
* passes the already visibility-filtered list, this falls out for free.
|
|
74
|
+
*/
|
|
75
|
+
export function groupFieldsBySection<F extends { section?: string }>(
|
|
76
|
+
fields: F[],
|
|
77
|
+
layout: FormLayout | undefined,
|
|
78
|
+
): FieldGroup<F>[] {
|
|
79
|
+
if (!layout?.sections?.length) {
|
|
80
|
+
return [{ key: DEFAULT_SECTION_KEY, isDefault: true, fields }]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const known = new Set(layout.sections.map((s) => s.key))
|
|
84
|
+
const bySection = new Map<string, F[]>()
|
|
85
|
+
const orphans: F[] = []
|
|
86
|
+
|
|
87
|
+
for (const f of fields) {
|
|
88
|
+
const sec = getFieldSection(f)
|
|
89
|
+
if (sec && known.has(sec)) {
|
|
90
|
+
const arr = bySection.get(sec)
|
|
91
|
+
if (arr) arr.push(f)
|
|
92
|
+
else bySection.set(sec, [f])
|
|
93
|
+
} else {
|
|
94
|
+
orphans.push(f)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const groups: FieldGroup<F>[] = []
|
|
99
|
+
// Orphan/default group leads (fields with no / unknown section).
|
|
100
|
+
if (orphans.length) {
|
|
101
|
+
groups.push({ key: DEFAULT_SECTION_KEY, isDefault: true, fields: orphans })
|
|
102
|
+
}
|
|
103
|
+
for (const s of layout.sections) {
|
|
104
|
+
const secFields = bySection.get(s.key)
|
|
105
|
+
if (!secFields || secFields.length === 0) continue // hide empty section
|
|
106
|
+
groups.push({
|
|
107
|
+
key: s.key,
|
|
108
|
+
title: s.title,
|
|
109
|
+
description: s.description,
|
|
110
|
+
collapsed: s.collapsed,
|
|
111
|
+
isDefault: false,
|
|
112
|
+
fields: secFields,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return groups
|
|
116
|
+
}
|
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
|
/**
|