@omnitend/dashboard-for-laravel 0.46.0 → 0.47.1
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 +28 -3
- package/dist/components/extended/DXFormErrorSummary.vue.d.ts +8 -0
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
- package/dist/composables/useForm.d.ts +22 -0
- package/dist/dashboard-for-laravel.js +9561 -9438
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +64 -0
- package/dist/utils/formErrorTargets.d.ts +26 -12
- package/dist/utils/formSchema.d.ts +16 -0
- package/docs/public/api-reference.json +127 -7
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +75 -0
- package/resources/js/components/extended/DXField.vue +115 -29
- package/resources/js/components/extended/DXForm.vue +17 -5
- package/resources/js/components/extended/DXFormErrorSummary.vue +12 -0
- package/resources/js/components/extended/DXFormField.vue +3 -1
- package/resources/js/components/extended/DXTable.vue +4 -1
- package/resources/js/components/extended/DXTableEditorModal.vue +35 -2
- package/resources/js/composables/useForm.ts +68 -0
- package/resources/js/composables/useResourceEditor.ts +64 -26
- package/resources/js/index.ts +3 -0
- package/resources/js/types/index.ts +68 -0
- package/resources/js/utils/formErrorTargets.ts +107 -15
- package/resources/js/utils/formSchema.ts +27 -0
|
@@ -274,6 +274,20 @@ export interface FieldDefinition {
|
|
|
274
274
|
*/
|
|
275
275
|
default?: any;
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* For a field whose value is a list of rows (a `repeater`, or a span
|
|
279
|
+
* field that renders its own rows and owns `key.N.*` error keys): the
|
|
280
|
+
* row's own name, called with the row (`model[key][index]`, or `{}` when
|
|
281
|
+
* the model has no such row) and its 0-based index. When it returns a
|
|
282
|
+
* non-empty string, the error summary lists that row's errors under it
|
|
283
|
+
* alone ("Chaucer Ale (Pint): Enter a price.") instead of
|
|
284
|
+
* "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
|
|
285
|
+
* (a row with no name yet), or throwing, keeps the line-number label.
|
|
286
|
+
* Only labels: which field owns the error, and what a click on the
|
|
287
|
+
* summary row focuses, are unchanged.
|
|
288
|
+
*/
|
|
289
|
+
rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
|
|
290
|
+
|
|
277
291
|
/** Label for a repeater's "add row" button (default: "Add"). */
|
|
278
292
|
addLabel?: string;
|
|
279
293
|
|
|
@@ -364,6 +378,10 @@ export interface FieldDefinition {
|
|
|
364
378
|
* every `editFields` key, so those decorative fields are POSTed alongside
|
|
365
379
|
* the real ones (an empty string, an empty array) purely because they were
|
|
366
380
|
* declared.
|
|
381
|
+
*
|
|
382
|
+
* Applies per KEY (#228): DXTable drops a key only when no field with that
|
|
383
|
+
* key is both submitting and visible, so a `submit: false` display twin
|
|
384
|
+
* does not strip the value of a visible editable field sharing its key.
|
|
367
385
|
*/
|
|
368
386
|
submit?: boolean;
|
|
369
387
|
|
|
@@ -440,6 +458,8 @@ export interface BaseFieldDef {
|
|
|
440
458
|
plaintext?: MaybeFn<boolean>;
|
|
441
459
|
submit?: boolean;
|
|
442
460
|
errorKeys?: string[];
|
|
461
|
+
/** On a repeater, or a span field that renders its own rows. */
|
|
462
|
+
rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
|
|
443
463
|
when?: MaybeFn<boolean>;
|
|
444
464
|
show?: () => boolean;
|
|
445
465
|
}
|
|
@@ -652,6 +672,54 @@ export interface FormTab {
|
|
|
652
672
|
lazy?: boolean;
|
|
653
673
|
}
|
|
654
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Slot props of DXField's `password-actions` slot (DXForm:
|
|
677
|
+
* `password-actions(<key>)`): content placed beside a password input, such as
|
|
678
|
+
* a "Generate password" button. It renders after the input group (the input
|
|
679
|
+
* and the reveal (eye) toggle), in `.dx-password-field__actions`, and wraps
|
|
680
|
+
* onto its own line below the input when the row is too narrow to keep
|
|
681
|
+
* `--dx-password-input-min-width`. `setRevealed` drives the same state the
|
|
682
|
+
* eye toggles.
|
|
683
|
+
*
|
|
684
|
+
* ```vue
|
|
685
|
+
* <template #password-actions(password)="{ update, setRevealed, disabled }">
|
|
686
|
+
* <DButton variant="secondary" :disabled="disabled"
|
|
687
|
+
* @click="update(generatePassword()); setRevealed(true)">Generate</DButton>
|
|
688
|
+
* </template>
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
691
|
+
export interface PasswordActionsSlotProps {
|
|
692
|
+
/** The field definition being rendered. */
|
|
693
|
+
field: FieldDefinition;
|
|
694
|
+
/** The model passed to field predicates (defaults to the live form data). */
|
|
695
|
+
model: any;
|
|
696
|
+
/** The current field value. */
|
|
697
|
+
value: any;
|
|
698
|
+
/** Write a new value (also marks the field touched and clears its error). */
|
|
699
|
+
update: (value: any) => void;
|
|
700
|
+
/** Whether the password is currently shown as plain text. */
|
|
701
|
+
revealed: boolean;
|
|
702
|
+
/**
|
|
703
|
+
* Show (`true`) or mask (`false`) the password, exactly as the eye does.
|
|
704
|
+
* A no-op on a `revealable: false` field, which never unmasks.
|
|
705
|
+
*/
|
|
706
|
+
setRevealed: (revealed: boolean) => void;
|
|
707
|
+
/** Whether the field is disabled or read-only. */
|
|
708
|
+
disabled: boolean;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Slot props of DXTable's `edit-password-actions(<key>)` slot, the edit/create
|
|
713
|
+
* modal's form of {@link PasswordActionsSlotProps}. `item` is the row being
|
|
714
|
+
* edited, `null` in create mode. (`model` is not passed, as with the modal's
|
|
715
|
+
* other field slots.)
|
|
716
|
+
*/
|
|
717
|
+
export interface EditPasswordActionsSlotProps
|
|
718
|
+
extends Omit<PasswordActionsSlotProps, "model"> {
|
|
719
|
+
/** The row being edited; `null` in create mode. */
|
|
720
|
+
item: any;
|
|
721
|
+
}
|
|
722
|
+
|
|
655
723
|
/**
|
|
656
724
|
* The theme's extra colour variant, `pending`: waiting, and the next move is
|
|
657
725
|
* not yours ("awaiting payment", "sent to printer, not yet confirmed").
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* `when`/`context`-aware predicates and a standalone summary can use the
|
|
8
8
|
* defaults below.
|
|
9
9
|
*/
|
|
10
|
+
import { toRaw } from "vue";
|
|
10
11
|
import type { FieldDefinition, FormTab } from "../types";
|
|
11
|
-
import type { ValidationErrors } from "../composables/useForm";
|
|
12
|
+
import type { FailedRow, ValidationErrors } from "../composables/useForm";
|
|
12
13
|
import {
|
|
13
14
|
isFieldVisible as isFieldVisibleFor,
|
|
14
15
|
resolvePredicate,
|
|
@@ -19,6 +20,14 @@ import { getByPath } from "./objectPath";
|
|
|
19
20
|
export interface ErrorTarget {
|
|
20
21
|
/** The key as the server sent it (`lines.0.price`). */
|
|
21
22
|
errorKey: string;
|
|
23
|
+
/**
|
|
24
|
+
* Where to take the user for this error now. The same as `errorKey`,
|
|
25
|
+
* except for a row error resolved with `failedRows`: the key at the
|
|
26
|
+
* failed row's CURRENT index (`lines.1.price` once row 0 is removed),
|
|
27
|
+
* or the owning field's key when that row is gone. Always set by
|
|
28
|
+
* `resolveErrorTargets`; optional so targets built by hand still type.
|
|
29
|
+
*/
|
|
30
|
+
focusKey?: string;
|
|
22
31
|
/** The server's messages for that key, as sent. */
|
|
23
32
|
messages: string[];
|
|
24
33
|
/**
|
|
@@ -31,7 +40,10 @@ export interface ErrorTarget {
|
|
|
31
40
|
fieldKey: string | null;
|
|
32
41
|
/** The tab holding the owning field, or `null` (flat form, or unowned). */
|
|
33
42
|
tabKey: string | null;
|
|
34
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* A readable name for the key: "Price (line 1)", "Delivery date", or a
|
|
45
|
+
* row's own name from the field's `rowLabel`.
|
|
46
|
+
*/
|
|
35
47
|
label: string;
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -62,6 +74,14 @@ export interface ResolveErrorTargetsOptions {
|
|
|
62
74
|
field: FieldDefinition,
|
|
63
75
|
labelModel: Record<string, any>,
|
|
64
76
|
) => string | null | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The rows a failed submit's errors named, as captured then
|
|
79
|
+
* (`SubmitFailure.failedRows`). Pass it when resolving that failure's
|
|
80
|
+
* errors after the form may have changed (the error summary does), so a
|
|
81
|
+
* row error keeps the name of the row that failed and points at where
|
|
82
|
+
* that row is now. Leave it out for the form's live errors.
|
|
83
|
+
*/
|
|
84
|
+
failedRows?: Record<string, FailedRow>;
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
interface Candidate {
|
|
@@ -137,6 +157,26 @@ function defaultLabel(
|
|
|
137
157
|
return typeof field.label === "function" ? field.label(model) : field.label;
|
|
138
158
|
}
|
|
139
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The field's `rowLabel` for one row, or `null` when it has none, names
|
|
162
|
+
* nothing (empty or not a string) or throws: the caller then falls back to
|
|
163
|
+
* "<label> (line N)", so a broken row name can never hide an error.
|
|
164
|
+
*/
|
|
165
|
+
function rowLabelFor(
|
|
166
|
+
field: FieldDefinition,
|
|
167
|
+
row: Record<string, any>,
|
|
168
|
+
index: number,
|
|
169
|
+
): string | null {
|
|
170
|
+
if (typeof field.rowLabel !== "function") return null;
|
|
171
|
+
let name: unknown;
|
|
172
|
+
try {
|
|
173
|
+
name = field.rowLabel(row, index);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return typeof name === "string" && name.trim() !== "" ? name : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
140
180
|
/**
|
|
141
181
|
* Resolve every key in `errors` to its owning field, tab and label, with the
|
|
142
182
|
* display line for each message. Rows come in tab order, then field order
|
|
@@ -151,7 +191,11 @@ function defaultLabel(
|
|
|
151
191
|
*
|
|
152
192
|
* Labels: the owning field's label; for `field.N.sub` the sub-field's label
|
|
153
193
|
* (when the field defines `fields` and one has that key) or the field's own,
|
|
154
|
-
* plus "(line N+1)"
|
|
194
|
+
* plus "(line N+1)" — unless the field's `rowLabel` names that row, when
|
|
195
|
+
* the row's name is the whole label. With `failedRows`, a row key's label
|
|
196
|
+
* reads the row as it was when the submit failed, and its `focusKey` and
|
|
197
|
+
* line number follow that row to where it is now (see `ownedTarget`); for
|
|
198
|
+
* any other nested key (a media map's
|
|
155
199
|
* `image_media.<uuid>`) the field's label. An unowned key is humanised.
|
|
156
200
|
*
|
|
157
201
|
* Display lines: "Label: message", except that the label is left off when
|
|
@@ -242,20 +286,68 @@ export function resolveErrorTargets(
|
|
|
242
286
|
return longest;
|
|
243
287
|
};
|
|
244
288
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Where a failed row is now: its index in the field's live rows, found by
|
|
291
|
+
* identity, or -1 when it is gone (removed, or the rows were replaced).
|
|
292
|
+
*/
|
|
293
|
+
const currentIndexOf = (field: FieldDefinition, failedRow: FailedRow): number => {
|
|
294
|
+
const liveRows = getByPath(model, field.key);
|
|
295
|
+
if (!Array.isArray(liveRows)) return -1;
|
|
296
|
+
return liveRows.findIndex((row) => toRaw(row) === failedRow.row);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The label and focus key for an owned error key. For a row key
|
|
301
|
+
* (`field.N.sub`) with a captured failed row, both follow that row: the
|
|
302
|
+
* label is read from the row as it was when the submit failed, and the
|
|
303
|
+
* focus key uses the row's current index, or is the field's key when the
|
|
304
|
+
* row is gone. "(line N)" then counts the row's CURRENT position while it
|
|
305
|
+
* exists (the line a click takes the user to), and the position it failed
|
|
306
|
+
* at once it is gone.
|
|
307
|
+
*/
|
|
308
|
+
const ownedTarget = (
|
|
309
|
+
errorKey: string,
|
|
310
|
+
field: FieldDefinition,
|
|
311
|
+
): { label: string; focusKey: string } => {
|
|
312
|
+
if (!isNestedUnder(errorKey, field.key)) {
|
|
313
|
+
return { label: labelFor(field), focusKey: errorKey };
|
|
314
|
+
}
|
|
315
|
+
const [first, second, ...deeper] = errorKey
|
|
316
|
+
.slice(field.key.length + 1)
|
|
317
|
+
.split(".");
|
|
318
|
+
if (!NUMERIC_SEGMENT.test(first)) {
|
|
319
|
+
return { label: labelFor(field), focusKey: errorKey };
|
|
320
|
+
}
|
|
321
|
+
const failedIndex = Number(first);
|
|
249
322
|
const subField =
|
|
250
323
|
second !== undefined
|
|
251
324
|
? field.fields?.find((candidate) => candidate.key === second)
|
|
252
325
|
: undefined;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
326
|
+
const failedRow = options.failedRows?.[`${field.key}.${first}`];
|
|
327
|
+
|
|
328
|
+
let rowModel: Record<string, any>;
|
|
329
|
+
let lineIndex = failedIndex;
|
|
330
|
+
let focusKey = errorKey;
|
|
331
|
+
if (failedRow !== undefined) {
|
|
332
|
+
rowModel = failedRow.snapshot;
|
|
333
|
+
const currentIndex = currentIndexOf(field, failedRow);
|
|
334
|
+
if (currentIndex >= 0) {
|
|
335
|
+
lineIndex = currentIndex;
|
|
336
|
+
const rest = second !== undefined ? [second, ...deeper] : [];
|
|
337
|
+
focusKey = [field.key, String(currentIndex), ...rest].join(".");
|
|
338
|
+
} else {
|
|
339
|
+
focusKey = field.key;
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
// DXRepeater renders a sub-field with the ROW as its model, so its
|
|
343
|
+
// label resolves against the row here too.
|
|
344
|
+
const row = getByPath(model, `${field.key}.${first}`);
|
|
345
|
+
rowModel = row !== null && typeof row === "object" ? row : {};
|
|
346
|
+
}
|
|
347
|
+
const rowName = rowLabelFor(field, rowModel, failedIndex);
|
|
348
|
+
if (rowName !== null) return { label: rowName, focusKey };
|
|
257
349
|
const base = subField ? labelFor(subField, rowModel) : labelFor(field);
|
|
258
|
-
return `${base} (line ${
|
|
350
|
+
return { label: `${base} (line ${lineIndex + 1})`, focusKey };
|
|
259
351
|
};
|
|
260
352
|
|
|
261
353
|
// ——— one target per error key with at least one message
|
|
@@ -272,15 +364,15 @@ export function resolveErrorTargets(
|
|
|
272
364
|
);
|
|
273
365
|
if (messages.length === 0) return;
|
|
274
366
|
const owner = ownerOf(errorKey);
|
|
367
|
+
const owned = owner ? ownedTarget(errorKey, owner.field) : null;
|
|
275
368
|
pending.push({
|
|
276
369
|
target: {
|
|
277
370
|
errorKey,
|
|
371
|
+
focusKey: owned ? owned.focusKey : errorKey,
|
|
278
372
|
messages,
|
|
279
373
|
fieldKey: owner ? owner.field.key : null,
|
|
280
374
|
tabKey: owner ? owner.tabKey : null,
|
|
281
|
-
label:
|
|
282
|
-
? ownedLabel(errorKey, owner.field)
|
|
283
|
-
: humaniseErrorKey(errorKey),
|
|
375
|
+
label: owned ? owned.label : humaniseErrorKey(errorKey),
|
|
284
376
|
},
|
|
285
377
|
rank: owner ? owner.rank : [UNOWNED_RANK, UNOWNED_RANK],
|
|
286
378
|
order,
|
|
@@ -124,3 +124,30 @@ export function isFieldVisible(
|
|
|
124
124
|
export function isSubmittableField(field: { submit?: boolean }): boolean {
|
|
125
125
|
return field.submit !== false;
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The payload keys to drop on submit: every field key for which NO field with
|
|
130
|
+
* that key is both submittable (`submit !== false`) and visible (#228).
|
|
131
|
+
*
|
|
132
|
+
* Decided per KEY, across all the fields that share it, never per field. Two
|
|
133
|
+
* fields may share a key: an editable select plus a read-only display twin
|
|
134
|
+
* (`submit: false`) that shows instead for system-created rows. Stripping a
|
|
135
|
+
* key because ONE of its fields is presentational or hidden removed it from
|
|
136
|
+
* every save, including while the select was the visible field in use. A key
|
|
137
|
+
* whose fields are all presentational (#110) or all hidden (#117) is still
|
|
138
|
+
* dropped.
|
|
139
|
+
*/
|
|
140
|
+
export function nonSubmittedFieldKeys<
|
|
141
|
+
TField extends { key: string; submit?: boolean },
|
|
142
|
+
>(fields: readonly TField[], isVisible: (field: TField) => boolean): Set<string> {
|
|
143
|
+
const submittedKeys = new Set(
|
|
144
|
+
fields
|
|
145
|
+
.filter((field) => isSubmittableField(field) && isVisible(field))
|
|
146
|
+
.map((field) => field.key),
|
|
147
|
+
);
|
|
148
|
+
return new Set(
|
|
149
|
+
fields
|
|
150
|
+
.map((field) => field.key)
|
|
151
|
+
.filter((key) => !submittedKeys.has(key)),
|
|
152
|
+
);
|
|
153
|
+
}
|