@omnitend/dashboard-for-laravel 0.38.0 → 0.40.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.
Files changed (110) hide show
  1. package/dist/dashboard-for-laravel.js +3841 -3804
  2. package/dist/dashboard-for-laravel.js.map +1 -1
  3. package/dist/dashboard-for-laravel.umd.cjs +5 -5
  4. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  5. package/dist/style.css +2 -2
  6. package/docs/public/api-reference.json +25 -6
  7. package/docs/public/docs-map.md +1 -1
  8. package/package.json +1 -1
  9. package/resources/css/theme.scss +5 -1
  10. package/resources/js/components/extended/DXCurrencyInput.vue +23 -1
  11. package/resources/js/components/extended/DXSwitch.vue +6 -4
  12. package/resources/js/components/extended/DXTable.vue +44 -4
  13. package/resources/js/composables/useResourceEditor.ts +102 -21
  14. package/dist/charts.d.ts +0 -19
  15. package/dist/components/base/DAccordion.vue.d.ts +0 -12
  16. package/dist/components/base/DAccordionItem.vue.d.ts +0 -12
  17. package/dist/components/base/DAlert.vue.d.ts +0 -17
  18. package/dist/components/base/DAspect.vue.d.ts +0 -12
  19. package/dist/components/base/DAutocomplete.vue.d.ts +0 -32
  20. package/dist/components/base/DAvatar.vue.d.ts +0 -12
  21. package/dist/components/base/DBadge.vue.d.ts +0 -12
  22. package/dist/components/base/DBreadcrumb.vue.d.ts +0 -12
  23. package/dist/components/base/DButton.vue.d.ts +0 -88
  24. package/dist/components/base/DButtonGroup.vue.d.ts +0 -12
  25. package/dist/components/base/DButtonToolbar.vue.d.ts +0 -12
  26. package/dist/components/base/DCard.vue.d.ts +0 -12
  27. package/dist/components/base/DCol.vue.d.ts +0 -12
  28. package/dist/components/base/DCollapse.vue.d.ts +0 -12
  29. package/dist/components/base/DContainer.vue.d.ts +0 -12
  30. package/dist/components/base/DDropdown.vue.d.ts +0 -12
  31. package/dist/components/base/DDropdownDivider.vue.d.ts +0 -2
  32. package/dist/components/base/DDropdownItem.vue.d.ts +0 -12
  33. package/dist/components/base/DForm.vue.d.ts +0 -12
  34. package/dist/components/base/DFormCheckbox.vue.d.ts +0 -12
  35. package/dist/components/base/DFormCheckboxGroup.vue.d.ts +0 -12
  36. package/dist/components/base/DFormGroup.vue.d.ts +0 -12
  37. package/dist/components/base/DFormInput.vue.d.ts +0 -2
  38. package/dist/components/base/DFormInvalidFeedback.vue.d.ts +0 -12
  39. package/dist/components/base/DFormOtp.vue.d.ts +0 -12
  40. package/dist/components/base/DFormRadio.vue.d.ts +0 -12
  41. package/dist/components/base/DFormRadioGroup.vue.d.ts +0 -12
  42. package/dist/components/base/DFormSelect.vue.d.ts +0 -27
  43. package/dist/components/base/DFormSpinbutton.vue.d.ts +0 -12
  44. package/dist/components/base/DFormTags.vue.d.ts +0 -12
  45. package/dist/components/base/DFormText.vue.d.ts +0 -12
  46. package/dist/components/base/DFormTextarea.vue.d.ts +0 -2
  47. package/dist/components/base/DImage.vue.d.ts +0 -12
  48. package/dist/components/base/DInputGroup.vue.d.ts +0 -12
  49. package/dist/components/base/DLink.vue.d.ts +0 -12
  50. package/dist/components/base/DListGroup.vue.d.ts +0 -12
  51. package/dist/components/base/DListGroupItem.vue.d.ts +0 -12
  52. package/dist/components/base/DModal.vue.d.ts +0 -12
  53. package/dist/components/base/DNav.vue.d.ts +0 -12
  54. package/dist/components/base/DNavItem.vue.d.ts +0 -12
  55. package/dist/components/base/DNavbar.vue.d.ts +0 -12
  56. package/dist/components/base/DNavbarBrand.vue.d.ts +0 -12
  57. package/dist/components/base/DNavbarNav.vue.d.ts +0 -12
  58. package/dist/components/base/DNavbarToggle.vue.d.ts +0 -12
  59. package/dist/components/base/DOffcanvas.vue.d.ts +0 -12
  60. package/dist/components/base/DOverlay.vue.d.ts +0 -12
  61. package/dist/components/base/DPagination.vue.d.ts +0 -2
  62. package/dist/components/base/DPlaceholder.vue.d.ts +0 -12
  63. package/dist/components/base/DPopover.vue.d.ts +0 -12
  64. package/dist/components/base/DProgress.vue.d.ts +0 -12
  65. package/dist/components/base/DRow.vue.d.ts +0 -12
  66. package/dist/components/base/DSpinner.vue.d.ts +0 -2
  67. package/dist/components/base/DTable.vue.d.ts +0 -34
  68. package/dist/components/base/DTabs.vue.d.ts +0 -12
  69. package/dist/components/base/DToast.vue.d.ts +0 -12
  70. package/dist/components/base/DToaster.vue.d.ts +0 -12
  71. package/dist/components/base/DTooltip.vue.d.ts +0 -12
  72. package/dist/components/charts/DXBarChart.vue.d.ts +0 -7
  73. package/dist/components/charts/DXDoughnutChart.vue.d.ts +0 -24
  74. package/dist/components/charts/DXLineChart.vue.d.ts +0 -7
  75. package/dist/components/charts/chartTheme.d.ts +0 -59
  76. package/dist/components/charts/useThemedChart.d.ts +0 -40
  77. package/dist/components/extended/DXBasicForm.vue.d.ts +0 -10
  78. package/dist/components/extended/DXChoiceField.vue.d.ts +0 -26
  79. package/dist/components/extended/DXCurrencyInput.vue.d.ts +0 -27
  80. package/dist/components/extended/DXDashboard.vue.d.ts +0 -105
  81. package/dist/components/extended/DXDashboardNavbar.vue.d.ts +0 -86
  82. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +0 -61
  83. package/dist/components/extended/DXField.vue.d.ts +0 -86
  84. package/dist/components/extended/DXFieldLabel.vue.d.ts +0 -18
  85. package/dist/components/extended/DXFieldShell.vue.d.ts +0 -73
  86. package/dist/components/extended/DXForm.vue.d.ts +0 -112
  87. package/dist/components/extended/DXFormField.vue.d.ts +0 -41
  88. package/dist/components/extended/DXNumericField.vue.d.ts +0 -23
  89. package/dist/components/extended/DXRepeater.vue.d.ts +0 -46
  90. package/dist/components/extended/DXRepeaterCards.vue.d.ts +0 -45
  91. package/dist/components/extended/DXStatCard.vue.d.ts +0 -56
  92. package/dist/components/extended/DXSwitch.vue.d.ts +0 -46
  93. package/dist/components/extended/DXTable.vue.d.ts +0 -466
  94. package/dist/components/extended/DXTableEditorModal.vue.d.ts +0 -100
  95. package/dist/components/extended/DXTablePagination.vue.d.ts +0 -50
  96. package/dist/components/extended/DXTableShell.vue.d.ts +0 -24
  97. package/dist/components/extended/DXUserAvatar.vue.d.ts +0 -40
  98. package/dist/components/extended/dxFieldContext.d.ts +0 -11
  99. package/dist/composables/defineForm.d.ts +0 -41
  100. package/dist/composables/useAsyncOptions.d.ts +0 -20
  101. package/dist/composables/useContainerWidth.d.ts +0 -96
  102. package/dist/composables/useForm.d.ts +0 -46
  103. package/dist/composables/useResourceEditor.d.ts +0 -75
  104. package/dist/composables/useToast.d.ts +0 -2
  105. package/dist/index.d.ts +0 -93
  106. package/dist/types/index.d.ts +0 -477
  107. package/dist/types/navigation.d.ts +0 -43
  108. package/dist/utils/api.d.ts +0 -30
  109. package/dist/utils/formSchema.d.ts +0 -59
  110. package/dist/utils/objectPath.d.ts +0 -18
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-23T18:28:14.921Z",
2
+ "generated": "2026-08-25T19:01:45.537Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.38.0"
5
+ "version": "0.40.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -1478,7 +1478,7 @@
1478
1478
  "name": "DXCurrencyInput",
1479
1479
  "category": "extended",
1480
1480
  "filePath": "resources/js/components/extended/DXCurrencyInput.vue",
1481
- "description": "DXCurrencyInput — the money input leaf (#152). A currency-symbol-prefixed numeric input with the display/model split from #69: the MODEL is always a plain number (or null), while the displayed text is padded to the minor-unit precision on blur/seed (`3.8` -> `3.80`) and never reformatted mid-edit. Clearing the input emits `null` — never `NaN`, never `\"\"` — so \"no value\" is one predictable shape (#152). `minorUnits` (#116) edits an integer-minor-unit model (pence) as major units (pounds): display = model / 10^decimals, emit = round(input * 10^decimals). `min`/`max`/`step` and the displayed text are always in MAJOR units — only the model is scaled. DXField's `currency` field type renders this leaf; it exists standalone so a money input outside a form (an inline table correction, a quick filter) doesn't re-inline the `£`-prefix + toFixed + parse foot-guns.",
1481
+ "description": "DXCurrencyInput — the money input leaf (#152). A currency-symbol-prefixed numeric input with the display/model split from #69: the MODEL is always a plain number (or null), while the displayed text is padded to the minor-unit precision on blur/seed (`3.8` -> `3.80`) and never reformatted mid-edit. Clearing the input emits `null` — never `NaN`, never `\"\"` — so \"no value\" is one predictable shape (#152). `minorUnits` (#116) edits an integer-minor-unit model (pence) as major units (pounds): display = model / 10^decimals, emit = round(input * 10^decimals). `min`/`max`/`step` and the displayed text are always in MAJOR units — only the model is scaled. `append` puts a trailing affix after the input — the unit the price is per (\"Pint\", \"per case\", \"each\"). Without it a consumer needing a unit beside the amount has to hand-roll a `DInputGroup` + bare number input, which puts every foot-gun above straight back. A plain string covers most cases; the `#append` slot takes arbitrary content (a unit picker, a button) and wins over the prop, the same prop-with-slot-fallback shape `DXFieldShell` uses for `hint`. DXField's `currency` field type renders this leaf; it exists standalone so a money input outside a form (an inline table correction, a quick filter) doesn't re-inline the `£`-prefix + toFixed + parse foot-guns. On a DXField, reach the affix through the field's `inputProps` (`{ append: 'per case' }`) — it binds to this prop like any other.",
1482
1482
  "props": [
1483
1483
  {
1484
1484
  "name": "modelValue",
@@ -1514,6 +1514,13 @@
1514
1514
  "required": false,
1515
1515
  "default": "undefined",
1516
1516
  "description": "Input step in MAJOR units (default: one minor unit, e.g. 0.01)."
1517
+ },
1518
+ {
1519
+ "name": "append",
1520
+ "type": "string",
1521
+ "required": false,
1522
+ "default": "undefined",
1523
+ "description": "Trailing affix shown after the input — the unit the amount is per\n (\"Pint\", \"per case\", \"each\"). Omit for no trailing cell. The `#append`\n slot overrides it when the affix needs to be more than a string."
1517
1524
  }
1518
1525
  ],
1519
1526
  "events": [
@@ -1523,7 +1530,13 @@
1523
1530
  "properties": []
1524
1531
  }
1525
1532
  ],
1526
- "slots": [],
1533
+ "slots": [
1534
+ {
1535
+ "name": "append",
1536
+ "description": "Trailing content after the input — arbitrary markup, overriding the `append` prop.",
1537
+ "bindings": []
1538
+ }
1539
+ ],
1527
1540
  "methods": []
1528
1541
  },
1529
1542
  {
@@ -3097,7 +3110,7 @@
3097
3110
  "name": "size",
3098
3111
  "type": "\"sm\"",
3099
3112
  "required": false,
3100
- "description": "`\"sm\"` renders a compact box (~31px vs the default ~35px input height) for\ndense rows that repeat the switch per item (a per-line visibility toggle in\na modal). Omit for the standard size, which matches the `.form-control`\nheight so a switch lines up with inputs beside it."
3113
+ "description": "`\"sm\"` renders a compact box (a few px shorter than the default, which\nmatches the input height) for dense rows that repeat the switch per item (a\nper-line visibility toggle in a modal). Omit for the standard size, which\nlines the switch up with the `.form-control`\nheight so a switch lines up with inputs beside it."
3101
3114
  }
3102
3115
  ],
3103
3116
  "events": [
@@ -3414,7 +3427,13 @@
3414
3427
  "name": "deleteGuard",
3415
3428
  "type": "TSFunctionType",
3416
3429
  "required": false,
3417
- "description": "Guard run when Delete is clicked, before the confirm dialog and request.\nReturn a message for a non-deletable item to show it immediately (as a\ntoast) and skip both the confirm and the delete request; return\n`null`/`undefined` to proceed with the normal confirm + delete. Lets you\nshort-circuit a doomed delete (e.g. a record with dependents that the\nserver would reject) with an immediate, specific reason."
3430
+ "description": "Guard run when Delete is clicked, before the confirm dialog and request.\nReturn a message for a non-deletable item to show it immediately (as a\ntoast) and skip both the confirm and the delete request; return\n`null`/`undefined` to proceed with the normal confirm + delete. Lets you\nshort-circuit a doomed delete (e.g. a record with dependents that the\nserver would reject) with an immediate, specific reason.\n\nAWAITED, so it may return a promise — the same shape as `saveGuard`, so\nan `async` guard behaves the way its author expects. A guard that throws\nalso aborts, surfacing its message."
3431
+ },
3432
+ {
3433
+ "name": "saveGuard",
3434
+ "type": "TSFunctionType",
3435
+ "required": false,
3436
+ "description": "Guard run when the modal's Save/Create is clicked, before the request.\nReturn a message to abort the save — it is shown immediately (as a toast)\nand no request is made; return `null`/`undefined` to proceed. Covers both\nthe `editUrl` and `createUrl` paths.\n\nAWAITED (as `deleteGuard` is), so it may return a promise: the case it\nexists for is an async control in an `edit-value` slot — an image\nupload — that has not finished when Save is clicked. Without the guard the\nform submits the previous media map, the save SUCCEEDS, the toast says\nsaved and the modal closes: the image is lost with no error anywhere,\nbecause every individual step worked. Await the upload here and the save\nsimply waits for it.\n\n`item` is the row being edited, `null` in create mode (as in the\n`edit-value` slot's binding); `data` is the live form data. Aborting\nleaves the modal open with the user's edits intact. A guard that throws\nalso aborts, surfacing its message."
3418
3437
  },
3419
3438
  {
3420
3439
  "name": "createUrl",
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-23T18:28:14.969Z
4
+ > Last updated: 2026-08-25T19:01:45.587Z
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -48,7 +48,11 @@ $color-contrast-light: #fff; // Light text color (for dark backgrounds)
48
48
  // ----------------------------------------------------------------------------
49
49
 
50
50
  $font-family-base: "Poppins", sans-serif;
51
- $font-size-base: 0.875rem; // 14px
51
+ $font-size-base: 1rem; // 16px — the default tier. Legacy omnitend rendered body
52
+ // and table text at 16px; the earlier 0.875rem (14px) shrank every ported page's
53
+ // text ~12% while rem-fixed chrome stayed put, so controls read oversized and
54
+ // text miniaturised (PO20). The small tier is Bootstrap's `.875em` (14px) via
55
+ // `.small`/`<small>`; the extra-small tier is `.75em` (12px, badges/fine print).
52
56
  $font-weight-base: 400;
53
57
  $line-height-base: 1.5;
54
58
 
@@ -12,9 +12,18 @@
12
12
  `min`/`max`/`step` and the displayed text are always in MAJOR units — only
13
13
  the model is scaled.
14
14
 
15
+ `append` puts a trailing affix after the input — the unit the price is per
16
+ ("Pint", "per case", "each"). Without it a consumer needing a unit beside the
17
+ amount has to hand-roll a `DInputGroup` + bare number input, which puts every
18
+ foot-gun above straight back. A plain string covers most cases; the `#append`
19
+ slot takes arbitrary content (a unit picker, a button) and wins over the prop,
20
+ the same prop-with-slot-fallback shape `DXFieldShell` uses for `hint`.
21
+
15
22
  DXField's `currency` field type renders this leaf; it exists standalone so a
16
23
  money input outside a form (an inline table correction, a quick filter)
17
- doesn't re-inline the `£`-prefix + toFixed + parse foot-guns.
24
+ doesn't re-inline the `£`-prefix + toFixed + parse foot-guns. On a DXField,
25
+ reach the affix through the field's `inputProps` (`{ append: 'per case' }`) —
26
+ it binds to this prop like any other.
18
27
  -->
19
28
  <template>
20
29
  <DInputGroup>
@@ -30,6 +39,14 @@
30
39
  @focus="handleFocus"
31
40
  @blur="handleBlur"
32
41
  />
42
+ <!-- Trailing affix. Conditional so an input with no unit renders the
43
+ exact same markup as before (one prepend, no trailing cell). -->
44
+ <template v-if="append || $slots.append" #append>
45
+ <!-- @slot Trailing content after the input — arbitrary markup, overriding the `append` prop. -->
46
+ <slot name="append">
47
+ <span class="input-group-text">{{ append }}</span>
48
+ </slot>
49
+ </template>
33
50
  </DInputGroup>
34
51
  </template>
35
52
 
@@ -54,6 +71,10 @@ interface Props {
54
71
  minorUnits?: boolean;
55
72
  /** Input step in MAJOR units (default: one minor unit, e.g. 0.01). */
56
73
  step?: string | number;
74
+ /** Trailing affix shown after the input — the unit the amount is per
75
+ * ("Pint", "per case", "each"). Omit for no trailing cell. The `#append`
76
+ * slot overrides it when the affix needs to be more than a string. */
77
+ append?: string;
57
78
  }
58
79
 
59
80
  const props = withDefaults(defineProps<Props>(), {
@@ -62,6 +83,7 @@ const props = withDefaults(defineProps<Props>(), {
62
83
  decimals: 2,
63
84
  minorUnits: false,
64
85
  step: undefined,
86
+ append: undefined,
65
87
  });
66
88
 
67
89
  const emit = defineEmits<{
@@ -64,9 +64,10 @@ interface Props {
64
64
  */
65
65
  onVariant?: "success" | "neutral";
66
66
  /**
67
- * `"sm"` renders a compact box (~31px vs the default ~35px input height) for
68
- * dense rows that repeat the switch per item (a per-line visibility toggle in
69
- * a modal). Omit for the standard size, which matches the `.form-control`
67
+ * `"sm"` renders a compact box (a few px shorter than the default, which
68
+ * matches the input height) for dense rows that repeat the switch per item (a
69
+ * per-line visibility toggle in a modal). Omit for the standard size, which
70
+ * lines the switch up with the `.form-control`
70
71
  * height so a switch lines up with inputs beside it.
71
72
  */
72
73
  size?: "sm";
@@ -140,7 +141,8 @@ defineOptions({
140
141
 
141
142
  /* Compact size (#… size="sm"): a shorter box for dense rows that repeat the
142
143
  switch per item. `min-height: auto` drops the input-height floor so the box
143
- shrinks to its padded content (~31px vs ~35px); the smaller font pulls the
144
+ shrinks to its padded content, a few px under the input-height default;
145
+ the smaller font pulls the
144
146
  label in to match. The fixed-rem toggle itself is unchanged — only the box
145
147
  chrome tightens. */
146
148
  .dx-switch--sm :deep(.form-check) {
@@ -725,8 +725,38 @@ export interface Props<TItem = any> {
725
725
  * `null`/`undefined` to proceed with the normal confirm + delete. Lets you
726
726
  * short-circuit a doomed delete (e.g. a record with dependents that the
727
727
  * server would reject) with an immediate, specific reason.
728
+ *
729
+ * AWAITED, so it may return a promise — the same shape as `saveGuard`, so
730
+ * an `async` guard behaves the way its author expects. A guard that throws
731
+ * also aborts, surfacing its message.
732
+ */
733
+ deleteGuard?: (
734
+ item: TItem,
735
+ ) => string | null | undefined | Promise<string | null | undefined>;
736
+
737
+ /**
738
+ * Guard run when the modal's Save/Create is clicked, before the request.
739
+ * Return a message to abort the save — it is shown immediately (as a toast)
740
+ * and no request is made; return `null`/`undefined` to proceed. Covers both
741
+ * the `editUrl` and `createUrl` paths.
742
+ *
743
+ * AWAITED (as `deleteGuard` is), so it may return a promise: the case it
744
+ * exists for is an async control in an `edit-value` slot — an image
745
+ * upload — that has not finished when Save is clicked. Without the guard the
746
+ * form submits the previous media map, the save SUCCEEDS, the toast says
747
+ * saved and the modal closes: the image is lost with no error anywhere,
748
+ * because every individual step worked. Await the upload here and the save
749
+ * simply waits for it.
750
+ *
751
+ * `item` is the row being edited, `null` in create mode (as in the
752
+ * `edit-value` slot's binding); `data` is the live form data. Aborting
753
+ * leaves the modal open with the user's edits intact. A guard that throws
754
+ * also aborts, surfacing its message.
728
755
  */
729
- deleteGuard?: (item: TItem) => string | null | undefined;
756
+ saveGuard?: (
757
+ item: TItem | null,
758
+ data: Record<string, any>,
759
+ ) => string | null | undefined | Promise<string | null | undefined>;
730
760
 
731
761
  /** API endpoint for creating new items (e.g., "/api/products") — enables "New" button */
732
762
  createUrl?: string;
@@ -2182,10 +2212,20 @@ const handleFilterChange = (fieldKey: string, value: string | string[]) => {
2182
2212
  );
2183
2213
  }
2184
2214
 
2185
- // For API mode, provider will be called automatically by BTable
2186
- // when we trigger a refresh
2215
+ // API mode: a changed filter is a fresh result set — go back to page 1,
2216
+ // exactly as the Inertia branch above does. Without this, typing a
2217
+ // filter while on page 3 of the unfiltered set requests page 3 of the
2218
+ // NARROWED set, which is usually past its end: the server returns an
2219
+ // empty page and the table shows "no rows match your filters" over a
2220
+ // non-zero total. Same exactly-one-fetch dance as the apiUrl/provider
2221
+ // swap watchers: a page change re-invokes the provider on its own;
2222
+ // only when already on page 1 does refresh() need to force it.
2187
2223
  if (isProviderMode.value && tableRef.value) {
2188
- refresh();
2224
+ const wasOnFirstPage = apiCurrentPage.value === 1;
2225
+ apiCurrentPage.value = 1;
2226
+ if (wasOnFirstPage) {
2227
+ refresh();
2228
+ }
2189
2229
  }
2190
2230
 
2191
2231
  // Emit filterChange event for backward compatibility
@@ -30,8 +30,15 @@ export interface ResourceEditorProps<T = any> {
30
30
  deleteUrl?: string;
31
31
  /** `POST` endpoint for creating new items, e.g. `/api/products`. */
32
32
  createUrl?: string;
33
- /** Guard run before delete — a message short-circuits with a toast. */
34
- deleteGuard?: (item: T) => string | null | undefined;
33
+ /** Guard run before delete — a message short-circuits with a toast. Awaited. */
34
+ deleteGuard?: (
35
+ item: T,
36
+ ) => string | null | undefined | Promise<string | null | undefined>;
37
+ /** Guard run before save — a message aborts with a toast. Awaited. */
38
+ saveGuard?: (
39
+ item: T | null,
40
+ data: Record<string, any>,
41
+ ) => string | null | undefined | Promise<string | null | undefined>;
35
42
  }
36
43
 
37
44
  /**
@@ -299,18 +306,96 @@ export function useResourceEditor<T = any>(
299
306
  showEditModal.value = true;
300
307
  };
301
308
 
302
- // Save from the edit modal
309
+ // Save from the edit modal (create or edit).
303
310
  const save = async () => {
304
311
  if (!editForm.value) return;
305
312
 
313
+ // Don't save — or evaluate the guard — until the full record has loaded
314
+ // (showUrl). The thin list row is missing fields the form and the guard
315
+ // may both depend on, so a guard keyed on one of them would see
316
+ // `undefined` and wave the save through, which is the silent success the
317
+ // guard exists to prevent. Same rule as `remove`.
318
+ if (editLoading.value) return;
319
+
320
+ // Not re-entrant. The footer button disables itself while
321
+ // `pendingAction` is set, but DXForm's own submit (Enter in a field)
322
+ // reaches here directly — and with an AWAITED save guard there is now a
323
+ // real window in which a second Save would fire a second request.
324
+ if (pendingAction.value) return;
325
+
306
326
  pendingAction.value = 'save';
307
327
  try {
328
+ if (!(await passesSaveGuard())) return;
308
329
  await performSave();
309
330
  } finally {
310
331
  pendingAction.value = null;
311
332
  }
312
333
  };
313
334
 
335
+ /*
336
+ * BOTH guards run through here, which is the point of it: they must not
337
+ * diverge. Each is AWAITED. The case `saveGuard` exists for is a control
338
+ * that has not finished yet — an image upload still in flight when Save is
339
+ * clicked. The form would submit the previous media map, the request would
340
+ * SUCCEED, the toast would say saved and the modal would close: the image
341
+ * lost with no error anywhere, because every individual step worked.
342
+ * `deleteGuard` is awaited on the same path so an `async` guard does what
343
+ * its author expects — unawaited, its promise is merely truthy, which would
344
+ * block every delete with an unreadable message.
345
+ *
346
+ * Returns true to proceed. Aborting leaves the modal open and the form
347
+ * untouched — the user's edits are the thing being protected, so nothing is
348
+ * closed or reset on the way out. A guard that THROWS also aborts: a guard
349
+ * that could not decide is not permission to proceed.
350
+ */
351
+ const passesGuard = async (
352
+ run: () => string | null | undefined | Promise<string | null | undefined>,
353
+ title: string,
354
+ fallbackMessage: string,
355
+ ): Promise<boolean> => {
356
+ let message: string | null | undefined;
357
+ try {
358
+ message = await run();
359
+ } catch (error: any) {
360
+ message = error?.message ?? fallbackMessage;
361
+ }
362
+ if (!message) return true;
363
+
364
+ createToast?.({
365
+ title,
366
+ body: message,
367
+ variant: 'danger',
368
+ modelValue: 5000,
369
+ });
370
+ return false;
371
+ };
372
+
373
+ const passesSaveGuard = (): Promise<boolean> => {
374
+ const guard = props.saveGuard;
375
+ if (!guard) return Promise.resolve(true);
376
+
377
+ return passesGuard(
378
+ () =>
379
+ guard(
380
+ isCreateMode.value ? null : (selectedItem.value as T | null),
381
+ editForm.value.data,
382
+ ),
383
+ 'Cannot save',
384
+ 'Could not save. Please try again.',
385
+ );
386
+ };
387
+
388
+ const passesDeleteGuard = (): Promise<boolean> => {
389
+ const guard = props.deleteGuard;
390
+ if (!guard) return Promise.resolve(true);
391
+
392
+ return passesGuard(
393
+ () => guard(selectedItem.value as T),
394
+ 'Cannot delete',
395
+ 'Could not delete. Please try again.',
396
+ );
397
+ };
398
+
314
399
  const performSave = async () => {
315
400
  // Create mode: POST to createUrl
316
401
  if (isCreateMode.value && props.createUrl) {
@@ -441,27 +526,23 @@ export function useResourceEditor<T = any>(
441
526
  // may depend on fields the thin list row doesn't carry.
442
527
  if (editLoading.value) return;
443
528
 
444
- // Delete guard: a non-null message means this item can't be deleted — show
445
- // it immediately and skip the confirm and the request entirely.
446
- const guardMessage = props.deleteGuard?.(selectedItem.value as T);
447
- if (guardMessage) {
448
- createToast?.({
449
- title: 'Cannot delete',
450
- body: guardMessage,
451
- variant: 'danger',
452
- modelValue: 5000,
453
- });
454
- return;
455
- }
456
-
457
- // Confirm deletion
458
- const itemName = (selectedItem.value as any).name || (selectedItem.value as any).title || singularItemName.value;
459
- const confirmed = window.confirm(`Are you sure you want to delete "${itemName}"? This action cannot be undone.`);
460
-
461
- if (!confirmed) return;
529
+ // Not re-entrant, for the same reason as `save`: the guard is awaited,
530
+ // so a second Delete arriving by a path that doesn't disable the button
531
+ // would run the guard — and the confirm — a second time.
532
+ if (pendingAction.value) return;
462
533
 
463
534
  pendingAction.value = 'delete';
464
535
  try {
536
+ // Delete guard: a non-null message means this item can't be deleted —
537
+ // show it immediately and skip the confirm and the request entirely.
538
+ if (!(await passesDeleteGuard())) return;
539
+
540
+ // Confirm deletion
541
+ const itemName = (selectedItem.value as any).name || (selectedItem.value as any).title || singularItemName.value;
542
+ const confirmed = window.confirm(`Are you sure you want to delete "${itemName}"? This action cannot be undone.`);
543
+
544
+ if (!confirmed) return;
545
+
465
546
  const itemId = (selectedItem.value as any).id;
466
547
  const url = props.deleteUrl.replace(':id', itemId);
467
548
 
package/dist/charts.d.ts DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * Chart entry point — `@omnitend/dashboard-for-laravel/charts` (#142).
3
- *
4
- * The chart components are the ONLY ones that need `chart.js` / `vue-chartjs`,
5
- * so they live behind this separate subpath entry. The main entry
6
- * (`@omnitend/dashboard-for-laravel`) never references the chart libraries, so
7
- * they are genuinely OPTIONAL peers: a consumer that renders no charts installs
8
- * neither and their build no longer fails resolving them.
9
- *
10
- * import { DXBarChart } from '@omnitend/dashboard-for-laravel/charts';
11
- *
12
- * Chart containers are styled by `.dx-chart` in the main stylesheet
13
- * (`@omnitend/dashboard-for-laravel/style.css`), which chart consumers already
14
- * import for the theme and the `--dx-chart-*` palette variables — so this entry
15
- * ships no CSS of its own.
16
- */
17
- export { default as DXBarChart } from "./components/charts/DXBarChart.vue";
18
- export { default as DXLineChart } from "./components/charts/DXLineChart.vue";
19
- export { default as DXDoughnutChart } from "./components/charts/DXDoughnutChart.vue";
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: any;
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: any;
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };
@@ -1,17 +0,0 @@
1
- type __VLS_Props = {
2
- modelValue?: boolean | number;
3
- };
4
- declare var __VLS_7: string | number, __VLS_8: {};
5
- type __VLS_Slots = {} & {
6
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
7
- };
8
- declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
9
- modelValue: boolean | number;
10
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
12
- export default _default;
13
- type __VLS_WithSlots<T, S> = T & {
14
- new (): {
15
- $slots: S;
16
- };
17
- };
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: any;
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };
@@ -1,32 +0,0 @@
1
- import type { SelectOption } from "bootstrap-vue-next";
2
- type AutocompleteOptions = readonly (object | string | number | boolean)[];
3
- type FilterFunction = (options: SelectOption[], search: string) => SelectOption[];
4
- interface Props {
5
- /** Options passed straight through to BAutocomplete (see its `options` prop). */
6
- options?: AutocompleteOptions;
7
- /**
8
- * A custom filter. Composed with `null-option` rather than replaced: when a
9
- * null option is present it stays pinned and this runs on the rest.
10
- */
11
- filterFunction?: FilterFunction;
12
- /**
13
- * Prepend a pinned, never-filtered, selectable "no selection" row whose value
14
- * is `null` (#138). `true` → default label "None"; a string → that label.
15
- * Selecting it sets the model to `null` (bvn reads that as no selection, so
16
- * the ✕ hides and the placeholder shows). Off by default — unchanged
17
- * behaviour. DXTable does NOT use this; it builds its own "All" option.
18
- */
19
- nullOption?: boolean | string;
20
- }
21
- declare var __VLS_6: string | number, __VLS_7: any;
22
- type __VLS_Slots = {} & {
23
- [K in NonNullable<typeof __VLS_6>]?: (props: typeof __VLS_7) => any;
24
- };
25
- declare const __VLS_component: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
27
- export default _default;
28
- type __VLS_WithSlots<T, S> = T & {
29
- new (): {
30
- $slots: S;
31
- };
32
- };
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: any;
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: {};
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };
@@ -1,12 +0,0 @@
1
- declare var __VLS_7: string | number, __VLS_8: any;
2
- type __VLS_Slots = {} & {
3
- [K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };