@omnitend/dashboard-for-laravel 0.27.0 → 0.29.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 (42) hide show
  1. package/dist/charts.d.ts +19 -0
  2. package/dist/charts.js +225 -0
  3. package/dist/charts.js.map +1 -0
  4. package/dist/charts.umd.cjs +2 -0
  5. package/dist/charts.umd.cjs.map +1 -0
  6. package/dist/components/base/DAlert.vue.d.ts +6 -1
  7. package/dist/components/base/DFormCheckboxGroup.vue.d.ts +12 -0
  8. package/dist/components/charts/chartTheme.d.ts +1 -0
  9. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +2 -2
  10. package/dist/components/extended/DXField.vue.d.ts +4 -4
  11. package/dist/components/extended/DXForm.vue.d.ts +1 -1
  12. package/dist/components/extended/DXTable.vue.d.ts +9 -44
  13. package/dist/components/extended/DXTableEditorModal.vue.d.ts +80 -0
  14. package/dist/components/extended/DXTablePagination.vue.d.ts +27 -0
  15. package/dist/composables/useResourceEditor.d.ts +74 -0
  16. package/dist/dashboard-for-laravel.js +12463 -12819
  17. package/dist/dashboard-for-laravel.js.map +1 -1
  18. package/dist/dashboard-for-laravel.umd.cjs +8 -8
  19. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  20. package/dist/index.d.ts +1 -3
  21. package/dist/style.css +1 -1
  22. package/dist/types/index.d.ts +1 -1
  23. package/docs/public/api-reference.json +224 -9
  24. package/docs/public/docs-map.md +2 -2
  25. package/docs/public/llms.txt +3 -2
  26. package/package.json +12 -6
  27. package/resources/css/theme.scss +49 -7
  28. package/resources/js/charts.ts +19 -0
  29. package/resources/js/components/base/DAlert.vue +19 -1
  30. package/resources/js/components/base/DFormCheckboxGroup.vue +22 -0
  31. package/resources/js/components/charts/DXBarChart.vue +1 -10
  32. package/resources/js/components/charts/DXDoughnutChart.vue +1 -10
  33. package/resources/js/components/charts/DXLineChart.vue +1 -10
  34. package/resources/js/components/charts/chartTheme.ts +19 -16
  35. package/resources/js/components/extended/DXField.vue +12 -0
  36. package/resources/js/components/extended/DXTable.vue +161 -1034
  37. package/resources/js/components/extended/DXTableEditorModal.vue +247 -0
  38. package/resources/js/components/extended/DXTablePagination.vue +126 -0
  39. package/resources/js/composables/useForm.ts +10 -1
  40. package/resources/js/composables/useResourceEditor.ts +542 -0
  41. package/resources/js/index.ts +4 -3
  42. package/resources/js/types/index.ts +1 -0
@@ -0,0 +1,247 @@
1
+ <!--
2
+ @component
3
+ Internal edit/create modal for `DXTable` (#129). Renders the `DModal` +
4
+ `DXForm` and maps DXTable's `edit-value`/`edit-span`/`tab-*` slots onto
5
+ DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close`
6
+ handle. Extracted so `DXTable` is a thin façade over the table render, the
7
+ pagination, the editor logic (`useResourceEditor`) and this modal — rather than
8
+ owning all four.
9
+
10
+ The edit-modal slots reach here forwarded from DXTable (consumer → DXTable →
11
+ here → DXForm). This component computes which keyed slots the consumer actually
12
+ provided from its OWN `$slots`, so the slot-plumbing lives entirely with the
13
+ modal, not the composable.
14
+ -->
15
+ <template>
16
+ <DModal
17
+ :model-value="show"
18
+ :title="title"
19
+ :size="size"
20
+ @update:model-value="emit('update:show', $event)"
21
+ >
22
+ <!-- Loading the full record for edit (showUrl fetch) -->
23
+ <div
24
+ v-if="loading"
25
+ class="dx-edit-loading d-flex align-items-center gap-2 text-muted mb-3"
26
+ >
27
+ <DSpinner small />
28
+ <span>Loading…</span>
29
+ </div>
30
+
31
+ <!-- Edit/create form (tabbed when tabs provided, flat otherwise).
32
+
33
+ Keyed per modal open: the form OBJECT is created once and then
34
+ reseeded in place for each row, so without a key Vue reuses the
35
+ same DXField instances across records — and any per-field UI
36
+ state rides along with them. That leaked a revealed password
37
+ from one row into the next. The key gives each record a fresh
38
+ field subtree; the data still lives in `form`, so nothing
39
+ is lost by remounting. -->
40
+ <DXForm
41
+ v-if="form"
42
+ :key="formInstanceKey"
43
+ :active-tab="activeTab"
44
+ :form="form"
45
+ :fields="fields"
46
+ :tabs="tabs"
47
+ :context="item ?? undefined"
48
+ :show-submit="false"
49
+ @update:active-tab="emit('update:activeTab', $event)"
50
+ @submit="emit('save')"
51
+ >
52
+ <!-- Forward DXTable's edit-value(key) → DXForm value(key) -->
53
+ <template
54
+ v-for="key in editValueSlotKeys"
55
+ :key="`ev-${key}`"
56
+ #[`value(${key})`]="sp"
57
+ >
58
+ <!--
59
+ @slot Custom input for field `<key>` in the edit/create modal, forwarded to DXForm. Name it `edit-value(<fieldKey>)`.
60
+ @binding {object} item The row being edited (null in create mode).
61
+ @binding {any} value The current field value.
62
+ @binding {Function} update Call with a new value to update the field.
63
+ @binding {object} field The field definition.
64
+ -->
65
+ <slot
66
+ :name="`edit-value(${key})`"
67
+ :item="item"
68
+ :value="sp.value"
69
+ :update="sp.update"
70
+ :field="sp.field"
71
+ />
72
+ </template>
73
+
74
+ <!-- Forward edit-span(key) → span(key) -->
75
+ <template
76
+ v-for="key in editSpanSlotKeys"
77
+ :key="`es-${key}`"
78
+ #[`span(${key})`]="sp"
79
+ >
80
+ <!--
81
+ @slot Full-width custom content for field `<key>` in the edit/create modal, forwarded to DXForm's span slot. Name it `edit-span(<fieldKey>)`.
82
+ @binding {object} item The row being edited (null in create mode).
83
+ @binding {any} value The current field value.
84
+ @binding {Function} update Call with a new value to update the field.
85
+ @binding {Function} close Call to close the edit modal.
86
+ -->
87
+ <slot
88
+ :name="`edit-span(${key})`"
89
+ :item="item"
90
+ :value="sp.value"
91
+ :update="sp.update"
92
+ :close="close"
93
+ />
94
+ </template>
95
+
96
+ <!-- Forward tab-content / tab-before / tab-after slots -->
97
+ <template
98
+ v-for="key in tabContentSlotKeys"
99
+ :key="`tc-${key}`"
100
+ #[`tab-content(${key})`]="sp"
101
+ >
102
+ <!--
103
+ @slot Replaces the auto-rendered fields of edit-modal tab `<key>` with custom content. Name it `tab-content(<tabKey>)`.
104
+ @binding {object} item The row being edited (null in create mode).
105
+ @binding {object} tab The tab definition.
106
+ -->
107
+ <slot :name="`tab-content(${key})`" :item="item" :tab="sp.tab" />
108
+ </template>
109
+ <template
110
+ v-for="key in tabBeforeSlotKeys"
111
+ :key="`tb-${key}`"
112
+ #[`tab-before(${key})`]="sp"
113
+ >
114
+ <!--
115
+ @slot Custom content rendered before the fields of edit-modal tab `<key>`. Name it `tab-before(<tabKey>)`.
116
+ @binding {object} item The row being edited (null in create mode).
117
+ @binding {object} tab The tab definition.
118
+ -->
119
+ <slot :name="`tab-before(${key})`" :item="item" :tab="sp.tab" />
120
+ </template>
121
+ <template
122
+ v-for="key in tabAfterSlotKeys"
123
+ :key="`taf-${key}`"
124
+ #[`tab-after(${key})`]="sp"
125
+ >
126
+ <!--
127
+ @slot Custom content rendered after the fields of edit-modal tab `<key>`. Name it `tab-after(<tabKey>)`.
128
+ @binding {object} item The row being edited (null in create mode).
129
+ @binding {object} tab The tab definition.
130
+ -->
131
+ <slot :name="`tab-after(${key})`" :item="item" :tab="sp.tab" />
132
+ </template>
133
+ </DXForm>
134
+
135
+ <template #footer>
136
+ <div class="d-flex justify-content-between w-100">
137
+ <div>
138
+ <DButton
139
+ v-if="deleteUrl && !isCreateMode"
140
+ variant="danger"
141
+ :loading="pendingAction === 'delete'"
142
+ loading-text="Deleting..."
143
+ :disabled="form?.processing || loading"
144
+ @click="emit('delete')"
145
+ >
146
+ Delete
147
+ </DButton>
148
+ </div>
149
+ <div class="d-flex gap-2">
150
+ <DButton variant="secondary" @click="close">
151
+ Cancel
152
+ </DButton>
153
+ <DButton
154
+ variant="primary"
155
+ :loading="pendingAction === 'save'"
156
+ :loading-text="isCreateMode ? 'Creating...' : 'Saving...'"
157
+ :disabled="form?.processing || loading"
158
+ @click="emit('save')"
159
+ >
160
+ {{ isCreateMode ? 'Create' : 'Save Changes' }}
161
+ </DButton>
162
+ </div>
163
+ </div>
164
+ </template>
165
+ </DModal>
166
+ </template>
167
+
168
+ <script setup lang="ts">
169
+ import { computed, useSlots } from "vue";
170
+ import DModal from "../base/DModal.vue";
171
+ import DButton from "../base/DButton.vue";
172
+ import DSpinner from "../base/DSpinner.vue";
173
+ import DXForm from "./DXForm.vue";
174
+ import type { EditTab } from "./DXTable.vue";
175
+
176
+ interface Props {
177
+ /** Whether the modal is open (v-model:show). */
178
+ show: boolean;
179
+ /** Modal title. */
180
+ title: string;
181
+ /** Modal size. */
182
+ size?: 'sm' | 'md' | 'lg' | 'xl';
183
+ /** True while the full record is loading via showUrl. */
184
+ loading: boolean;
185
+ /** The edit/create form (a useForm instance), or null before first open. */
186
+ form: any;
187
+ /** Bumped per modal open to remount the DXForm subtree (fresh DXField state). */
188
+ formInstanceKey: number;
189
+ /** Active tab index (v-model:active-tab). */
190
+ activeTab: number;
191
+ /** Field definitions passed to DXForm. */
192
+ fields: any[];
193
+ /** Tab definitions passed to DXForm. */
194
+ tabs?: EditTab[];
195
+ /** The row being edited (null in create mode) — the slot/context row. */
196
+ item: any;
197
+ /** Whether the modal is in create mode (vs edit). */
198
+ isCreateMode: boolean;
199
+ /** Which action is in flight, so Save/Delete show their own loading label. */
200
+ pendingAction: 'save' | 'delete' | null;
201
+ /** Delete endpoint — gates the Delete button. */
202
+ deleteUrl?: string;
203
+ }
204
+
205
+ const props = defineProps<Props>();
206
+
207
+ const emit = defineEmits<{
208
+ /** v-model:show — the modal open state changed. */
209
+ 'update:show': [value: boolean];
210
+ /** v-model:active-tab — the active tab changed. */
211
+ 'update:activeTab': [value: number];
212
+ /** Save was requested (footer button or DXForm submit). */
213
+ save: [];
214
+ /** Cancel/close was requested. */
215
+ cancel: [];
216
+ /** Delete was requested. */
217
+ delete: [];
218
+ }>();
219
+
220
+ // Close the modal — shared by the Cancel button and the `close` binding handed
221
+ // to consumers' edit-span slots.
222
+ const close = () => emit('cancel');
223
+
224
+ // Which keyed slots the consumer actually provided (computed from THIS
225
+ // component's slots — they're forwarded here from DXTable). Forwarding only the
226
+ // provided ones stops DXForm mistaking an always-present wrapper for a real
227
+ // custom-value override.
228
+ const slots = useSlots();
229
+ const fieldKeys = computed<string[]>(() => (props.fields ?? []).map((field: any) => field.key));
230
+ const tabKeys = computed<string[]>(() => (props.tabs ?? []).map((tab) => tab.key));
231
+
232
+ const editValueSlotKeys = computed(() =>
233
+ fieldKeys.value.filter((key) => !!slots[`edit-value(${key})`]),
234
+ );
235
+ const editSpanSlotKeys = computed(() =>
236
+ fieldKeys.value.filter((key) => !!slots[`edit-span(${key})`]),
237
+ );
238
+ const tabContentSlotKeys = computed(() =>
239
+ tabKeys.value.filter((key) => !!slots[`tab-content(${key})`]),
240
+ );
241
+ const tabBeforeSlotKeys = computed(() =>
242
+ tabKeys.value.filter((key) => !!slots[`tab-before(${key})`]),
243
+ );
244
+ const tabAfterSlotKeys = computed(() =>
245
+ tabKeys.value.filter((key) => !!slots[`tab-after(${key})`]),
246
+ );
247
+ </script>
@@ -0,0 +1,126 @@
1
+ <!--
2
+ @component
3
+ Internal pagination + per-page footer for `DXTable`. Renders the pager, the
4
+ per-page selector, and the "X to Y out of Z items" info line (with the
5
+ "Filtered from N" note when a column filter is active).
6
+
7
+ Extracted because `DXTable` used to carry three byte-identical copies of this
8
+ footer — one per data mode (client-side / inertia / provider) — that differed
9
+ only in which `PaginationData` object and page-change handler they used, so
10
+ every footer fix had to be written three times (#123). `DXTable` now derives
11
+ the active pagination + handler and renders this once.
12
+
13
+ It owns its own `:deep(.pagination …)` styling (rather than leaving it in
14
+ `DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager
15
+ once it lives in a child component — the scope-id doesn't cross the boundary.
16
+ Anchoring the rules on this component's own `.dx-table-pagination` root (a
17
+ plain element it owns) gives the scope-id a deterministic host.
18
+ -->
19
+ <template>
20
+ <div class="dx-table-pagination mt-3">
21
+ <!-- Top row: Pagination and Per-page selector -->
22
+ <div class="d-flex justify-content-between align-items-center mb-2">
23
+ <!-- Pagination controls (only when multiple pages) -->
24
+ <DPagination
25
+ v-if="showPagination && pagination.total > pagination.per_page"
26
+ :model-value="pagination.current_page"
27
+ :total-rows="pagination.total"
28
+ :per-page="pagination.per_page"
29
+ size="sm"
30
+ @update:model-value="emit('page-change', $event as number)"
31
+ />
32
+ <div v-else></div>
33
+
34
+ <!-- Per-page selector -->
35
+ <div v-if="showPerPageSelector" class="d-flex align-items-center gap-2">
36
+ <label for="perPageSelect" class="mb-0 small text-muted">Per page</label>
37
+ <DFormSelect
38
+ id="perPageSelect"
39
+ :model-value="perPage"
40
+ :options="perPageOptions.map(n => ({ value: n, text: n.toString() }))"
41
+ size="sm"
42
+ style="width: 85px;"
43
+ @update:model-value="emit('per-page-change', $event)"
44
+ />
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Bottom row: Info text -->
49
+ <div class="small text-muted">
50
+ <div>
51
+ <template v-if="pagination.total > pagination.per_page">
52
+ {{ pagination.from }} to {{ pagination.to }} out of {{ pagination.total }} {{ pagination.total === 1 ? singularItemName : pluralItemName }}.
53
+ </template>
54
+ <template v-else-if="pagination.total === 1">
55
+ {{ pagination.total }} {{ singularItemName }}.
56
+ </template>
57
+ <template v-else>
58
+ {{ pagination.total }} {{ pluralItemName }}.
59
+ </template>
60
+ </div>
61
+ <div v-if="hasActiveFilters && pagination.total_unfiltered">
62
+ <small>Filtered from {{ pagination.total_unfiltered }} {{ pagination.total_unfiltered === 1 ? singularItemName : pluralItemName }}.</small>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import DPagination from "../base/DPagination.vue";
70
+ import DFormSelect from "../base/DFormSelect.vue";
71
+ import type { PaginationData } from "./DXTable.vue";
72
+
73
+ interface Props {
74
+ /** The active pagination metadata (client-side / inertia / provider). */
75
+ pagination: PaginationData;
76
+ /** The effective page size, shown in the selector. */
77
+ perPage: number;
78
+ /** Page sizes offered by the selector. */
79
+ perPageOptions: number[];
80
+ /** Whether to render the pager (still hidden when there is only one page). */
81
+ showPagination: boolean;
82
+ /** Whether to render the per-page selector. */
83
+ showPerPageSelector: boolean;
84
+ /** Singular item noun for the info line (e.g. "product"). */
85
+ singularItemName: string;
86
+ /** Plural item noun for the info line (e.g. "products"). */
87
+ pluralItemName: string;
88
+ /** Whether a column filter is active — gates the "Filtered from N" note. */
89
+ hasActiveFilters: boolean;
90
+ }
91
+
92
+ defineProps<Props>();
93
+
94
+ const emit = defineEmits<{
95
+ /** The user picked a page. Payload is the new 1-based page number. */
96
+ 'page-change': [page: number];
97
+ /**
98
+ * The per-page selector changed. Payload is the raw DFormSelect model value
99
+ * (the parent's `handlePerPageChange` narrows it — DFormSelect mirrors BVN's
100
+ * model, which can be a scalar, null, or array).
101
+ */
102
+ 'per-page-change': [value: number | string | null | (number | string)[]];
103
+ }>();
104
+ </script>
105
+
106
+ <style scoped>
107
+ /* Pagination button sizing to match form controls. Anchored on this component's
108
+ own `.dx-table-pagination` root so the scope-id has a plain-element host — a
109
+ `:deep()` rule that targeted the pager from DXTable would not reach it once
110
+ the pager lives here (the scope-id doesn't cross the component boundary). */
111
+ .dx-table-pagination :deep(.pagination) {
112
+ margin-bottom: 0;
113
+ }
114
+
115
+ .dx-table-pagination :deep(.pagination-sm .page-link) {
116
+ min-width: 2.25rem;
117
+ height: auto;
118
+ }
119
+
120
+ /* Make disabled pagination buttons more subtle */
121
+ .dx-table-pagination :deep(.pagination .page-item.disabled .page-link) {
122
+ background-color: transparent;
123
+ border-color: transparent;
124
+ opacity: 0.3;
125
+ }
126
+ </style>
@@ -255,7 +255,16 @@ export function useForm<TData extends Record<string, any>>(
255
255
 
256
256
  const payloadRaw = (
257
257
  options.transform
258
- ? options.transform(state.data as TData)
258
+ ? // Give `transform` a COPY of the form data (#150), so a transform
259
+ // that MUTATES what it receives — `data.allergens = assemble();
260
+ // return data` — shapes the outbound payload WITHOUT corrupting
261
+ // form state. (This is why consumers reach for a transform instead
262
+ // of mutating `form.data` in a validation guard.) A shallow copy:
263
+ // returning a new object is still the tidiest style, and a
264
+ // deeply-nested field should be replaced, not mutated in place.
265
+ options.transform({
266
+ ...(toRaw(state.data) as Record<string, any>),
267
+ } as TData)
259
268
  : (state.data as TData)
260
269
  ) as any;
261
270