@omnitend/dashboard-for-laravel 0.46.0 → 0.47.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/dist/components/extended/DXField.vue.d.ts +16 -0
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
- package/dist/dashboard-for-laravel.js +9527 -9462
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +5 -5
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +60 -0
- package/dist/utils/formErrorTargets.d.ts +6 -2
- package/dist/utils/formSchema.d.ts +16 -0
- package/docs/public/api-reference.json +125 -5
- 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 +86 -23
- 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/useResourceEditor.ts +64 -26
- package/resources/js/index.ts +2 -0
- package/resources/js/types/index.ts +64 -0
- package/resources/js/utils/formErrorTargets.ts +28 -2
- package/resources/js/utils/formSchema.ts +27 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveFieldDefault,
|
|
8
8
|
isFieldVisible as isFieldVisibleFor,
|
|
9
9
|
isSubmittableField,
|
|
10
|
+
nonSubmittedFieldKeys as nonSubmittedFieldKeysFor,
|
|
10
11
|
} from "../utils/formSchema";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -207,24 +208,67 @@ export function useResourceEditor<T = any>(
|
|
|
207
208
|
// Enforced at SUBMIT, not just at seeding: the modal still renders every field,
|
|
208
209
|
// so a `submit: false` control — or a `span` slot calling `update` — can write
|
|
209
210
|
// its key back into the form data after seeding. Strip them on the way out.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Hidden by `when` at submit time (#117). Submitting a field
|
|
219
|
-
// the user cannot see is a silent write — and with `default`
|
|
220
|
-
// set it writes a value they never chose. Omitting the key
|
|
221
|
-
// leaves the stored value alone.
|
|
222
|
-
!isEditFieldVisible(field),
|
|
223
|
-
)
|
|
224
|
-
.map((field) => field.key),
|
|
225
|
-
),
|
|
211
|
+
//
|
|
212
|
+
// Decided per KEY (#228): a key is dropped only when no field with that key
|
|
213
|
+
// is both submitting and visible at submit time. A presentational field
|
|
214
|
+
// (#110) holds no data, and a field hidden by `when` (#117) must not write a
|
|
215
|
+
// value the user cannot see — but a display-only twin sharing its key with
|
|
216
|
+
// the visible editable field must not strip the editable field's value.
|
|
217
|
+
const nonSubmittedFieldKeys = computed(() =>
|
|
218
|
+
nonSubmittedFieldKeysFor(props.editFields ?? [], isEditFieldVisible),
|
|
226
219
|
);
|
|
227
220
|
|
|
221
|
+
/*
|
|
222
|
+
* Seed the form's data, ONE value per key (#228). Presentational fields are
|
|
223
|
+
* never seeded. When several submitting fields share a key (alternative
|
|
224
|
+
* controls for one value, switched by `when`), the key is seeded from the
|
|
225
|
+
* first of them that is visible against the seeded model, else the first
|
|
226
|
+
* declared. This used to be last-declared-wins, so a hidden alternative
|
|
227
|
+
* declared after the visible field replaced its default.
|
|
228
|
+
*
|
|
229
|
+
* Two submitting fields with one key that are visible TOGETHER are almost
|
|
230
|
+
* certainly a mistake (two controls bound to one value): the first declared
|
|
231
|
+
* wins, and DXTable warns once per key.
|
|
232
|
+
*/
|
|
233
|
+
const warnedSharedVisibleKeys = new Set<string>();
|
|
234
|
+
const seedFormData = (
|
|
235
|
+
valueFor: (field: FieldDefinition) => any,
|
|
236
|
+
context: Record<string, any>,
|
|
237
|
+
): Record<string, any> => {
|
|
238
|
+
const fieldsByKey = new Map<string, FieldDefinition[]>();
|
|
239
|
+
submittableEditFields.value.forEach((field) => {
|
|
240
|
+
const sharing = fieldsByKey.get(field.key);
|
|
241
|
+
if (sharing) sharing.push(field);
|
|
242
|
+
else fieldsByKey.set(field.key, [field]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const formData: Record<string, any> = {};
|
|
246
|
+
fieldsByKey.forEach((fields, key) => {
|
|
247
|
+
formData[key] = valueFor(fields[0]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const seededModel = { ...context, ...formData };
|
|
251
|
+
fieldsByKey.forEach((fields, key) => {
|
|
252
|
+
if (fields.length < 2) return;
|
|
253
|
+
const visibleFields = fields.filter((field) =>
|
|
254
|
+
isFieldVisibleFor(field, seededModel),
|
|
255
|
+
);
|
|
256
|
+
if (visibleFields.length > 1 && !warnedSharedVisibleKeys.has(key)) {
|
|
257
|
+
warnedSharedVisibleKeys.add(key);
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
259
|
+
console.warn(
|
|
260
|
+
`[DXTable] ${visibleFields.length} submitting edit fields share the key "${key}" and are visible together, ` +
|
|
261
|
+
'so they edit one value; the first declared seeds it. ' +
|
|
262
|
+
'Give alternatives mutually exclusive `when` predicates, or mark a display-only one `submit: false`.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (visibleFields.length > 0 && visibleFields[0] !== fields[0]) {
|
|
266
|
+
formData[key] = valueFor(visibleFields[0]);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return formData;
|
|
270
|
+
};
|
|
271
|
+
|
|
228
272
|
const stripNonSubmittedFields = (data: Record<string, any>): Record<string, any> => {
|
|
229
273
|
if (nonSubmittedFieldKeys.value.size === 0) return data;
|
|
230
274
|
return Object.fromEntries(
|
|
@@ -265,11 +309,9 @@ export function useResourceEditor<T = any>(
|
|
|
265
309
|
// between successive row opens. Presentational fields (submit: false)
|
|
266
310
|
// lay the form out; they hold no data and must not be POSTed just
|
|
267
311
|
// because they were declared.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
startSession(formData);
|
|
312
|
+
startSession(
|
|
313
|
+
seedFormData((field) => seedValueFor(field, item), item as any),
|
|
314
|
+
);
|
|
273
315
|
|
|
274
316
|
// Open modal
|
|
275
317
|
editFormInstanceKey.value++;
|
|
@@ -332,11 +374,7 @@ export function useResourceEditor<T = any>(
|
|
|
332
374
|
editLoading.value = false;
|
|
333
375
|
|
|
334
376
|
// A fresh form seeded with the field defaults (see `startSession`).
|
|
335
|
-
|
|
336
|
-
submittableEditFields.value.forEach(field => {
|
|
337
|
-
formData[field.key] = resolveFieldDefault(field);
|
|
338
|
-
});
|
|
339
|
-
startSession(formData);
|
|
377
|
+
startSession(seedFormData(resolveFieldDefault, {}));
|
|
340
378
|
editFormInstanceKey.value++;
|
|
341
379
|
showEditModal.value = true;
|
|
342
380
|
};
|
package/resources/js/index.ts
CHANGED
|
@@ -148,6 +148,8 @@ export type {
|
|
|
148
148
|
FormTab,
|
|
149
149
|
MaybeFn,
|
|
150
150
|
OptionsLoader,
|
|
151
|
+
PasswordActionsSlotProps,
|
|
152
|
+
EditPasswordActionsSlotProps,
|
|
151
153
|
// Strict, discriminated field types (#131) — opt-in; canonical in a future
|
|
152
154
|
// major. `FieldDef` is the union; the rest are its per-type members.
|
|
153
155
|
FieldDef,
|
|
@@ -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
|
|
|
@@ -579,6 +597,7 @@ export interface ComponentFieldDef extends BaseFieldDef {
|
|
|
579
597
|
export interface RepeaterFieldDef extends BaseFieldDef {
|
|
580
598
|
type: "repeater";
|
|
581
599
|
fields?: FieldDef[];
|
|
600
|
+
rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
|
|
582
601
|
addLabel?: string;
|
|
583
602
|
minItems?: number;
|
|
584
603
|
maxItems?: number;
|
|
@@ -652,6 +671,51 @@ export interface FormTab {
|
|
|
652
671
|
lazy?: boolean;
|
|
653
672
|
}
|
|
654
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Slot props of DXField's `password-actions` slot (DXForm:
|
|
676
|
+
* `password-actions(<key>)`): content placed beside a password input, inside
|
|
677
|
+
* its input group after the reveal (eye) toggle, such as a "Generate password"
|
|
678
|
+
* button. `setRevealed` drives the same state the eye toggles.
|
|
679
|
+
*
|
|
680
|
+
* ```vue
|
|
681
|
+
* <template #password-actions(password)="{ update, setRevealed, disabled }">
|
|
682
|
+
* <DButton variant="outline-secondary" :disabled="disabled"
|
|
683
|
+
* @click="update(generatePassword()); setRevealed(true)">Generate</DButton>
|
|
684
|
+
* </template>
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
export interface PasswordActionsSlotProps {
|
|
688
|
+
/** The field definition being rendered. */
|
|
689
|
+
field: FieldDefinition;
|
|
690
|
+
/** The model passed to field predicates (defaults to the live form data). */
|
|
691
|
+
model: any;
|
|
692
|
+
/** The current field value. */
|
|
693
|
+
value: any;
|
|
694
|
+
/** Write a new value (also marks the field touched and clears its error). */
|
|
695
|
+
update: (value: any) => void;
|
|
696
|
+
/** Whether the password is currently shown as plain text. */
|
|
697
|
+
revealed: boolean;
|
|
698
|
+
/**
|
|
699
|
+
* Show (`true`) or mask (`false`) the password, exactly as the eye does.
|
|
700
|
+
* A no-op on a `revealable: false` field, which never unmasks.
|
|
701
|
+
*/
|
|
702
|
+
setRevealed: (revealed: boolean) => void;
|
|
703
|
+
/** Whether the field is disabled or read-only. */
|
|
704
|
+
disabled: boolean;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Slot props of DXTable's `edit-password-actions(<key>)` slot, the edit/create
|
|
709
|
+
* modal's form of {@link PasswordActionsSlotProps}. `item` is the row being
|
|
710
|
+
* edited, `null` in create mode. (`model` is not passed, as with the modal's
|
|
711
|
+
* other field slots.)
|
|
712
|
+
*/
|
|
713
|
+
export interface EditPasswordActionsSlotProps
|
|
714
|
+
extends Omit<PasswordActionsSlotProps, "model"> {
|
|
715
|
+
/** The row being edited; `null` in create mode. */
|
|
716
|
+
item: any;
|
|
717
|
+
}
|
|
718
|
+
|
|
655
719
|
/**
|
|
656
720
|
* The theme's extra colour variant, `pending`: waiting, and the next move is
|
|
657
721
|
* not yours ("awaiting payment", "sent to printer, not yet confirmed").
|
|
@@ -31,7 +31,10 @@ export interface ErrorTarget {
|
|
|
31
31
|
fieldKey: string | null;
|
|
32
32
|
/** The tab holding the owning field, or `null` (flat form, or unowned). */
|
|
33
33
|
tabKey: string | null;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A readable name for the key: "Price (line 1)", "Delivery date", or a
|
|
36
|
+
* row's own name from the field's `rowLabel`.
|
|
37
|
+
*/
|
|
35
38
|
label: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -137,6 +140,26 @@ function defaultLabel(
|
|
|
137
140
|
return typeof field.label === "function" ? field.label(model) : field.label;
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The field's `rowLabel` for one row, or `null` when it has none, names
|
|
145
|
+
* nothing (empty or not a string) or throws: the caller then falls back to
|
|
146
|
+
* "<label> (line N)", so a broken row name can never hide an error.
|
|
147
|
+
*/
|
|
148
|
+
function rowLabelFor(
|
|
149
|
+
field: FieldDefinition,
|
|
150
|
+
row: Record<string, any>,
|
|
151
|
+
index: number,
|
|
152
|
+
): string | null {
|
|
153
|
+
if (typeof field.rowLabel !== "function") return null;
|
|
154
|
+
let name: unknown;
|
|
155
|
+
try {
|
|
156
|
+
name = field.rowLabel(row, index);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return typeof name === "string" && name.trim() !== "" ? name : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
140
163
|
/**
|
|
141
164
|
* Resolve every key in `errors` to its owning field, tab and label, with the
|
|
142
165
|
* display line for each message. Rows come in tab order, then field order
|
|
@@ -151,7 +174,8 @@ function defaultLabel(
|
|
|
151
174
|
*
|
|
152
175
|
* Labels: the owning field's label; for `field.N.sub` the sub-field's label
|
|
153
176
|
* (when the field defines `fields` and one has that key) or the field's own,
|
|
154
|
-
* plus "(line N+1)"
|
|
177
|
+
* plus "(line N+1)" — unless the field's `rowLabel` names that row, when
|
|
178
|
+
* the row's name is the whole label; for any other nested key (a media map's
|
|
155
179
|
* `image_media.<uuid>`) the field's label. An unowned key is humanised.
|
|
156
180
|
*
|
|
157
181
|
* Display lines: "Label: message", except that the label is left off when
|
|
@@ -254,6 +278,8 @@ export function resolveErrorTargets(
|
|
|
254
278
|
// label resolves against the row here too.
|
|
255
279
|
const row = getByPath(model, `${field.key}.${first}`);
|
|
256
280
|
const rowModel = row !== null && typeof row === "object" ? row : {};
|
|
281
|
+
const rowName = rowLabelFor(field, rowModel, Number(first));
|
|
282
|
+
if (rowName !== null) return rowName;
|
|
257
283
|
const base = subField ? labelFor(subField, rowModel) : labelFor(field);
|
|
258
284
|
return `${base} (line ${Number(first) + 1})`;
|
|
259
285
|
};
|
|
@@ -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
|
+
}
|