@omnitend/dashboard-for-laravel 0.16.1 → 0.16.3
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/dist/components/extended/DXField.vue.d.ts +4 -4
- package/dist/dashboard-for-laravel.js +5399 -5348
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +4 -4
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +20 -0
- package/docs/public/api-reference.json +4 -4
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +19 -0
- package/resources/js/components/extended/DXField.vue +83 -9
- package/resources/js/components/extended/DXRepeater.vue +58 -23
- package/resources/js/types/index.ts +22 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -75,6 +75,14 @@ export interface FieldDefinition {
|
|
|
75
75
|
max?: number | string;
|
|
76
76
|
/** Symbol shown for `currency` fields (default: the locale's, "£"). */
|
|
77
77
|
currencySymbol?: string;
|
|
78
|
+
/**
|
|
79
|
+
* For `currency` fields: minor-unit decimal places the *display* value is
|
|
80
|
+
* formatted to on blur and on initial seed (default: 2, e.g. GBP/EUR/USD —
|
|
81
|
+
* pass 0 for a currency with no minor unit, e.g. JPY). The model stays a
|
|
82
|
+
* plain number throughout; only the input's shown text is padded (`3.8` →
|
|
83
|
+
* `3.80`). Typing is never reformatted mid-edit.
|
|
84
|
+
*/
|
|
85
|
+
decimals?: number;
|
|
78
86
|
/**
|
|
79
87
|
* For `percentage` fields: treat the underlying model value as a 0–1
|
|
80
88
|
* fraction while showing/editing it as a 0–100 percentage. The model keeps
|
|
@@ -136,6 +144,18 @@ export interface FieldDefinition {
|
|
|
136
144
|
/** Minimum / maximum number of repeater rows. */
|
|
137
145
|
minItems?: number;
|
|
138
146
|
maxItems?: number;
|
|
147
|
+
/**
|
|
148
|
+
* For `repeater` fields backed by an upsert-children API contract (rows
|
|
149
|
+
* carrying a persisted `id` are only deleted server-side when submitted
|
|
150
|
+
* flagged, e.g. Laravel's `{ id, to_delete: true }` pattern): the
|
|
151
|
+
* sub-field key to set `true` when a row is removed, instead of splicing
|
|
152
|
+
* it out of the array. Only applies to rows that already have an `id`
|
|
153
|
+
* (freshly added, never-persisted rows are still spliced normally since
|
|
154
|
+
* the server has never seen them). Flagged rows remain in `form.data`
|
|
155
|
+
* (so they're submitted) but are hidden from the UI and excluded from
|
|
156
|
+
* `minItems`/`maxItems` counts.
|
|
157
|
+
*/
|
|
158
|
+
softDeleteKey?: string;
|
|
139
159
|
/** Disable the field (static or computed from the model). */
|
|
140
160
|
disabled?: MaybeFn<boolean>;
|
|
141
161
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-06T19:02:59.128Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.16.2"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -2146,7 +2146,7 @@
|
|
|
2146
2146
|
"name": "number"
|
|
2147
2147
|
},
|
|
2148
2148
|
"name": "index",
|
|
2149
|
-
"description": "The row's zero-based
|
|
2149
|
+
"description": "The row's zero-based position among visible rows."
|
|
2150
2150
|
},
|
|
2151
2151
|
{
|
|
2152
2152
|
"title": "binding",
|
|
@@ -2162,7 +2162,7 @@
|
|
|
2162
2162
|
"name": "function"
|
|
2163
2163
|
},
|
|
2164
2164
|
"name": "remove",
|
|
2165
|
-
"description": "Removes this row (respects `minItems`)."
|
|
2165
|
+
"description": "Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`)."
|
|
2166
2166
|
},
|
|
2167
2167
|
{
|
|
2168
2168
|
"title": "binding",
|
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2026-07-
|
|
4
|
+
> Last updated: 2026-07-06T19:02:59.193Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
package/package.json
CHANGED
package/resources/css/theme.scss
CHANGED
|
@@ -152,6 +152,25 @@ $dashboard-navbar-height: 64px;
|
|
|
152
152
|
|
|
153
153
|
@import "bootstrap-vue-next/dist/bootstrap-vue-next.css";
|
|
154
154
|
|
|
155
|
+
// ----------------------------------------------------------------------------
|
|
156
|
+
// Forms
|
|
157
|
+
// ----------------------------------------------------------------------------
|
|
158
|
+
// Bootstrap's compiled `.form-control`/`.form-select` height isn't exposed as
|
|
159
|
+
// a CSS custom property, so a component that needs to match input height
|
|
160
|
+
// exactly (e.g. DXField's `switch` box, which isn't itself a `.form-control`)
|
|
161
|
+
// would otherwise have to hardcode the same padding-y/line-height/border-width
|
|
162
|
+
// formula a second time — and silently drift from this theme if those
|
|
163
|
+
// variables ever change here. Compute it once, from the real theme variables,
|
|
164
|
+
// and expose it as a var so such components can reference the single source
|
|
165
|
+
// of truth instead of duplicating the formula.
|
|
166
|
+
:root {
|
|
167
|
+
// $input-border-width resolves to a CSS var() (Bootstrap 5.3+), which Sass
|
|
168
|
+
// can't multiply directly — fold the *2 into the calc() itself instead.
|
|
169
|
+
--dx-input-height: calc(
|
|
170
|
+
#{$input-line-height * 1em} + #{$input-padding-y * 2} + (2 * #{$input-border-width})
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
155
174
|
// ----------------------------------------------------------------------------
|
|
156
175
|
// Dashboard Custom Styles
|
|
157
176
|
// ----------------------------------------------------------------------------
|
|
@@ -247,12 +247,34 @@
|
|
|
247
247
|
<template v-if="field.type === 'currency'" #prepend>
|
|
248
248
|
<span class="input-group-text">{{ field.currencySymbol || "£" }}</span>
|
|
249
249
|
</template>
|
|
250
|
+
|
|
251
|
+
<!-- Currency: the displayed text is padded to the minor-unit
|
|
252
|
+
precision on blur/seed (e.g. `3.8` -> `3.80`); the model stays
|
|
253
|
+
a plain number and typing is never reformatted mid-edit. -->
|
|
254
|
+
<DFormInput
|
|
255
|
+
v-if="field.type === 'currency'"
|
|
256
|
+
:model-value="currencyDisplayValue"
|
|
257
|
+
type="number"
|
|
258
|
+
:required="field.required"
|
|
259
|
+
:placeholder="field.placeholder"
|
|
260
|
+
:step="field.step ?? '0.01'"
|
|
261
|
+
:min="field.min"
|
|
262
|
+
:max="field.max"
|
|
263
|
+
:state="fieldState"
|
|
264
|
+
:disabled="isDisabled"
|
|
265
|
+
:readonly="isReadonly"
|
|
266
|
+
v-bind="field.inputProps"
|
|
267
|
+
@input="handleCurrencyInput"
|
|
268
|
+
@focus="handleCurrencyFocus"
|
|
269
|
+
@blur="handleCurrencyBlur"
|
|
270
|
+
/>
|
|
250
271
|
<DFormInput
|
|
272
|
+
v-else
|
|
251
273
|
v-model="numericInputValue"
|
|
252
274
|
type="number"
|
|
253
275
|
:required="field.required"
|
|
254
276
|
:placeholder="field.placeholder"
|
|
255
|
-
:step="field.step
|
|
277
|
+
:step="field.step"
|
|
256
278
|
:min="field.min"
|
|
257
279
|
:max="field.max"
|
|
258
280
|
:state="fieldState"
|
|
@@ -260,6 +282,7 @@
|
|
|
260
282
|
:readonly="isReadonly"
|
|
261
283
|
v-bind="field.inputProps"
|
|
262
284
|
/>
|
|
285
|
+
|
|
263
286
|
<template v-if="field.type === 'percentage'" #append>
|
|
264
287
|
<span class="input-group-text">%</span>
|
|
265
288
|
</template>
|
|
@@ -474,9 +497,54 @@ function setValue(value: any): void {
|
|
|
474
497
|
} else {
|
|
475
498
|
(props.form.data as Record<string, any>)[props.field.key] = next;
|
|
476
499
|
}
|
|
500
|
+
props.form.touched[errorKey.value] = true;
|
|
477
501
|
props.form.clearError(errorKey.value);
|
|
478
502
|
}
|
|
479
503
|
|
|
504
|
+
// ————————————————— currency display formatting (#69)
|
|
505
|
+
|
|
506
|
+
// The model stays a plain number throughout; only the input's *displayed*
|
|
507
|
+
// text is padded to the minor-unit precision (`3.8` -> `3.80`), on blur and on
|
|
508
|
+
// initial seed. Reformatting reactively on every keystroke (rather than on
|
|
509
|
+
// blur) would fight the user mid-edit — e.g. rounding "3." to "3.00" before
|
|
510
|
+
// they've typed the fractional digits — so a local ref tracks the shown text
|
|
511
|
+
// independently of the numeric model, and is only resynced from the model
|
|
512
|
+
// while the input isn't focused.
|
|
513
|
+
const currencyDecimals = computed(() => props.field.decimals ?? 2);
|
|
514
|
+
const isCurrencyFocused = ref(false);
|
|
515
|
+
const currencyDisplayValue = ref("");
|
|
516
|
+
|
|
517
|
+
function formatCurrency(value: any): string {
|
|
518
|
+
if (value === null || value === undefined || value === "") return "";
|
|
519
|
+
const num = Number(value);
|
|
520
|
+
if (!Number.isFinite(num)) return String(value);
|
|
521
|
+
return num.toFixed(currencyDecimals.value);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
watch(
|
|
525
|
+
() => fieldValue.value,
|
|
526
|
+
(value) => {
|
|
527
|
+
if (isCurrencyFocused.value) return;
|
|
528
|
+
currencyDisplayValue.value = formatCurrency(value);
|
|
529
|
+
},
|
|
530
|
+
{ immediate: true },
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
function handleCurrencyFocus(): void {
|
|
534
|
+
isCurrencyFocused.value = true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function handleCurrencyInput(event: Event): void {
|
|
538
|
+
const raw = (event.target as HTMLInputElement).value;
|
|
539
|
+
currencyDisplayValue.value = raw;
|
|
540
|
+
setValue(raw);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function handleCurrencyBlur(): void {
|
|
544
|
+
isCurrencyFocused.value = false;
|
|
545
|
+
currencyDisplayValue.value = formatCurrency(fieldValue.value);
|
|
546
|
+
}
|
|
547
|
+
|
|
480
548
|
const fieldState = computed(() => props.form.getState(errorKey.value));
|
|
481
549
|
|
|
482
550
|
const resolvedLabel = computed(
|
|
@@ -622,17 +690,23 @@ onBeforeUnmount(() => {
|
|
|
622
690
|
border-radius: var(--bs-border-radius);
|
|
623
691
|
}
|
|
624
692
|
|
|
625
|
-
/* Switch field: a
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
693
|
+
/* Switch field: a compact "filled box" toggle, sized to its content — like the
|
|
694
|
+
plain `checkbox` field type, it doesn't stretch to fill the form row — but
|
|
695
|
+
matching standard input height so it lines up with sibling text/select
|
|
696
|
+
fields. Colour-codes its state: a neutral box when off, a filled success
|
|
697
|
+
green when on. The label sits on the left and the toggle on the right.
|
|
698
|
+
Renders the underlying `.form-check` (from the switch checkbox) as the box.
|
|
699
|
+
`justify-content: space-between` on a full-width box used to leave a wide
|
|
700
|
+
unclickable gap between the label and the toggle (only the tiny toggle
|
|
701
|
+
itself was a real click target); shrinking the box to fit its content
|
|
702
|
+
removes that dead zone entirely. */
|
|
629
703
|
.dx-switch :deep(.form-check) {
|
|
630
|
-
display: flex;
|
|
704
|
+
display: inline-flex;
|
|
631
705
|
align-items: center;
|
|
632
|
-
justify-content: space-between;
|
|
633
706
|
gap: 0.75rem;
|
|
634
|
-
width:
|
|
635
|
-
|
|
707
|
+
width: fit-content;
|
|
708
|
+
max-width: 100%; /* wrap/ellipsize rather than overflow a narrow field */
|
|
709
|
+
min-height: var(--dx-input-height, calc(1.5em + 0.75rem + 2px)); /* match .form-control height */
|
|
636
710
|
margin: 0;
|
|
637
711
|
/* Override Bootstrap `.form-switch`'s 2.5em left padding for the floated input. */
|
|
638
712
|
padding: 0.375rem 0.75rem;
|
|
@@ -7,37 +7,37 @@
|
|
|
7
7
|
<template>
|
|
8
8
|
<div class="dx-repeater">
|
|
9
9
|
<div
|
|
10
|
-
v-for="(
|
|
11
|
-
:key="rowKey(
|
|
10
|
+
v-for="(entry, position) in visibleRows"
|
|
11
|
+
:key="rowKey(entry.row, position)"
|
|
12
12
|
class="dx-repeater-row"
|
|
13
13
|
>
|
|
14
14
|
<!--
|
|
15
15
|
@slot Custom layout for a single repeater row, replacing the default sub-field stack.
|
|
16
16
|
@binding {object} row The current row's data object.
|
|
17
|
-
@binding {number} index The row's zero-based
|
|
17
|
+
@binding {number} index The row's zero-based position among visible rows.
|
|
18
18
|
@binding {FieldDefinition[]} fields The sub-field definitions for the row.
|
|
19
|
-
@binding {function} remove Removes this row (respects `minItems`).
|
|
19
|
+
@binding {function} remove Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`).
|
|
20
20
|
@binding {string} path The dot path into `form.data` for this row (e.g. `lines.0`).
|
|
21
21
|
-->
|
|
22
22
|
<slot
|
|
23
23
|
v-if="$slots.row"
|
|
24
24
|
name="row"
|
|
25
|
-
:row="row"
|
|
26
|
-
:index="
|
|
25
|
+
:row="entry.row"
|
|
26
|
+
:index="position"
|
|
27
27
|
:fields="subFields"
|
|
28
|
-
:remove="() => removeRow(index)"
|
|
29
|
-
:path="rowPath(index)"
|
|
28
|
+
:remove="() => removeRow(entry.index)"
|
|
29
|
+
:path="rowPath(entry.index)"
|
|
30
30
|
/>
|
|
31
31
|
|
|
32
32
|
<!-- Default row: stack each sub-field -->
|
|
33
33
|
<template v-else>
|
|
34
34
|
<div class="dx-repeater-row-header">
|
|
35
|
-
<span class="dx-repeater-row-index">{{
|
|
35
|
+
<span class="dx-repeater-row-index">{{ position + 1 }}</span>
|
|
36
36
|
<DButton
|
|
37
37
|
variant="outline-danger"
|
|
38
38
|
size="sm"
|
|
39
|
-
:disabled="
|
|
40
|
-
@click="removeRow(index)"
|
|
39
|
+
:disabled="visibleRows.length <= minItems"
|
|
40
|
+
@click="removeRow(entry.index)"
|
|
41
41
|
>
|
|
42
42
|
Remove
|
|
43
43
|
</DButton>
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
:key="subField.key"
|
|
48
48
|
:field="subField"
|
|
49
49
|
:form="form"
|
|
50
|
-
:model="row"
|
|
51
|
-
:key-path="`${rowPath(index)}.${subField.key}`"
|
|
52
|
-
:error-key="`${rowPath(index)}.${subField.key}`"
|
|
50
|
+
:model="entry.row"
|
|
51
|
+
:key-path="`${rowPath(entry.index)}.${subField.key}`"
|
|
52
|
+
:error-key="`${rowPath(entry.index)}.${subField.key}`"
|
|
53
53
|
/>
|
|
54
54
|
</template>
|
|
55
55
|
</div>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<DButton
|
|
58
58
|
variant="outline-primary"
|
|
59
59
|
size="sm"
|
|
60
|
-
:disabled="maxItems !== undefined &&
|
|
60
|
+
:disabled="maxItems !== undefined && visibleRows.length >= maxItems"
|
|
61
61
|
@click="addRow"
|
|
62
62
|
>
|
|
63
63
|
{{ field.addLabel || "Add" }}
|
|
@@ -97,10 +97,21 @@ const arrayPath = computed(() => props.keyPath ?? props.field.key);
|
|
|
97
97
|
const subFields = computed<FieldDefinition[]>(() => props.field.fields ?? []);
|
|
98
98
|
const minItems = computed(() => props.field.minItems ?? 0);
|
|
99
99
|
const maxItems = computed(() => props.field.maxItems);
|
|
100
|
+
const softDeleteKey = computed(() => props.field.softDeleteKey);
|
|
101
|
+
|
|
102
|
+
function isSoftDeleted(row: any): boolean {
|
|
103
|
+
return Boolean(
|
|
104
|
+
softDeleteKey.value &&
|
|
105
|
+
row &&
|
|
106
|
+
typeof row === "object" &&
|
|
107
|
+
row[softDeleteKey.value],
|
|
108
|
+
);
|
|
109
|
+
}
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
|
-
* The
|
|
103
|
-
* push/splice are reactive even when
|
|
112
|
+
* The full rows array, including any row flagged via `softDeleteKey`. Ensures
|
|
113
|
+
* form.data holds an array at the path so push/splice are reactive even when
|
|
114
|
+
* the form was seeded without one.
|
|
104
115
|
*/
|
|
105
116
|
const rows = computed<any[]>(() => {
|
|
106
117
|
const existing = getByPath(props.form.data, arrayPath.value);
|
|
@@ -108,6 +119,18 @@ const rows = computed<any[]>(() => {
|
|
|
108
119
|
return [];
|
|
109
120
|
});
|
|
110
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Rows shown in the UI, paired with their real (unfiltered) array index —
|
|
124
|
+
* a soft-deleted row stays in `form.data` (so it's submitted) but is hidden
|
|
125
|
+
* here, and every path/splice operation must still target its true index,
|
|
126
|
+
* not its position among visible rows.
|
|
127
|
+
*/
|
|
128
|
+
const visibleRows = computed<Array<{ row: any; index: number }>>(() =>
|
|
129
|
+
rows.value
|
|
130
|
+
.map((row, index) => ({ row, index }))
|
|
131
|
+
.filter(({ row }) => !isSoftDeleted(row)),
|
|
132
|
+
);
|
|
133
|
+
|
|
111
134
|
const rowPath = (index: number): string => `${arrayPath.value}.${index}`;
|
|
112
135
|
|
|
113
136
|
// Stable v-for keys tied to row object identity, so removing a middle row
|
|
@@ -118,8 +141,7 @@ const rowPath = (index: number): string => `${arrayPath.value}.${index}`;
|
|
|
118
141
|
const generatedKeys = new WeakMap<object, number>();
|
|
119
142
|
let nextGeneratedKey = 0;
|
|
120
143
|
|
|
121
|
-
const rowKey = (
|
|
122
|
-
const row = rows.value[index];
|
|
144
|
+
const rowKey = (row: any, fallbackPosition: number): string | number => {
|
|
123
145
|
if (row && typeof row === "object") {
|
|
124
146
|
if (row.id !== undefined && row.id !== null) return row.id;
|
|
125
147
|
let key = generatedKeys.get(row);
|
|
@@ -130,7 +152,7 @@ const rowKey = (index: number): string | number => {
|
|
|
130
152
|
}
|
|
131
153
|
return `gen-${key}`;
|
|
132
154
|
}
|
|
133
|
-
return
|
|
155
|
+
return fallbackPosition;
|
|
134
156
|
};
|
|
135
157
|
|
|
136
158
|
/** Default value for a freshly added row's sub-field. */
|
|
@@ -188,7 +210,7 @@ function clearArrayErrors(): void {
|
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
function addRow(): void {
|
|
191
|
-
if (maxItems.value !== undefined &&
|
|
213
|
+
if (maxItems.value !== undefined && visibleRows.value.length >= maxItems.value) {
|
|
192
214
|
return;
|
|
193
215
|
}
|
|
194
216
|
// Ensure the array exists on form.data before pushing (path may be
|
|
@@ -202,8 +224,21 @@ function addRow(): void {
|
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
function removeRow(index: number): void {
|
|
205
|
-
if (
|
|
206
|
-
getByPath(props.form.data, arrayPath.value)
|
|
227
|
+
if (visibleRows.value.length <= minItems.value) return;
|
|
228
|
+
const array = getByPath(props.form.data, arrayPath.value);
|
|
229
|
+
const row = array?.[index];
|
|
230
|
+
|
|
231
|
+
// A row the server already knows about (carries an `id`) can't just be
|
|
232
|
+
// spliced away under an upsert-children contract — the server never
|
|
233
|
+
// learns it was removed. Flag it instead so it's still submitted, and
|
|
234
|
+
// let it disappear from the UI via `visibleRows`.
|
|
235
|
+
if (softDeleteKey.value && row && typeof row === "object" && row.id !== undefined && row.id !== null) {
|
|
236
|
+
row[softDeleteKey.value] = true;
|
|
237
|
+
clearArrayErrors();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
array.splice(index, 1);
|
|
207
242
|
clearArrayErrors();
|
|
208
243
|
}
|
|
209
244
|
</script>
|
|
@@ -114,6 +114,15 @@ export interface FieldDefinition {
|
|
|
114
114
|
/** Symbol shown for `currency` fields (default: the locale's, "£"). */
|
|
115
115
|
currencySymbol?: string;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* For `currency` fields: minor-unit decimal places the *display* value is
|
|
119
|
+
* formatted to on blur and on initial seed (default: 2, e.g. GBP/EUR/USD —
|
|
120
|
+
* pass 0 for a currency with no minor unit, e.g. JPY). The model stays a
|
|
121
|
+
* plain number throughout; only the input's shown text is padded (`3.8` →
|
|
122
|
+
* `3.80`). Typing is never reformatted mid-edit.
|
|
123
|
+
*/
|
|
124
|
+
decimals?: number;
|
|
125
|
+
|
|
117
126
|
/**
|
|
118
127
|
* For `percentage` fields: treat the underlying model value as a 0–1
|
|
119
128
|
* fraction while showing/editing it as a 0–100 percentage. The model keeps
|
|
@@ -190,6 +199,19 @@ export interface FieldDefinition {
|
|
|
190
199
|
minItems?: number;
|
|
191
200
|
maxItems?: number;
|
|
192
201
|
|
|
202
|
+
/**
|
|
203
|
+
* For `repeater` fields backed by an upsert-children API contract (rows
|
|
204
|
+
* carrying a persisted `id` are only deleted server-side when submitted
|
|
205
|
+
* flagged, e.g. Laravel's `{ id, to_delete: true }` pattern): the
|
|
206
|
+
* sub-field key to set `true` when a row is removed, instead of splicing
|
|
207
|
+
* it out of the array. Only applies to rows that already have an `id`
|
|
208
|
+
* (freshly added, never-persisted rows are still spliced normally since
|
|
209
|
+
* the server has never seen them). Flagged rows remain in `form.data`
|
|
210
|
+
* (so they're submitted) but are hidden from the UI and excluded from
|
|
211
|
+
* `minItems`/`maxItems` counts.
|
|
212
|
+
*/
|
|
213
|
+
softDeleteKey?: string;
|
|
214
|
+
|
|
193
215
|
/** Disable the field (static or computed from the model). */
|
|
194
216
|
disabled?: MaybeFn<boolean>;
|
|
195
217
|
|