@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/dist/types.d.ts
CHANGED
|
@@ -282,6 +282,12 @@ export interface ColumnDefinition {
|
|
|
282
282
|
visible_when?: VisibleWhen;
|
|
283
283
|
/** camelCase alias for `visible_when`. */
|
|
284
284
|
visibleWhen?: VisibleWhen;
|
|
285
|
+
/**
|
|
286
|
+
* Form-layout membership: the key of the `form_layout` section this column's
|
|
287
|
+
* modal field belongs to (kernel PR #230). Absent → the default group. See
|
|
288
|
+
* `groupFieldsBySection`.
|
|
289
|
+
*/
|
|
290
|
+
section?: string;
|
|
285
291
|
}
|
|
286
292
|
/**
|
|
287
293
|
* One declared sub-field of a jsonb line-items column (see
|
|
@@ -477,6 +483,12 @@ export interface ActionFieldDef {
|
|
|
477
483
|
visible_when?: VisibleWhen;
|
|
478
484
|
/** camelCase alias for `visible_when`. */
|
|
479
485
|
visibleWhen?: VisibleWhen;
|
|
486
|
+
/**
|
|
487
|
+
* Form-layout membership: the key of the `form_layout` section this field
|
|
488
|
+
* belongs to (kernel PR #230). Absent → the field lands in the default
|
|
489
|
+
* group. See `groupFieldsBySection`.
|
|
490
|
+
*/
|
|
491
|
+
section?: string;
|
|
480
492
|
}
|
|
481
493
|
/**
|
|
482
494
|
* Declarative reconciliation constraint on a line-items field: the summed value
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// DynamicRecordDialog payload contract: a field hidden by its `visible_when`
|
|
2
|
+
// predicate must NOT be POSTed. A DiscountRule with rule_scope=category shows
|
|
3
|
+
// only category_id; the product_id / customer_id it isn't showing must be
|
|
4
|
+
// stripped from the create payload. `stripHiddenFieldValues` is the pure helper
|
|
5
|
+
// the dialog runs just before `normalizeRefFieldsForSubmit`, mirroring
|
|
6
|
+
// dynamic-form.tsx, which builds its Zod schema only over the visible fields.
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
8
|
+
import { stripHiddenFieldValues } from '../dialogs/dynamic-record'
|
|
9
|
+
|
|
10
|
+
// Modal fields for a DiscountRule: a `rule_scope` discriminator gates the
|
|
11
|
+
// product / customer / category reference fields via visible_when.
|
|
12
|
+
const fields = [
|
|
13
|
+
{ key: 'name', label: 'Nombre', type: 'text', required: true },
|
|
14
|
+
{ key: 'rule_scope', label: 'Alcance', type: 'text', required: true },
|
|
15
|
+
{
|
|
16
|
+
key: 'product_id',
|
|
17
|
+
label: 'Producto',
|
|
18
|
+
type: 'text',
|
|
19
|
+
visible_when: { field: 'rule_scope', equals: 'product' },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'customer_id',
|
|
23
|
+
label: 'Cliente',
|
|
24
|
+
type: 'text',
|
|
25
|
+
visible_when: { field: 'rule_scope', equals: 'customer' },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'category_id',
|
|
29
|
+
label: 'Categoría',
|
|
30
|
+
type: 'text',
|
|
31
|
+
visible_when: { field: 'rule_scope', equals: 'category' },
|
|
32
|
+
},
|
|
33
|
+
] as any
|
|
34
|
+
|
|
35
|
+
describe('stripHiddenFieldValues (visible_when submit parity)', () => {
|
|
36
|
+
it('excludes fields hidden by visible_when from the create payload', () => {
|
|
37
|
+
// Scope=category → only category_id visible; product_id / customer_id are
|
|
38
|
+
// hidden and must be dropped even though they carry a value.
|
|
39
|
+
const out = stripHiddenFieldValues(
|
|
40
|
+
{
|
|
41
|
+
name: 'Promo',
|
|
42
|
+
rule_scope: 'category',
|
|
43
|
+
category_id: 'cat-1',
|
|
44
|
+
product_id: 'prod-1',
|
|
45
|
+
customer_id: 'cust-1',
|
|
46
|
+
},
|
|
47
|
+
fields,
|
|
48
|
+
'create',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(out.category_id).toBe('cat-1')
|
|
52
|
+
expect(out.rule_scope).toBe('category')
|
|
53
|
+
expect(out.name).toBe('Promo')
|
|
54
|
+
expect('product_id' in out).toBe(false)
|
|
55
|
+
expect('customer_id' in out).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('keeps the matching branch field for a different scope', () => {
|
|
59
|
+
const out = stripHiddenFieldValues(
|
|
60
|
+
{ rule_scope: 'product', product_id: 'prod-1', category_id: 'cat-1' },
|
|
61
|
+
fields,
|
|
62
|
+
'create',
|
|
63
|
+
)
|
|
64
|
+
expect(out.product_id).toBe('prod-1')
|
|
65
|
+
expect('category_id' in out).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('never drops fields that declare no visible_when', () => {
|
|
69
|
+
const out = stripHiddenFieldValues(
|
|
70
|
+
{ name: '', rule_scope: '', extra: 'x' },
|
|
71
|
+
fields,
|
|
72
|
+
'create',
|
|
73
|
+
)
|
|
74
|
+
// name has no visible_when → kept; extra has no declared field → kept.
|
|
75
|
+
expect('name' in out).toBe(true)
|
|
76
|
+
expect(out.extra).toBe('x')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Declarative form_layout grouping contract (kernel PR #230). The two renderers
|
|
2
|
+
// (dynamic-form.tsx, dialogs/dynamic-record.tsx) both drive their sections /
|
|
3
|
+
// wizard steps off `groupFieldsBySection`, so — following PR #673 — we test the
|
|
4
|
+
// pure grouping helper directly instead of booting the full modal under
|
|
5
|
+
// happy-dom (which hangs on the async metadata/options fetches).
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
groupFieldsBySection,
|
|
9
|
+
DEFAULT_SECTION_KEY,
|
|
10
|
+
type FormLayout,
|
|
11
|
+
} from '../form-layout'
|
|
12
|
+
|
|
13
|
+
interface F {
|
|
14
|
+
key: string
|
|
15
|
+
section?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const layout: FormLayout = {
|
|
19
|
+
mode: 'sections',
|
|
20
|
+
sections: [
|
|
21
|
+
{ key: 'general', title: 'General' },
|
|
22
|
+
{ key: 'billing', title: 'Facturación', collapsed: true },
|
|
23
|
+
{ key: 'notes', title: 'Notas' }, // will end up empty → hidden
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('groupFieldsBySection — sections', () => {
|
|
28
|
+
it('groups visible fields by section respecting the sections order', () => {
|
|
29
|
+
const fields: F[] = [
|
|
30
|
+
{ key: 'tax_id', section: 'billing' },
|
|
31
|
+
{ key: 'name', section: 'general' },
|
|
32
|
+
{ key: 'email', section: 'general' },
|
|
33
|
+
{ key: 'currency', section: 'billing' },
|
|
34
|
+
]
|
|
35
|
+
const groups = groupFieldsBySection(fields, layout)
|
|
36
|
+
|
|
37
|
+
// No orphans → no default group; sections in authored order; the empty
|
|
38
|
+
// `notes` section is omitted entirely.
|
|
39
|
+
expect(groups.map(g => g.key)).toEqual(['general', 'billing'])
|
|
40
|
+
expect(groups[0].fields.map(f => f.key)).toEqual(['name', 'email'])
|
|
41
|
+
expect(groups[1].fields.map(f => f.key)).toEqual(['tax_id', 'currency'])
|
|
42
|
+
// `collapsed` is carried through so the chrome can start collapsed.
|
|
43
|
+
expect(groups[1].collapsed).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('puts section-less and unknown-section fields in a default group placed first', () => {
|
|
47
|
+
const fields: F[] = [
|
|
48
|
+
{ key: 'name', section: 'general' },
|
|
49
|
+
{ key: 'orphan' }, // no section
|
|
50
|
+
{ key: 'stray', section: 'does_not_exist' }, // unknown section
|
|
51
|
+
]
|
|
52
|
+
const groups = groupFieldsBySection(fields, layout)
|
|
53
|
+
|
|
54
|
+
expect(groups[0].key).toBe(DEFAULT_SECTION_KEY)
|
|
55
|
+
expect(groups[0].isDefault).toBe(true)
|
|
56
|
+
expect(groups[0].fields.map(f => f.key)).toEqual(['orphan', 'stray'])
|
|
57
|
+
expect(groups[1].key).toBe('general')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('hides a section whose only members are filtered out (visible_when)', () => {
|
|
61
|
+
// The caller passes the ALREADY visibility-filtered list, so a section
|
|
62
|
+
// left with no fields must not render an empty shell / empty step.
|
|
63
|
+
const fields: F[] = [{ key: 'name', section: 'general' }]
|
|
64
|
+
const groups = groupFieldsBySection(fields, layout)
|
|
65
|
+
expect(groups.map(g => g.key)).toEqual(['general'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('without a layout returns a single default group carrying every field in order', () => {
|
|
69
|
+
const fields: F[] = [{ key: 'a' }, { key: 'b' }, { key: 'c', section: 'general' }]
|
|
70
|
+
const groups = groupFieldsBySection(fields, undefined)
|
|
71
|
+
expect(groups).toHaveLength(1)
|
|
72
|
+
expect(groups[0].key).toBe(DEFAULT_SECTION_KEY)
|
|
73
|
+
expect(groups[0].isDefault).toBe(true)
|
|
74
|
+
expect(groups[0].fields.map(f => f.key)).toEqual(['a', 'b', 'c'])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('groupFieldsBySection — steps', () => {
|
|
79
|
+
const stepsLayout: FormLayout = {
|
|
80
|
+
mode: 'steps',
|
|
81
|
+
sections: [
|
|
82
|
+
{ key: 'who', title: 'Cliente' },
|
|
83
|
+
{ key: 'what', title: 'Productos' },
|
|
84
|
+
{ key: 'pay', title: 'Pago' },
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it('yields one group per non-empty step in order, ready for wizard navigation', () => {
|
|
89
|
+
const fields: F[] = [
|
|
90
|
+
{ key: 'amount', section: 'pay' },
|
|
91
|
+
{ key: 'customer_id', section: 'who' },
|
|
92
|
+
{ key: 'items', section: 'what' },
|
|
93
|
+
]
|
|
94
|
+
const groups = groupFieldsBySection(fields, stepsLayout)
|
|
95
|
+
|
|
96
|
+
// Ordered as authored → step 0 = who, last = pay, so a wizard advances
|
|
97
|
+
// who → what → pay and submits on `pay`.
|
|
98
|
+
expect(groups.map(g => g.key)).toEqual(['who', 'what', 'pay'])
|
|
99
|
+
const last = groups.length - 1
|
|
100
|
+
expect(groups[last].key).toBe('pay')
|
|
101
|
+
expect(groups[last].title).toBe('Pago')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('drops an empty step so the wizard never stops on a blank page', () => {
|
|
105
|
+
const fields: F[] = [
|
|
106
|
+
{ key: 'customer_id', section: 'who' },
|
|
107
|
+
{ key: 'amount', section: 'pay' },
|
|
108
|
+
]
|
|
109
|
+
const groups = groupFieldsBySection(fields, stepsLayout)
|
|
110
|
+
// `what` has no visible field → omitted; wizard is who → pay.
|
|
111
|
+
expect(groups.map(g => g.key)).toEqual(['who', 'pay'])
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -54,6 +54,8 @@ import { DynamicRelations } from '../dynamic-relations'
|
|
|
54
54
|
import { useOptionsResolver, type ResolvedOption } from '../use-options-resolver'
|
|
55
55
|
import { getFieldRef, getVisibleWhen, evaluateVisibleWhen } from '../dynamic-form-schema'
|
|
56
56
|
import type { VisibleWhen } from '../types'
|
|
57
|
+
import { groupFieldsBySection, type FormLayout } from '../form-layout'
|
|
58
|
+
import { FieldSection, WizardProgress } from '../form-layout-ui'
|
|
57
59
|
import { FieldCell } from '../field-grid'
|
|
58
60
|
import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
|
|
59
61
|
import { normalizeRefFieldsForSubmit } from './normalize-submit'
|
|
@@ -158,6 +160,12 @@ export interface FieldDef {
|
|
|
158
160
|
visible_when?: VisibleWhen
|
|
159
161
|
/** camelCase alias for `visible_when`. */
|
|
160
162
|
visibleWhen?: VisibleWhen
|
|
163
|
+
/**
|
|
164
|
+
* Form-layout membership: the key of the `form_layout` section this field
|
|
165
|
+
* belongs to (kernel PR #230). Absent → the default group. See
|
|
166
|
+
* `groupFieldsBySection`.
|
|
167
|
+
*/
|
|
168
|
+
section?: string
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
// Permissive shape: the wire payload may omit some fields (e.g. `title` is
|
|
@@ -175,6 +183,15 @@ interface ModalMetadata {
|
|
|
175
183
|
createTitle?: string
|
|
176
184
|
editTitle?: string
|
|
177
185
|
fields?: FieldDef[]
|
|
186
|
+
/**
|
|
187
|
+
* Declarative form layout (kernel PR #230): groups the fields into named
|
|
188
|
+
* sections rendered stacked (`mode:"sections"`) or as a wizard
|
|
189
|
+
* (`mode:"steps"`). Absent → the legacy flat two-column grid. Tolerates the
|
|
190
|
+
* camelCase alias an app might author.
|
|
191
|
+
*/
|
|
192
|
+
form_layout?: FormLayout
|
|
193
|
+
/** camelCase alias for `form_layout`. */
|
|
194
|
+
formLayout?: FormLayout
|
|
178
195
|
/**
|
|
179
196
|
* Backend-localized CRUD success messages (modal metadata). Preferred over
|
|
180
197
|
* the raw response message which is not localized.
|
|
@@ -490,6 +507,29 @@ export function filterVisibleFields(
|
|
|
490
507
|
})
|
|
491
508
|
}
|
|
492
509
|
|
|
510
|
+
// stripHiddenFieldValues drops from a flat form-values object the keys of any
|
|
511
|
+
// declared field currently hidden by its `visible_when` predicate, so the
|
|
512
|
+
// submit never POSTs a value the form isn't showing (e.g. a DiscountRule with
|
|
513
|
+
// rule_scope=category must not send the product_id / customer_id it hides).
|
|
514
|
+
// This mirrors dynamic-form.tsx, which builds its Zod schema only over the
|
|
515
|
+
// visible fields — hidden fields are dropped from BOTH the render/required-gate
|
|
516
|
+
// AND the submitted values. Keys with no matching declared field, or whose
|
|
517
|
+
// field carries no `visible_when`, always pass through (retrocompat).
|
|
518
|
+
export function stripHiddenFieldValues(
|
|
519
|
+
values: Record<string, any>,
|
|
520
|
+
fields: FieldDef[] | undefined,
|
|
521
|
+
mode: 'view' | 'edit' | 'create',
|
|
522
|
+
): Record<string, any> {
|
|
523
|
+
const visibleKeys = new Set(filterVisibleFields(fields, mode, values).map(f => f.key))
|
|
524
|
+
const out: Record<string, any> = {}
|
|
525
|
+
for (const [key, value] of Object.entries(values)) {
|
|
526
|
+
const field = (fields ?? []).find(f => f.key === key)
|
|
527
|
+
if (field && !visibleKeys.has(key) && getVisibleWhen(field)) continue
|
|
528
|
+
out[key] = value
|
|
529
|
+
}
|
|
530
|
+
return out
|
|
531
|
+
}
|
|
532
|
+
|
|
493
533
|
export function DynamicRecordDialog({
|
|
494
534
|
open,
|
|
495
535
|
onOpenChange,
|
|
@@ -526,6 +566,8 @@ export function DynamicRecordDialog({
|
|
|
526
566
|
const [loading, setLoading] = useState(false)
|
|
527
567
|
const [saving, setSaving] = useState(false)
|
|
528
568
|
const [deleting, setDeleting] = useState(false)
|
|
569
|
+
// Wizard step cursor (form_layout mode:"steps" only).
|
|
570
|
+
const [stepIndex, setStepIndex] = useState(0)
|
|
529
571
|
|
|
530
572
|
const isCreate = mode === 'create'
|
|
531
573
|
const isView = mode === 'view'
|
|
@@ -537,8 +579,10 @@ export function DynamicRecordDialog({
|
|
|
537
579
|
if (!open) return
|
|
538
580
|
if (!isCreate && !recordId) return
|
|
539
581
|
|
|
540
|
-
// Fresh open → drop any validation errors from a prior submit
|
|
582
|
+
// Fresh open → drop any validation errors from a prior submit and reset
|
|
583
|
+
// the wizard to its first step.
|
|
541
584
|
setFieldErrors({})
|
|
585
|
+
setStepIndex(0)
|
|
542
586
|
|
|
543
587
|
let cancelled = false
|
|
544
588
|
|
|
@@ -752,9 +796,16 @@ export function DynamicRecordDialog({
|
|
|
752
796
|
// Required check passed → clear any prior validation errors.
|
|
753
797
|
setFieldErrors({})
|
|
754
798
|
|
|
799
|
+
// Fields hidden by their `visible_when` predicate must not be POSTed:
|
|
800
|
+
// the render, the required-gate and the payload all drive off the same
|
|
801
|
+
// filter, so a DiscountRule with scope=category never submits the
|
|
802
|
+
// product_id / customer_id it isn't showing. Mirrors dynamic-form.tsx,
|
|
803
|
+
// which builds its Zod only over visibleFields.
|
|
804
|
+
const submittedValues = stripHiddenFieldValues(formValues, modalMeta.fields, mode)
|
|
805
|
+
|
|
755
806
|
// Empty reference pickers → null (not "" / nil-UUID) so nullable FK
|
|
756
807
|
// columns accept them instead of raising a 23503 FK violation.
|
|
757
|
-
const payload = normalizeRefFieldsForSubmit(
|
|
808
|
+
const payload = normalizeRefFieldsForSubmit(submittedValues, modalMeta.fields)
|
|
758
809
|
|
|
759
810
|
setSaving(true)
|
|
760
811
|
try {
|
|
@@ -829,6 +880,65 @@ export function DynamicRecordDialog({
|
|
|
829
880
|
|
|
830
881
|
const visibleFields = filterVisibleFields(modalMeta?.fields, mode, formValues)
|
|
831
882
|
|
|
883
|
+
// Declarative form layout: group the (already visibility-filtered) fields by
|
|
884
|
+
// their section. Empty sections drop out for free. Steps mode only drives a
|
|
885
|
+
// wizard in editable modes — view mode always stacks the sections.
|
|
886
|
+
const formLayout = modalMeta?.form_layout ?? modalMeta?.formLayout
|
|
887
|
+
const groups = groupFieldsBySection(visibleFields, formLayout)
|
|
888
|
+
const isSteps = isEditable && formLayout?.mode === 'steps' && groups.length > 1
|
|
889
|
+
const clampedStep = Math.min(stepIndex, Math.max(groups.length - 1, 0))
|
|
890
|
+
const isLastStep = clampedStep === groups.length - 1
|
|
891
|
+
|
|
892
|
+
// Renders a list of fields into the shared two-column grid (each FieldCell
|
|
893
|
+
// gives min-w-0 so long values can't blow the columns past the dialog).
|
|
894
|
+
const renderFields = (groupFields: FieldDef[]) =>
|
|
895
|
+
groupFields.map(field => {
|
|
896
|
+
const isFullWidth =
|
|
897
|
+
field.type === 'textarea' ||
|
|
898
|
+
field.widget === 'textarea' ||
|
|
899
|
+
field.widget === 'richtext'
|
|
900
|
+
return (
|
|
901
|
+
<FieldCell key={field.key} fullWidth={isFullWidth}>
|
|
902
|
+
<FieldRow
|
|
903
|
+
field={field}
|
|
904
|
+
record={record}
|
|
905
|
+
value={formValues[field.key] ?? ''}
|
|
906
|
+
mode={mode}
|
|
907
|
+
error={fieldErrors[field.key]}
|
|
908
|
+
onChange={val => {
|
|
909
|
+
setFormValues((prev: Record<string, any>) => ({ ...prev, [field.key]: val }))
|
|
910
|
+
setFieldErrors(prev => {
|
|
911
|
+
if (!prev[field.key]) return prev
|
|
912
|
+
const next = { ...prev }
|
|
913
|
+
delete next[field.key]
|
|
914
|
+
return next
|
|
915
|
+
})
|
|
916
|
+
}}
|
|
917
|
+
/>
|
|
918
|
+
</FieldCell>
|
|
919
|
+
)
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// Wizard "Siguiente": gate only the CURRENT step's required (visible) fields,
|
|
923
|
+
// then advance. Mirrors handleSubmit's required check but scoped to the step.
|
|
924
|
+
const goNextStep = () => {
|
|
925
|
+
const step = groups[clampedStep]
|
|
926
|
+
const missing: Record<string, string> = {}
|
|
927
|
+
for (const field of step?.fields ?? []) {
|
|
928
|
+
if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
|
|
929
|
+
missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (Object.keys(missing).length) {
|
|
933
|
+
setFieldErrors(missing)
|
|
934
|
+
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }))
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
setFieldErrors({})
|
|
938
|
+
setStepIndex(Math.min(clampedStep + 1, groups.length - 1))
|
|
939
|
+
}
|
|
940
|
+
const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0))
|
|
941
|
+
|
|
832
942
|
return (
|
|
833
943
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
834
944
|
<DialogContent className="sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden">
|
|
@@ -845,54 +955,42 @@ export function DynamicRecordDialog({
|
|
|
845
955
|
<ImageUrlContext.Provider value={getImageUrl}>
|
|
846
956
|
<TimeZoneContext.Provider value={timeZone}>
|
|
847
957
|
<CurrencyContext.Provider value={currency}>
|
|
848
|
-
{/* The
|
|
849
|
-
|
|
958
|
+
{/* The form element groups its fields by the declared
|
|
959
|
+
form_layout (sections stacked, or the current
|
|
960
|
+
wizard step). Without a layout this is a single
|
|
961
|
+
default group rendered with no chrome — the legacy
|
|
962
|
+
two-column grid, unchanged. FieldCell gives each
|
|
850
963
|
cell `min-w-0` so a long select/input value can't
|
|
851
964
|
blow the two columns past the dialog width. */}
|
|
852
965
|
<form
|
|
853
966
|
id="dynamic-record-form"
|
|
854
967
|
onSubmit={handleSubmit}
|
|
855
|
-
className="grid
|
|
968
|
+
className="grid gap-y-4"
|
|
856
969
|
>
|
|
857
|
-
{
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
value={formValues[field.key] ?? ''}
|
|
868
|
-
mode={mode}
|
|
869
|
-
error={fieldErrors[field.key]}
|
|
870
|
-
onChange={val => {
|
|
871
|
-
setFormValues((prev: Record<string, any>) => ({ ...prev, [field.key]: val }))
|
|
872
|
-
// Clear this field's error as soon as the user edits it.
|
|
873
|
-
setFieldErrors(prev => {
|
|
874
|
-
if (!prev[field.key]) return prev
|
|
875
|
-
const next = { ...prev }
|
|
876
|
-
delete next[field.key]
|
|
877
|
-
return next
|
|
878
|
-
})
|
|
879
|
-
}}
|
|
880
|
-
/>
|
|
881
|
-
</FieldCell>
|
|
882
|
-
)
|
|
883
|
-
})}
|
|
970
|
+
{isSteps && (
|
|
971
|
+
<WizardProgress groups={groups} stepIndex={clampedStep} />
|
|
972
|
+
)}
|
|
973
|
+
{(isSteps ? [groups[clampedStep]] : groups).map(group => (
|
|
974
|
+
<FieldSection key={group.key} group={group}>
|
|
975
|
+
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
|
|
976
|
+
{renderFields(group.fields)}
|
|
977
|
+
</div>
|
|
978
|
+
</FieldSection>
|
|
979
|
+
))}
|
|
884
980
|
|
|
885
981
|
{record?.external_url && (
|
|
886
|
-
<div className="sm:
|
|
887
|
-
<
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
982
|
+
<div className="grid grid-cols-1 sm:grid-cols-2">
|
|
983
|
+
<div className="sm:col-span-2 min-w-0">
|
|
984
|
+
<a
|
|
985
|
+
href={record.external_url}
|
|
986
|
+
target="_blank"
|
|
987
|
+
rel="noreferrer"
|
|
988
|
+
className="inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1"
|
|
989
|
+
>
|
|
990
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
991
|
+
Ver en {record.external_provider ?? 'proveedor externo'}
|
|
992
|
+
</a>
|
|
993
|
+
</div>
|
|
896
994
|
</div>
|
|
897
995
|
)}
|
|
898
996
|
</form>
|
|
@@ -933,9 +1031,17 @@ export function DynamicRecordDialog({
|
|
|
933
1031
|
</Button>
|
|
934
1032
|
) : <span />}
|
|
935
1033
|
<div className="flex items-center gap-2">
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1034
|
+
{/* Wizard "Anterior" replaces Cancel from the second step
|
|
1035
|
+
on; the first step still shows Cancel. */}
|
|
1036
|
+
{isSteps && clampedStep > 0 ? (
|
|
1037
|
+
<Button variant="outline" onClick={goBackStep} disabled={saving || deleting}>
|
|
1038
|
+
Anterior
|
|
1039
|
+
</Button>
|
|
1040
|
+
) : (
|
|
1041
|
+
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving || deleting}>
|
|
1042
|
+
{config.cancelLabel}
|
|
1043
|
+
</Button>
|
|
1044
|
+
)}
|
|
939
1045
|
{isView && onDelete && (
|
|
940
1046
|
<Button
|
|
941
1047
|
variant="destructive"
|
|
@@ -951,7 +1057,15 @@ export function DynamicRecordDialog({
|
|
|
951
1057
|
Editar
|
|
952
1058
|
</Button>
|
|
953
1059
|
)}
|
|
954
|
-
{
|
|
1060
|
+
{/* Non-final wizard step → "Siguiente" validates the step
|
|
1061
|
+
and advances instead of submitting. The last step (and
|
|
1062
|
+
every non-wizard form) keeps the real submit button. */}
|
|
1063
|
+
{isEditable && isSteps && !isLastStep && (
|
|
1064
|
+
<Button type="button" onClick={goNextStep} disabled={saving || loading}>
|
|
1065
|
+
Siguiente
|
|
1066
|
+
</Button>
|
|
1067
|
+
)}
|
|
1068
|
+
{isEditable && (!isSteps || isLastStep) && (
|
|
955
1069
|
<Button
|
|
956
1070
|
type="submit"
|
|
957
1071
|
form="dynamic-record-form"
|