@omnitend/dashboard-for-laravel 0.42.2 → 0.44.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/base/DFormSelect.vue.d.ts +1 -1
- package/dist/components/extended/DXChoiceField.vue.d.ts +3 -3
- package/dist/components/extended/DXCurrencyInput.vue.d.ts +2 -2
- package/dist/components/extended/DXDashboard.vue.d.ts +14 -6
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +12 -3
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +21 -1
- package/dist/components/extended/DXForm.vue.d.ts +66 -1
- package/dist/components/extended/DXFormErrorSummary.vue.d.ts +70 -0
- package/dist/components/extended/DXFormField.vue.d.ts +3 -0
- package/dist/components/extended/DXStackingTable.vue.d.ts +25 -0
- package/dist/components/extended/DXTable.vue.d.ts +20 -0
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +14 -10
- package/dist/composables/useAsyncOptions.d.ts +14 -1
- package/dist/composables/useForm.d.ts +26 -0
- package/dist/dashboard-for-laravel.js +12002 -11223
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +12 -0
- package/dist/types/navigation.d.ts +8 -0
- package/dist/utils/formErrorTargets.d.ts +86 -0
- package/dist/utils/pageScrollLock.d.ts +9 -0
- package/docs/public/api-reference.json +223 -9
- package/docs/public/docs-map.md +4 -2
- package/docs/public/llms.txt +4 -2
- package/package.json +9 -8
- package/resources/css/theme.scss +508 -36
- package/resources/js/components/base/DFormSelect.vue +5 -2
- package/resources/js/components/base/DModal.vue +20 -2
- package/resources/js/components/extended/DXChoiceField.vue +42 -26
- package/resources/js/components/extended/DXCurrencyInput.vue +3 -1
- package/resources/js/components/extended/DXDashboard.vue +236 -43
- package/resources/js/components/extended/DXDashboardNavbar.vue +23 -13
- package/resources/js/components/extended/DXDashboardSidebar.vue +103 -29
- package/resources/js/components/extended/DXField.vue +15 -9
- package/resources/js/components/extended/DXForm.vue +676 -28
- package/resources/js/components/extended/DXFormErrorSummary.vue +232 -0
- package/resources/js/components/extended/DXFormField.vue +11 -2
- package/resources/js/components/extended/DXRepeater.vue +30 -4
- package/resources/js/components/extended/DXStackingTable.vue +264 -0
- package/resources/js/components/extended/DXTable.vue +36 -4
- package/resources/js/components/extended/DXTableEditorModal.vue +29 -1
- package/resources/js/composables/useAsyncOptions.ts +30 -5
- package/resources/js/composables/useContainerWidth.ts +12 -3
- package/resources/js/composables/useForm.ts +196 -18
- package/resources/js/composables/useResourceEditor.ts +140 -107
- package/resources/js/index.ts +17 -0
- package/resources/js/types/index.ts +13 -0
- package/resources/js/types/navigation.ts +9 -0
- package/resources/js/utils/formErrorTargets.ts +317 -0
- package/resources/js/utils/pageScrollLock.ts +27 -0
- package/scripts/mcp-server.mjs +58 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, ref, watch, type Ref } from "vue";
|
|
2
2
|
import { api } from "../utils/api";
|
|
3
|
-
import { useForm } from "./useForm";
|
|
3
|
+
import { isAbortError, useForm, type FormError } from "./useForm";
|
|
4
4
|
import { useToast } from "./useToast";
|
|
5
5
|
import type { FieldDefinition } from "../types";
|
|
6
6
|
import {
|
|
@@ -126,6 +126,31 @@ export function useResourceEditor<T = any>(
|
|
|
126
126
|
{ flush: 'sync' },
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
+
/*
|
|
130
|
+
* Each open (edit or create) starts a new SESSION with a FRESH form
|
|
131
|
+
* (#194 review). The modal used to reuse one `useForm` and reseed it,
|
|
132
|
+
* which carried the previous session's `submitFailure` (row A's failure
|
|
133
|
+
* summary over row B, or over create) and let a request still in flight
|
|
134
|
+
* from the previous session record its outcome onto the new one. A fresh
|
|
135
|
+
* form makes both impossible by construction: an abandoned request settles
|
|
136
|
+
* into the OLD form object, which nothing renders any more. The DXForm
|
|
137
|
+
* subtree is already remounted per open (`editFormInstanceKey`), so no
|
|
138
|
+
* rendered state depended on the object surviving. Resetting one shared
|
|
139
|
+
* form instead would have needed every piece of per-submit state (failure,
|
|
140
|
+
* success flags, the in-flight counter, timers) reset in step, plus a
|
|
141
|
+
* guard against late responses: the fresh form is one line and cannot
|
|
142
|
+
* miss a field added later.
|
|
143
|
+
*
|
|
144
|
+
* The generation is bumped too, so the save/delete paths can tell a
|
|
145
|
+
* response for THIS session from one for an abandoned session, and the
|
|
146
|
+
* previous session's pending action no longer blocks this one.
|
|
147
|
+
*/
|
|
148
|
+
const startSession = (formData: Record<string, any>) => {
|
|
149
|
+
editGeneration.value++;
|
|
150
|
+
pendingAction.value = null;
|
|
151
|
+
editForm.value = useForm(formData);
|
|
152
|
+
};
|
|
153
|
+
|
|
129
154
|
// Monotonic token so a slow fetch for a previously-opened row can't overwrite
|
|
130
155
|
// the form after the user has since opened a different row.
|
|
131
156
|
let editFetchToken = 0;
|
|
@@ -235,23 +260,16 @@ export function useResourceEditor<T = any>(
|
|
|
235
260
|
// Reset to first tab
|
|
236
261
|
activeTabIndex.value = 0;
|
|
237
262
|
|
|
238
|
-
//
|
|
239
|
-
// seeding is synchronous — no interleaving
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} else {
|
|
249
|
-
// Update existing form
|
|
250
|
-
submittableEditFields.value.forEach(field => {
|
|
251
|
-
editForm.value.data[field.key] = seedValueFor(field, item);
|
|
252
|
-
});
|
|
253
|
-
editForm.value.clearErrors();
|
|
254
|
-
}
|
|
263
|
+
// A fresh form seeded from the row (see `startSession`). `useForm` is
|
|
264
|
+
// statically imported, so seeding is synchronous — no interleaving
|
|
265
|
+
// between successive row opens. Presentational fields (submit: false)
|
|
266
|
+
// lay the form out; they hold no data and must not be POSTed just
|
|
267
|
+
// because they were declared.
|
|
268
|
+
const formData: Record<string, any> = {};
|
|
269
|
+
submittableEditFields.value.forEach(field => {
|
|
270
|
+
formData[field.key] = seedValueFor(field, item);
|
|
271
|
+
});
|
|
272
|
+
startSession(formData);
|
|
255
273
|
|
|
256
274
|
// Open modal
|
|
257
275
|
editFormInstanceKey.value++;
|
|
@@ -313,19 +331,12 @@ export function useResourceEditor<T = any>(
|
|
|
313
331
|
editFetchToken++;
|
|
314
332
|
editLoading.value = false;
|
|
315
333
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
} else {
|
|
323
|
-
// Reset existing form to defaults
|
|
324
|
-
submittableEditFields.value.forEach(field => {
|
|
325
|
-
editForm.value.data[field.key] = resolveFieldDefault(field);
|
|
326
|
-
});
|
|
327
|
-
editForm.value.clearErrors();
|
|
328
|
-
}
|
|
334
|
+
// A fresh form seeded with the field defaults (see `startSession`).
|
|
335
|
+
const formData: Record<string, any> = {};
|
|
336
|
+
submittableEditFields.value.forEach(field => {
|
|
337
|
+
formData[field.key] = resolveFieldDefault(field);
|
|
338
|
+
});
|
|
339
|
+
startSession(formData);
|
|
329
340
|
editFormInstanceKey.value++;
|
|
330
341
|
showEditModal.value = true;
|
|
331
342
|
};
|
|
@@ -359,9 +370,11 @@ export function useResourceEditor<T = any>(
|
|
|
359
370
|
// performSave so it covers the whole class, not one call site.
|
|
360
371
|
if (editGeneration.value !== generation) return;
|
|
361
372
|
|
|
362
|
-
await performSave();
|
|
373
|
+
await performSave(generation);
|
|
363
374
|
} finally {
|
|
364
|
-
|
|
375
|
+
// Only this session's own pending action: a session opened since
|
|
376
|
+
// has already cleared it, and may have started its own.
|
|
377
|
+
if (editGeneration.value === generation) pendingAction.value = null;
|
|
365
378
|
}
|
|
366
379
|
};
|
|
367
380
|
|
|
@@ -429,11 +442,60 @@ export function useResourceEditor<T = any>(
|
|
|
429
442
|
);
|
|
430
443
|
};
|
|
431
444
|
|
|
432
|
-
|
|
445
|
+
/**
|
|
446
|
+
* A failed create/edit/delete request. `useForm` hands `onError` the
|
|
447
|
+
* `ApiError` (`{ message, errors, status }`), not an errors map, and
|
|
448
|
+
* records every non-abort failure in `form.submitFailure`, which the
|
|
449
|
+
* modal footer's error summary shows beside Save (the server's message
|
|
450
|
+
* and every validation message, #194). While the session that made the
|
|
451
|
+
* request still has its modal open, that summary IS the report, so a
|
|
452
|
+
* toast would only repeat it: nothing is toasted. A failure that settles
|
|
453
|
+
* after its modal closed (or another row opened) has no summary left, so
|
|
454
|
+
* it toasts the client's message for it ("Server error. Please try again
|
|
455
|
+
* later.", or the server's 422 line) or `fallback` for a non-HTTP failure.
|
|
456
|
+
* An abort is a cancellation, never reported.
|
|
457
|
+
*/
|
|
458
|
+
const reportFailure = (
|
|
459
|
+
error: FormError | unknown,
|
|
460
|
+
fallback: string,
|
|
461
|
+
generation: number,
|
|
462
|
+
) => {
|
|
463
|
+
if (isAbortError(error)) return;
|
|
464
|
+
if (editGeneration.value === generation && showEditModal.value) return;
|
|
465
|
+
const status = (error as FormError | null)?.status;
|
|
466
|
+
// Only an HTTP failure's message is written for users; a thrown
|
|
467
|
+
// TypeError ("Failed to fetch") gets the fallback.
|
|
468
|
+
const message = typeof status === 'number' ? (error as FormError).message : '';
|
|
469
|
+
createToast?.({
|
|
470
|
+
title: 'Error',
|
|
471
|
+
body: typeof message === 'string' && message !== '' ? message : fallback,
|
|
472
|
+
variant: 'danger',
|
|
473
|
+
modelValue: 5000,
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/*
|
|
478
|
+
* `generation` is the session that started this save. The request may
|
|
479
|
+
* settle after the user has closed the modal and opened another row (or
|
|
480
|
+
* create): the record was still written, so the success toast, the
|
|
481
|
+
* `rowCreated`/`rowUpdated` emit and the table refresh still happen, but
|
|
482
|
+
* only the SAME session's modal is closed. The edited row and the form
|
|
483
|
+
* are captured up front, so the emits name the row that was saved, not
|
|
484
|
+
* whichever row is open by the time the response arrives.
|
|
485
|
+
*
|
|
486
|
+
* One error emit per failure: `useForm` calls `onError` and then
|
|
487
|
+
* rethrows, so the `catch` emits only for a throw that never reached
|
|
488
|
+
* `onError` (one raised before the request was made).
|
|
489
|
+
*/
|
|
490
|
+
const performSave = async (generation: number) => {
|
|
491
|
+
const form = editForm.value;
|
|
492
|
+
const isCurrentSession = () => editGeneration.value === generation;
|
|
493
|
+
|
|
433
494
|
// Create mode: POST to createUrl
|
|
434
495
|
if (isCreateMode.value && props.createUrl) {
|
|
496
|
+
let errorReported = false;
|
|
435
497
|
try {
|
|
436
|
-
await
|
|
498
|
+
await form.post(props.createUrl, {
|
|
437
499
|
transform: stripNonSubmittedFields,
|
|
438
500
|
onSuccess: (data: any) => {
|
|
439
501
|
createToast?.({
|
|
@@ -444,49 +506,38 @@ export function useResourceEditor<T = any>(
|
|
|
444
506
|
});
|
|
445
507
|
|
|
446
508
|
emit('rowCreated', data?.data ?? data, data);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
509
|
+
if (isCurrentSession()) {
|
|
510
|
+
showEditModal.value = false;
|
|
511
|
+
selectedItem.value = null;
|
|
512
|
+
isCreateMode.value = false;
|
|
513
|
+
}
|
|
450
514
|
|
|
451
515
|
refresh();
|
|
452
516
|
},
|
|
453
|
-
onError: (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (typeof firstError === 'string') {
|
|
458
|
-
errorMessage = firstError;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
createToast?.({
|
|
463
|
-
title: 'Error',
|
|
464
|
-
body: errorMessage,
|
|
465
|
-
variant: 'danger',
|
|
466
|
-
modelValue: 5000,
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// DXForm switches to the first errored tab via its
|
|
470
|
-
// own watcher on editForm.errors.
|
|
471
|
-
emit('createError', errors);
|
|
517
|
+
onError: (error: FormError) => {
|
|
518
|
+
errorReported = true;
|
|
519
|
+
reportFailure(error, 'Failed to create. Please try again.', generation);
|
|
520
|
+
emit('createError', error);
|
|
472
521
|
}
|
|
473
522
|
});
|
|
474
523
|
} catch (error) {
|
|
475
|
-
emit('createError', error);
|
|
524
|
+
if (!errorReported) emit('createError', error);
|
|
476
525
|
}
|
|
477
526
|
return;
|
|
478
527
|
}
|
|
479
528
|
|
|
480
529
|
// Edit mode: PUT to editUrl
|
|
481
|
-
|
|
530
|
+
const item = selectedItem.value as T | null;
|
|
531
|
+
if (!item) return;
|
|
482
532
|
|
|
533
|
+
let errorReported = false;
|
|
483
534
|
try {
|
|
484
535
|
// If editUrl provided, handle API call internally
|
|
485
536
|
if (props.editUrl) {
|
|
486
|
-
const itemId = (
|
|
537
|
+
const itemId = (item as any).id;
|
|
487
538
|
const url = props.editUrl.replace(':id', itemId);
|
|
488
539
|
|
|
489
|
-
await
|
|
540
|
+
await form.put(url, {
|
|
490
541
|
transform: stripNonSubmittedFields,
|
|
491
542
|
onSuccess: (data: any) => {
|
|
492
543
|
// Show success toast
|
|
@@ -497,44 +548,29 @@ export function useResourceEditor<T = any>(
|
|
|
497
548
|
modelValue: 3000, // Auto-dismiss after 3 seconds
|
|
498
549
|
});
|
|
499
550
|
|
|
500
|
-
emit('rowUpdated',
|
|
501
|
-
|
|
502
|
-
|
|
551
|
+
emit('rowUpdated', item, data);
|
|
552
|
+
if (isCurrentSession()) {
|
|
553
|
+
showEditModal.value = false;
|
|
554
|
+
selectedItem.value = null;
|
|
555
|
+
}
|
|
503
556
|
|
|
504
557
|
// Refresh table data to show updated values
|
|
505
558
|
refresh();
|
|
506
559
|
},
|
|
507
|
-
onError: (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const firstError = Object.values(errors).flat()[0];
|
|
512
|
-
if (typeof firstError === 'string') {
|
|
513
|
-
errorMessage = firstError;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Show error toast with specific message
|
|
518
|
-
createToast?.({
|
|
519
|
-
title: 'Error',
|
|
520
|
-
body: errorMessage,
|
|
521
|
-
variant: 'danger',
|
|
522
|
-
modelValue: 5000, // Auto-dismiss after 5 seconds
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// DXForm switches to the first errored tab via its
|
|
526
|
-
// own watcher on editForm.errors.
|
|
527
|
-
emit('editError', selectedItem.value as T, errors);
|
|
560
|
+
onError: (error: FormError) => {
|
|
561
|
+
errorReported = true;
|
|
562
|
+
reportFailure(error, 'Failed to update. Please try again.', generation);
|
|
563
|
+
emit('editError', item, error);
|
|
528
564
|
}
|
|
529
565
|
});
|
|
530
566
|
} else {
|
|
531
567
|
// No editUrl - just emit event for custom handling
|
|
532
|
-
emit('rowUpdated',
|
|
568
|
+
emit('rowUpdated', item, form.data);
|
|
533
569
|
showEditModal.value = false;
|
|
534
570
|
selectedItem.value = null;
|
|
535
571
|
}
|
|
536
572
|
} catch (error) {
|
|
537
|
-
emit('editError',
|
|
573
|
+
if (!errorReported) emit('editError', item, error);
|
|
538
574
|
}
|
|
539
575
|
};
|
|
540
576
|
|
|
@@ -566,6 +602,11 @@ export function useResourceEditor<T = any>(
|
|
|
566
602
|
|
|
567
603
|
pendingAction.value = 'delete';
|
|
568
604
|
const generation = editGeneration.value;
|
|
605
|
+
// Captured for the same reasons as in `performSave`: a late response
|
|
606
|
+
// names the row it deleted and closes only its own session's modal.
|
|
607
|
+
const item = selectedItem.value as T;
|
|
608
|
+
const form = editForm.value;
|
|
609
|
+
let deleteErrorReported = false;
|
|
569
610
|
try {
|
|
570
611
|
// Delete guard: a non-null message means this item can't be deleted —
|
|
571
612
|
// show it immediately and skip the confirm and the request entirely.
|
|
@@ -583,10 +624,10 @@ export function useResourceEditor<T = any>(
|
|
|
583
624
|
// event loop, so nothing can slip in during it.
|
|
584
625
|
if (editGeneration.value !== generation) return;
|
|
585
626
|
|
|
586
|
-
const itemId = (
|
|
627
|
+
const itemId = (item as any).id;
|
|
587
628
|
const url = props.deleteUrl.replace(':id', itemId);
|
|
588
629
|
|
|
589
|
-
await
|
|
630
|
+
await form.delete(url, {
|
|
590
631
|
onSuccess: (data: any) => {
|
|
591
632
|
// Show success toast
|
|
592
633
|
createToast?.({
|
|
@@ -596,33 +637,25 @@ export function useResourceEditor<T = any>(
|
|
|
596
637
|
modelValue: 3000, // Auto-dismiss after 3 seconds
|
|
597
638
|
});
|
|
598
639
|
|
|
599
|
-
emit('rowDeleted',
|
|
600
|
-
|
|
601
|
-
|
|
640
|
+
emit('rowDeleted', item, data);
|
|
641
|
+
if (editGeneration.value === generation) {
|
|
642
|
+
showEditModal.value = false;
|
|
643
|
+
selectedItem.value = null;
|
|
644
|
+
}
|
|
602
645
|
|
|
603
646
|
// Refresh table data to remove deleted item
|
|
604
647
|
refresh();
|
|
605
648
|
},
|
|
606
649
|
onError: (error: any) => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
// Show error toast with server message
|
|
612
|
-
createToast?.({
|
|
613
|
-
title: 'Error',
|
|
614
|
-
body: errorMessage,
|
|
615
|
-
variant: 'danger',
|
|
616
|
-
modelValue: 5000, // Auto-dismiss after 5 seconds
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
emit('deleteError', selectedItem.value as T, error);
|
|
650
|
+
reportFailure(error, 'Failed to delete. Please try again.', generation);
|
|
651
|
+
deleteErrorReported = true;
|
|
652
|
+
emit('deleteError', item, error);
|
|
620
653
|
}
|
|
621
654
|
});
|
|
622
655
|
} catch (error) {
|
|
623
|
-
emit('deleteError',
|
|
656
|
+
if (!deleteErrorReported) emit('deleteError', item, error);
|
|
624
657
|
} finally {
|
|
625
|
-
pendingAction.value = null;
|
|
658
|
+
if (editGeneration.value === generation) pendingAction.value = null;
|
|
626
659
|
}
|
|
627
660
|
};
|
|
628
661
|
|
package/resources/js/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from 'bootstrap-vue-next';
|
|
|
7
7
|
// Extended components (custom functionality beyond Bootstrap Vue Next)
|
|
8
8
|
export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
|
|
9
9
|
export { default as DXForm } from "./components/extended/DXForm.vue";
|
|
10
|
+
export { default as DXFormErrorSummary } from "./components/extended/DXFormErrorSummary.vue";
|
|
10
11
|
export { default as DXField } from "./components/extended/DXField.vue";
|
|
11
12
|
export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
|
|
12
13
|
export { default as DXModalActions } from "./components/extended/DXModalActions.vue";
|
|
@@ -20,6 +21,7 @@ export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
|
|
|
20
21
|
// (#142) so chart.js / vue-chartjs stay genuinely optional peers — the main
|
|
21
22
|
// entry no longer references them. See resources/js/charts.ts.
|
|
22
23
|
export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
|
|
24
|
+
export { default as DXStackingTable } from "./components/extended/DXStackingTable.vue";
|
|
23
25
|
/**
|
|
24
26
|
* @deprecated Use `DXForm`. `DXBasicForm` is a thin wrapper around `DXForm`
|
|
25
27
|
* (a flat form is just `DXForm` without a `tabs` prop) that logs a one-time
|
|
@@ -129,6 +131,12 @@ export {
|
|
|
129
131
|
isFieldVisible,
|
|
130
132
|
isSubmittableField,
|
|
131
133
|
} from "./utils/formSchema";
|
|
134
|
+
// Which field a validation-error key belongs to (#194): the one rule the
|
|
135
|
+
// error summary, tab selection and scrolling share.
|
|
136
|
+
export {
|
|
137
|
+
resolveErrorTargets,
|
|
138
|
+
humaniseErrorKey,
|
|
139
|
+
} from "./utils/formErrorTargets";
|
|
132
140
|
|
|
133
141
|
// Types
|
|
134
142
|
export type {
|
|
@@ -167,8 +175,16 @@ export type {
|
|
|
167
175
|
FormSubmitOptions,
|
|
168
176
|
FormState,
|
|
169
177
|
UseFormReturn,
|
|
178
|
+
SubmitFailure,
|
|
170
179
|
} from "./composables/useForm";
|
|
171
180
|
|
|
181
|
+
export type {
|
|
182
|
+
ErrorTarget,
|
|
183
|
+
ResolveErrorTargetsOptions,
|
|
184
|
+
} from "./utils/formErrorTargets";
|
|
185
|
+
|
|
186
|
+
export type { ErrorSummarySelection } from "./components/extended/DXFormErrorSummary.vue";
|
|
187
|
+
|
|
172
188
|
export type {
|
|
173
189
|
FormFieldDefinition,
|
|
174
190
|
DefineFormReturn,
|
|
@@ -202,4 +218,5 @@ export type {
|
|
|
202
218
|
Navigation,
|
|
203
219
|
NavbarSearchAlign,
|
|
204
220
|
NavbarActionsOnMobile,
|
|
221
|
+
SidebarVariant,
|
|
205
222
|
} from "./types/navigation";
|
|
@@ -367,6 +367,18 @@ export interface FieldDefinition {
|
|
|
367
367
|
*/
|
|
368
368
|
submit?: boolean;
|
|
369
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Extra validation-error keys this field owns (#194), for a field whose
|
|
372
|
+
* server-side errors arrive under keys other than its own: a span or
|
|
373
|
+
* component field that edits nested data, say. Patterns, where `*`
|
|
374
|
+
* matches exactly one dot segment: `["lines.*", "lines.*.*"]` claims
|
|
375
|
+
* `lines.0` and `lines.0.price` but not `lines` or `lines.0.a.b`.
|
|
376
|
+
* Checked before the usual matching (the field's own key, then any key
|
|
377
|
+
* nested under it), so it decides which field an error belongs to: the
|
|
378
|
+
* label it is listed under in the error summary and the tab it selects.
|
|
379
|
+
*/
|
|
380
|
+
errorKeys?: string[];
|
|
381
|
+
|
|
370
382
|
/**
|
|
371
383
|
* Conditionally show or hide this field. When omitted the field is
|
|
372
384
|
* always visible. Boolean or a function of the form model; evaluated
|
|
@@ -427,6 +439,7 @@ export interface BaseFieldDef {
|
|
|
427
439
|
readonly?: MaybeFn<boolean>;
|
|
428
440
|
plaintext?: MaybeFn<boolean>;
|
|
429
441
|
submit?: boolean;
|
|
442
|
+
errorKeys?: string[];
|
|
430
443
|
when?: MaybeFn<boolean>;
|
|
431
444
|
show?: () => boolean;
|
|
432
445
|
}
|
|
@@ -38,6 +38,15 @@ export type Navigation = NavigationGroup[];
|
|
|
38
38
|
*/
|
|
39
39
|
export type NavbarSearchAlign = "start" | "center";
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Colour scheme of the dashboard sidebar: `"light"` (the default: a white pane,
|
|
43
|
+
* tinted group headers, a green active item, Poppins) or `"dark"` (the navy
|
|
44
|
+
* rail dfl shipped before). Every colour comes from a `--dx-sidebar-*` custom
|
|
45
|
+
* property, so either can be rebranded in CSS. Shared by DXDashboard (which
|
|
46
|
+
* forwards it as `sidebarVariant`) and DXDashboardSidebar (`variant`).
|
|
47
|
+
*/
|
|
48
|
+
export type SidebarVariant = "light" | "dark";
|
|
49
|
+
|
|
41
50
|
/**
|
|
42
51
|
* What the navbar actions slot does below the `md` breakpoint: `"wrap"` moves
|
|
43
52
|
* it to its own full-width row (the bar grows), `"hide"` removes it entirely
|