@omnitend/dashboard-for-laravel 0.4.8 → 0.4.10
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/DXTable.vue.d.ts +4 -0
- package/dist/dashboard-for-laravel.js +6036 -5828
- 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/style.css +1 -1
- package/docs/public/api-reference.json +2 -2
- package/docs/public/docs-map.md +17 -6
- package/docs/public/llms.txt +2 -0
- package/package.json +1 -1
- package/resources/js/components/extended/DXTable.vue +324 -5
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2025-
|
|
4
|
+
> Last updated: 2025-12-02T13:23:09.125Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
|
@@ -12,6 +12,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
12
12
|
### Base
|
|
13
13
|
|
|
14
14
|
- [DAccordion](/components/base/DAccordion): Type-safe wrapper around Bootstrap Vue Next's BAccordion component
|
|
15
|
+
- [DAccordionItem](/components/base/DAccordionItem): Type-safe wrapper around Bootstrap Vue Next's BAccordionItem component
|
|
15
16
|
- [DAlert](/components/base/DAlert): Type-safe wrapper around Bootstrap Vue Next's BAlert component
|
|
16
17
|
- [DAvatar](/components/base/DAvatar): Type-safe wrapper around Bootstrap Vue Next's BAvatar component
|
|
17
18
|
- [DBadge](/components/base/DBadge): Type-safe wrapper around Bootstrap Vue Next's BBadge component
|
|
@@ -21,6 +22,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
21
22
|
- [DButtonToolbar](/components/base/DButtonToolbar): Type-safe wrapper around Bootstrap Vue Next's BButtonToolbar component
|
|
22
23
|
- [DCard](/components/base/DCard): Type-safe wrapper around Bootstrap Vue Next's BCard component
|
|
23
24
|
- [DCarousel](/components/base/DCarousel): Type-safe wrapper around Bootstrap Vue Next's BCarousel component
|
|
25
|
+
- [DCarouselSlide](/components/base/DCarouselSlide): Type-safe wrapper around Bootstrap Vue Next's BCarouselSlide component
|
|
24
26
|
- [DCol](/components/base/DCol): Type-safe wrapper around Bootstrap Vue Next's BCol component
|
|
25
27
|
- [DCollapse](/components/base/DCollapse): Type-safe wrapper around Bootstrap Vue Next's BCollapse component
|
|
26
28
|
- [DContainer](/components/base/DContainer): Type-safe wrapper around Bootstrap Vue Next's BContainer component
|
|
@@ -31,19 +33,25 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
31
33
|
- [DFormCheckbox](/components/base/DFormCheckbox): Type-safe wrapper around Bootstrap Vue Next's BFormCheckbox component
|
|
32
34
|
- [DFormGroup](/components/base/DFormGroup): Type-safe wrapper around Bootstrap Vue Next's BFormGroup component
|
|
33
35
|
- [DFormInput](/components/base/DFormInput): Type-safe wrapper around Bootstrap Vue Next's BFormInput component
|
|
36
|
+
- [DFormInvalidFeedback](/components/base/DFormInvalidFeedback): Type-safe wrapper around Bootstrap Vue Next's BFormInvalidFeedback component
|
|
34
37
|
- [DFormRadio](/components/base/DFormRadio): Type-safe wrapper around Bootstrap Vue Next's BFormRadio component
|
|
35
38
|
- [DFormSelect](/components/base/DFormSelect): Type-safe wrapper around Bootstrap Vue Next's BFormSelect component
|
|
36
39
|
- [DFormSpinbutton](/components/base/DFormSpinbutton): Type-safe wrapper around Bootstrap Vue Next's BFormSpinbutton component
|
|
37
40
|
- [DFormTags](/components/base/DFormTags): Type-safe wrapper around Bootstrap Vue Next's BFormTags component
|
|
41
|
+
- [DFormText](/components/base/DFormText): Type-safe wrapper around Bootstrap Vue Next's BFormText component
|
|
38
42
|
- [DFormTextarea](/components/base/DFormTextarea): Type-safe wrapper around Bootstrap Vue Next's BFormTextarea component
|
|
39
43
|
- [DImage](/components/base/DImage): Type-safe wrapper around Bootstrap Vue Next's BImage component
|
|
40
44
|
- [DInputGroup](/components/base/DInputGroup): Type-safe wrapper around Bootstrap Vue Next's BInputGroup component
|
|
41
45
|
- [DLink](/components/base/DLink): Type-safe wrapper around Bootstrap Vue Next's BLink component
|
|
42
46
|
- [DListGroup](/components/base/DListGroup): Type-safe wrapper around Bootstrap Vue Next's BListGroup component
|
|
47
|
+
- [DListGroupItem](/components/base/DListGroupItem): Type-safe wrapper around Bootstrap Vue Next's BListGroupItem component
|
|
43
48
|
- [DModal](/components/base/DModal): Type-safe wrapper around Bootstrap Vue Next's BModal component
|
|
44
49
|
- [DNav](/components/base/DNav): Type-safe wrapper around Bootstrap Vue Next's BNav component
|
|
45
50
|
- [DNavItem](/components/base/DNavItem): DNavItem - A type-safe wrapper around Bootstrap Vue Next's BNavItem component. Individual navigation
|
|
46
51
|
- [DNavbar](/components/base/DNavbar): Type-safe wrapper around Bootstrap Vue Next BNavbar component. Responsive navigation header with sup
|
|
52
|
+
- [DNavbarBrand](/components/base/DNavbarBrand): Type-safe wrapper around Bootstrap Vue Next's BNavbarBrand component
|
|
53
|
+
- [DNavbarNav](/components/base/DNavbarNav): Type-safe wrapper around Bootstrap Vue Next's BNavbarNav component
|
|
54
|
+
- [DNavbarToggle](/components/base/DNavbarToggle): Type-safe wrapper around Bootstrap Vue Next's BNavbarToggle component
|
|
47
55
|
- [DOffcanvas](/components/base/DOffcanvas): DOffcanvas - A type-safe wrapper around Bootstrap Vue Next's BOffcanvas component. Hidden sidebar co
|
|
48
56
|
- [DOverlay](/components/base/DOverlay): Type-safe wrapper around Bootstrap Vue Next BOverlay component. Overlay component for indicating loa
|
|
49
57
|
- [DPagination](/components/base/DPagination): DPagination - A type-safe wrapper around Bootstrap Vue Next's BPagination component. Provides naviga
|
|
@@ -52,6 +60,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
52
60
|
- [DProgress](/components/base/DProgress): Type-safe wrapper around Bootstrap Vue Next BProgress component. Progress bar for displaying simple
|
|
53
61
|
- [DRow](/components/base/DRow): DRow - A type-safe wrapper around Bootstrap Vue Next's BRow component. Container for columns within
|
|
54
62
|
- [DSpinner](/components/base/DSpinner): DSpinner - A type-safe wrapper around Bootstrap Vue Next's BSpinner component. Loading indicator to
|
|
63
|
+
- [DTab](/components/base/DTab): Type-safe wrapper around Bootstrap Vue Next's BTab component
|
|
55
64
|
- [DTable](/components/base/DTable): DTable - A type-safe wrapper around Bootstrap Vue Next's BTable component. Feature-rich data table w
|
|
56
65
|
- [DTabs](/components/base/DTabs): Type-safe wrapper around Bootstrap Vue Next BTabs component. Create tabbed panes of local content wi
|
|
57
66
|
- [DToast](/components/base/DToast): DToast - A type-safe wrapper around Bootstrap Vue Next's BToast component. Lightweight notification
|
|
@@ -65,7 +74,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
65
74
|
- [DXDashboardNavbar](/components/extended/DXDashboardNavbar): A responsive top navigation bar component for Laravel dashboards with user menu, search functionalit
|
|
66
75
|
- [DXDashboardSidebar](/components/extended/DXDashboardSidebar): A collapsible sidebar navigation component for Laravel dashboards with support for navigation groups
|
|
67
76
|
- [DXForm](/components/extended/DXForm): Form object from defineForm
|
|
68
|
-
- [DXTable](/components/extended/DXTable): A comprehensive data table component with built-in pagination,
|
|
77
|
+
- [DXTable](/components/extended/DXTable): A comprehensive data table component with built-in pagination, sorting, filtering, and CRUD operatio
|
|
69
78
|
|
|
70
79
|
### Examples
|
|
71
80
|
|
|
@@ -73,13 +82,15 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
73
82
|
|
|
74
83
|
### Guide
|
|
75
84
|
|
|
76
|
-
- [
|
|
77
|
-
- [
|
|
78
|
-
- [
|
|
85
|
+
- [AI Integration](/guide/ai-integration): This library includes an MCP (Model Context Protocol) server that provides AI agents with structured
|
|
86
|
+
- [Contributing](/guide/contributing): This library is open source and welcomes contributions. Start here for setup, testing, and how to su
|
|
87
|
+
- [Forms](/guide/forms): The library includes a type-safe form system with validation, auto-generated forms, and composables.
|
|
88
|
+
- [Getting Started](/guide/getting-started): > **Alpha Notice:** v0.x — expect changes between minor versions.
|
|
89
|
+
- [Installation](/guide/installation): ```bash
|
|
79
90
|
- [Theming](/guide/theming): The library includes a custom Bootstrap 5 theme that can be easily customised using CSS variables an
|
|
80
91
|
- [TypeScript](/guide/typescript): The library is written in TypeScript and includes full type definitions for all components, composab
|
|
81
92
|
|
|
82
93
|
|
|
83
94
|
---
|
|
84
95
|
|
|
85
|
-
**Total Pages**:
|
|
96
|
+
**Total Pages**: 69
|
package/docs/public/llms.txt
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
## Getting Started
|
|
13
13
|
|
|
14
|
+
- [AI Integration](/guide/ai-integration): Documentation guide
|
|
15
|
+
- [Contributing](/guide/contributing): Documentation guide
|
|
14
16
|
- [Forms](/guide/forms): Type-safe form handling and validation
|
|
15
17
|
- [Getting Started](/guide/getting-started): Quick start guide and core concepts
|
|
16
18
|
- [Installation](/guide/installation): Install and configure the package
|
package/package.json
CHANGED
|
@@ -114,9 +114,98 @@
|
|
|
114
114
|
</template>
|
|
115
115
|
</DTable>
|
|
116
116
|
|
|
117
|
+
<!-- Client-Side Mode: Local filtering, sorting, pagination -->
|
|
118
|
+
<DTable
|
|
119
|
+
v-else-if="isClientSideMode"
|
|
120
|
+
:items="clientSidePaginatedItems"
|
|
121
|
+
:fields="fields"
|
|
122
|
+
:sort-by="effectiveSortBy"
|
|
123
|
+
:multisort="false"
|
|
124
|
+
:no-local-sorting="true"
|
|
125
|
+
:no-sortable-icon="true"
|
|
126
|
+
:striped="striped"
|
|
127
|
+
:hover="hover"
|
|
128
|
+
:responsive="responsive"
|
|
129
|
+
@update:sort-by="handleSortChange"
|
|
130
|
+
@row-clicked="handleRowClick"
|
|
131
|
+
>
|
|
132
|
+
<!-- Inline Filter Row -->
|
|
133
|
+
<template v-if="hasFilters" #thead-top>
|
|
134
|
+
<tr class="filter-row">
|
|
135
|
+
<th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
|
|
136
|
+
<!-- Text Filter -->
|
|
137
|
+
<DFormInput
|
|
138
|
+
v-if="field.filter === 'text'"
|
|
139
|
+
:model-value="effectiveFilters[field.key] || ''"
|
|
140
|
+
:placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
|
|
141
|
+
size="sm"
|
|
142
|
+
@update:model-value="handleFilterChange(field.key, $event as string)"
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<!-- Select Filter -->
|
|
146
|
+
<DFormSelect
|
|
147
|
+
v-else-if="field.filter === 'select'"
|
|
148
|
+
:model-value="effectiveFilters[field.key] || ''"
|
|
149
|
+
:options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
|
|
150
|
+
size="sm"
|
|
151
|
+
@update:model-value="handleFilterChange(field.key, $event as string)"
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<!-- Number Filter -->
|
|
155
|
+
<DFormInput
|
|
156
|
+
v-else-if="field.filter === 'number'"
|
|
157
|
+
:model-value="effectiveFilters[field.key] || ''"
|
|
158
|
+
:placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
|
|
159
|
+
type="number"
|
|
160
|
+
size="sm"
|
|
161
|
+
@update:model-value="handleFilterChange(field.key, $event as string)"
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<!-- Date Filter -->
|
|
165
|
+
<DFormInput
|
|
166
|
+
v-else-if="field.filter === 'date'"
|
|
167
|
+
:model-value="effectiveFilters[field.key] || ''"
|
|
168
|
+
type="date"
|
|
169
|
+
size="sm"
|
|
170
|
+
@update:model-value="handleFilterChange(field.key, $event as string)"
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<!-- No filter for this column -->
|
|
174
|
+
<div v-else></div>
|
|
175
|
+
</th>
|
|
176
|
+
</tr>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<!-- Custom headers for all fields -->
|
|
180
|
+
<template v-for="field in fields" :key="`head-${field.key}`" #[`head(${field.key})`]="{ label }">
|
|
181
|
+
<div class="d-flex align-items-center justify-content-between gap-2">
|
|
182
|
+
<div class="flex-grow-1">
|
|
183
|
+
<div class="fw-semibold">{{ label || field.key }}</div>
|
|
184
|
+
<small v-if="field.hint" class="text-muted d-block" style="font-weight: normal;">{{ field.hint }}</small>
|
|
185
|
+
</div>
|
|
186
|
+
<div v-if="field.sortable" class="sort-indicator text-muted flex-shrink-0" style="font-size: 0.75rem; line-height: 1.1; display: flex; flex-direction: column; align-items: center;">
|
|
187
|
+
<span :style="{ opacity: getFieldSortState(field.key) === 'asc' ? 1 : 0.3 }">▲</span>
|
|
188
|
+
<span :style="{ opacity: getFieldSortState(field.key) === 'desc' ? 1 : 0.3 }">▼</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
<!-- Pass through all cell slots -->
|
|
194
|
+
<template
|
|
195
|
+
v-for="(_, name) in $slots"
|
|
196
|
+
#[name]="slotProps"
|
|
197
|
+
>
|
|
198
|
+
<slot
|
|
199
|
+
v-if="typeof name === 'string' && name.startsWith('cell')"
|
|
200
|
+
:name="name"
|
|
201
|
+
v-bind="slotProps"
|
|
202
|
+
/>
|
|
203
|
+
</template>
|
|
204
|
+
</DTable>
|
|
205
|
+
|
|
117
206
|
<!-- Inertia Mode: Use items prop -->
|
|
118
207
|
<DTable
|
|
119
|
-
v-else
|
|
208
|
+
v-else-if="isInertiaMode"
|
|
120
209
|
:items="items"
|
|
121
210
|
:fields="fields"
|
|
122
211
|
:sort-by="effectiveSortBy"
|
|
@@ -204,6 +293,54 @@
|
|
|
204
293
|
</template>
|
|
205
294
|
</DTable>
|
|
206
295
|
|
|
296
|
+
<!-- Pagination and Controls (Client-Side mode) -->
|
|
297
|
+
<div v-if="isClientSideMode" class="mt-3">
|
|
298
|
+
<!-- Top row: Pagination and Per-page selector -->
|
|
299
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
300
|
+
<!-- Pagination controls (only when multiple pages) -->
|
|
301
|
+
<DPagination
|
|
302
|
+
v-if="showPagination && clientSidePagination.total > clientSidePagination.per_page"
|
|
303
|
+
:model-value="clientSidePagination.current_page"
|
|
304
|
+
:total-rows="clientSidePagination.total"
|
|
305
|
+
:per-page="clientSidePagination.per_page"
|
|
306
|
+
size="sm"
|
|
307
|
+
@update:model-value="handleClientSidePageChange"
|
|
308
|
+
/>
|
|
309
|
+
<div v-else></div>
|
|
310
|
+
|
|
311
|
+
<!-- Per-page selector -->
|
|
312
|
+
<div v-if="clientSidePagination.total >= Math.min(...perPageOptions)" class="d-flex align-items-center gap-2">
|
|
313
|
+
<label for="perPageSelectClientSide" class="mb-0 small text-muted">Per page</label>
|
|
314
|
+
<DFormSelect
|
|
315
|
+
id="perPageSelectClientSide"
|
|
316
|
+
:model-value="effectivePerPage"
|
|
317
|
+
:options="perPageOptions.map(n => ({ value: n, text: n.toString() }))"
|
|
318
|
+
size="sm"
|
|
319
|
+
style="width: 85px;"
|
|
320
|
+
@update:model-value="handlePerPageChange"
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<!-- Bottom row: Info text -->
|
|
326
|
+
<div class="small text-muted">
|
|
327
|
+
<div>
|
|
328
|
+
<template v-if="clientSidePagination.total > clientSidePagination.per_page">
|
|
329
|
+
{{ clientSidePagination.from }} to {{ clientSidePagination.to }} out of {{ clientSidePagination.total }} {{ clientSidePagination.total === 1 ? singularItemName : pluralItemName }}.
|
|
330
|
+
</template>
|
|
331
|
+
<template v-else-if="clientSidePagination.total === 1">
|
|
332
|
+
{{ clientSidePagination.total }} {{ singularItemName }}.
|
|
333
|
+
</template>
|
|
334
|
+
<template v-else>
|
|
335
|
+
{{ clientSidePagination.total }} {{ pluralItemName }}.
|
|
336
|
+
</template>
|
|
337
|
+
</div>
|
|
338
|
+
<div v-if="hasActiveFilters && clientSidePagination.total_unfiltered">
|
|
339
|
+
<small>Filtered from {{ clientSidePagination.total_unfiltered }} {{ clientSidePagination.total_unfiltered === 1 ? singularItemName : pluralItemName }}.</small>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
207
344
|
<!-- Pagination and Controls (Inertia mode) -->
|
|
208
345
|
<div v-if="isInertiaMode && pagination" class="mt-3">
|
|
209
346
|
<!-- Top row: Pagination and Per-page selector -->
|
|
@@ -383,13 +520,21 @@
|
|
|
383
520
|
v-model="editForm.data[fieldKey]"
|
|
384
521
|
:required="getField(fieldKey).required"
|
|
385
522
|
:rows="getField(fieldKey).rows || 3"
|
|
523
|
+
:state="editForm.getState(fieldKey)"
|
|
524
|
+
@input="editForm.clearError(fieldKey)"
|
|
386
525
|
/>
|
|
387
526
|
<DFormInput
|
|
388
527
|
v-else
|
|
389
528
|
v-model="editForm.data[fieldKey]"
|
|
390
529
|
:type="getField(fieldKey).type || 'text'"
|
|
391
530
|
:required="getField(fieldKey).required"
|
|
531
|
+
:state="editForm.getState(fieldKey)"
|
|
532
|
+
@input="editForm.clearError(fieldKey)"
|
|
392
533
|
/>
|
|
534
|
+
<!-- Validation error -->
|
|
535
|
+
<DFormInvalidFeedback v-if="editForm.hasError(fieldKey)">
|
|
536
|
+
{{ editForm.getError(fieldKey) }}
|
|
537
|
+
</DFormInvalidFeedback>
|
|
393
538
|
</DFormGroup>
|
|
394
539
|
</template>
|
|
395
540
|
|
|
@@ -461,6 +606,7 @@ import DTab from "../base/DTab.vue";
|
|
|
461
606
|
import DFormGroup from "../base/DFormGroup.vue";
|
|
462
607
|
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
463
608
|
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
609
|
+
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
464
610
|
import DXBasicForm from "./DXBasicForm.vue";
|
|
465
611
|
export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
|
|
466
612
|
|
|
@@ -620,6 +766,9 @@ export interface Props<TItem = any> {
|
|
|
620
766
|
|
|
621
767
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
622
768
|
deleteUrl?: string;
|
|
769
|
+
|
|
770
|
+
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
771
|
+
clientSide?: boolean;
|
|
623
772
|
}
|
|
624
773
|
|
|
625
774
|
const props = withDefaults(defineProps<Props<T>>(), {
|
|
@@ -666,10 +815,16 @@ const emit = defineEmits<{
|
|
|
666
815
|
}>();
|
|
667
816
|
|
|
668
817
|
// Mode detection
|
|
669
|
-
const isProviderMode = computed(() => !!props.provider || !!props.apiUrl);
|
|
670
|
-
const isInertiaMode = computed(() => !props.provider && !props.apiUrl && !!props.items);
|
|
818
|
+
const isProviderMode = computed(() => !props.clientSide && (!!props.provider || !!props.apiUrl));
|
|
819
|
+
const isInertiaMode = computed(() => !props.clientSide && !props.provider && !props.apiUrl && !!props.items);
|
|
820
|
+
const isClientSideMode = computed(() => props.clientSide === true && !!props.items);
|
|
671
821
|
const hasInertiaUrl = computed(() => !!props.inertiaUrl);
|
|
672
822
|
|
|
823
|
+
// Warn about invalid prop combinations in client-side mode
|
|
824
|
+
if (props.clientSide && (props.apiUrl || props.inertiaUrl)) {
|
|
825
|
+
console.warn('[DXTable] clientSide mode ignores apiUrl and inertiaUrl props. Data is processed locally from items.');
|
|
826
|
+
}
|
|
827
|
+
|
|
673
828
|
// Computed for effective busy state (provider mode uses 'busy', inertia uses 'loading')
|
|
674
829
|
const effectiveBusy = computed(() => isProviderMode.value ? props.busy : props.loading);
|
|
675
830
|
|
|
@@ -697,6 +852,94 @@ const hasActiveFilters = computed(() => {
|
|
|
697
852
|
// API mode pagination metadata (extracted from responses)
|
|
698
853
|
const apiPaginationMeta = ref<PaginationData | null>(null);
|
|
699
854
|
|
|
855
|
+
// ============================================
|
|
856
|
+
// Client-Side Mode: Filtering, Sorting, Pagination
|
|
857
|
+
// ============================================
|
|
858
|
+
|
|
859
|
+
// Client-side current page
|
|
860
|
+
const clientSideCurrentPage = ref(1);
|
|
861
|
+
|
|
862
|
+
// Client-side filtered items
|
|
863
|
+
const clientSideFilteredItems = computed<T[]>(() => {
|
|
864
|
+
if (!isClientSideMode.value || !props.items) return [];
|
|
865
|
+
|
|
866
|
+
const filters = effectiveFilters.value;
|
|
867
|
+
const filterKeys = Object.keys(filters).filter(key => filters[key] && filters[key].trim() !== '');
|
|
868
|
+
|
|
869
|
+
if (filterKeys.length === 0) {
|
|
870
|
+
return props.items;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return props.items.filter(item => {
|
|
874
|
+
return filterKeys.every(key => {
|
|
875
|
+
const filterValue = filters[key].trim().toLowerCase();
|
|
876
|
+
const field = props.fields.find(f => f.key === key);
|
|
877
|
+
const itemValue = (item as any)[key];
|
|
878
|
+
|
|
879
|
+
if (itemValue === null || itemValue === undefined) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const filterType = field?.filter;
|
|
884
|
+
|
|
885
|
+
switch (filterType) {
|
|
886
|
+
case 'text':
|
|
887
|
+
// Case-insensitive contains search
|
|
888
|
+
return String(itemValue).toLowerCase().includes(filterValue);
|
|
889
|
+
|
|
890
|
+
case 'select':
|
|
891
|
+
// Exact match
|
|
892
|
+
return String(itemValue) === filters[key];
|
|
893
|
+
|
|
894
|
+
case 'number':
|
|
895
|
+
// Exact numeric match
|
|
896
|
+
return Number(itemValue) === Number(filters[key]);
|
|
897
|
+
|
|
898
|
+
case 'date':
|
|
899
|
+
// Exact date match
|
|
900
|
+
return String(itemValue) === filters[key];
|
|
901
|
+
|
|
902
|
+
default:
|
|
903
|
+
// Default: case-insensitive contains
|
|
904
|
+
return String(itemValue).toLowerCase().includes(filterValue);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Client-side sorted items
|
|
911
|
+
const clientSideSortedItems = computed<T[]>(() => {
|
|
912
|
+
if (!isClientSideMode.value) return [];
|
|
913
|
+
|
|
914
|
+
const items = [...clientSideFilteredItems.value];
|
|
915
|
+
const sortBy = effectiveSortBy.value;
|
|
916
|
+
|
|
917
|
+
if (!sortBy || sortBy.length === 0 || !sortBy[0].key) {
|
|
918
|
+
return items;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const { key, order } = sortBy[0];
|
|
922
|
+
const direction = order === 'desc' ? -1 : 1;
|
|
923
|
+
|
|
924
|
+
return items.sort((a, b) => {
|
|
925
|
+
const aVal = (a as any)[key];
|
|
926
|
+
const bVal = (b as any)[key];
|
|
927
|
+
|
|
928
|
+
// Handle null/undefined
|
|
929
|
+
if (aVal == null && bVal == null) return 0;
|
|
930
|
+
if (aVal == null) return direction;
|
|
931
|
+
if (bVal == null) return -direction;
|
|
932
|
+
|
|
933
|
+
// Numeric comparison
|
|
934
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
935
|
+
return (aVal - bVal) * direction;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// String comparison (case-insensitive)
|
|
939
|
+
return String(aVal).localeCompare(String(bVal), undefined, { sensitivity: 'base' }) * direction;
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
700
943
|
// API mode filter values (extracted from responses)
|
|
701
944
|
const apiFilterValues = ref<Record<string, string[]>>({});
|
|
702
945
|
|
|
@@ -819,6 +1062,66 @@ const effectivePerPage = computed(() => {
|
|
|
819
1062
|
return internalPerPage.value;
|
|
820
1063
|
});
|
|
821
1064
|
|
|
1065
|
+
// ============================================
|
|
1066
|
+
// Client-Side Mode: Pagination (requires effectivePerPage)
|
|
1067
|
+
// ============================================
|
|
1068
|
+
|
|
1069
|
+
// Client-side paginated items (final output)
|
|
1070
|
+
const clientSidePaginatedItems = computed<T[]>(() => {
|
|
1071
|
+
if (!isClientSideMode.value) return [];
|
|
1072
|
+
|
|
1073
|
+
const perPage = effectivePerPage.value;
|
|
1074
|
+
const start = (clientSideCurrentPage.value - 1) * perPage;
|
|
1075
|
+
const end = start + perPage;
|
|
1076
|
+
|
|
1077
|
+
return clientSideSortedItems.value.slice(start, end);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Client-side pagination metadata
|
|
1081
|
+
const clientSidePagination = computed<PaginationData>(() => {
|
|
1082
|
+
const total = clientSideFilteredItems.value.length;
|
|
1083
|
+
const totalUnfiltered = props.items?.length || 0;
|
|
1084
|
+
const perPage = effectivePerPage.value;
|
|
1085
|
+
const currentPage = clientSideCurrentPage.value;
|
|
1086
|
+
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
1087
|
+
|
|
1088
|
+
// Ensure current page is valid
|
|
1089
|
+
const validPage = Math.min(Math.max(1, currentPage), lastPage);
|
|
1090
|
+
|
|
1091
|
+
const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
|
|
1092
|
+
const to = Math.min(validPage * perPage, total);
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
current_page: validPage,
|
|
1096
|
+
per_page: perPage,
|
|
1097
|
+
total,
|
|
1098
|
+
total_unfiltered: totalUnfiltered !== total ? totalUnfiltered : undefined,
|
|
1099
|
+
from,
|
|
1100
|
+
to,
|
|
1101
|
+
last_page: lastPage,
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Reset to page 1 when filters change in client-side mode
|
|
1106
|
+
watch(effectiveFilters, () => {
|
|
1107
|
+
if (isClientSideMode.value) {
|
|
1108
|
+
clientSideCurrentPage.value = 1;
|
|
1109
|
+
}
|
|
1110
|
+
}, { deep: true });
|
|
1111
|
+
|
|
1112
|
+
// Reset to page 1 when perPage changes in client-side mode
|
|
1113
|
+
watch(effectivePerPage, () => {
|
|
1114
|
+
if (isClientSideMode.value) {
|
|
1115
|
+
clientSideCurrentPage.value = 1;
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Handle client-side page change
|
|
1120
|
+
const handleClientSidePageChange = (page: number) => {
|
|
1121
|
+
clientSideCurrentPage.value = page;
|
|
1122
|
+
emit('pageChange', page);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
822
1125
|
// Computed: determine if per-page selector should be shown
|
|
823
1126
|
// Hide it when total items is less than the smallest page size option
|
|
824
1127
|
const shouldShowPerPageSelector = computed(() => {
|
|
@@ -1046,6 +1349,13 @@ const handleFilterChange = (fieldKey: string, value: string) => {
|
|
|
1046
1349
|
// Emit v-model update
|
|
1047
1350
|
emit('update:filters', newFilters);
|
|
1048
1351
|
|
|
1352
|
+
// Client-side mode: filtering happens reactively via computed properties
|
|
1353
|
+
// No server requests needed, just emit the event
|
|
1354
|
+
if (isClientSideMode.value) {
|
|
1355
|
+
emit('filterChange', newFilters);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1049
1359
|
// Debounce server requests for text inputs
|
|
1050
1360
|
if (filterDebounceTimer) {
|
|
1051
1361
|
clearTimeout(filterDebounceTimer);
|
|
@@ -1216,10 +1526,19 @@ const handleEditSave = async () => {
|
|
|
1216
1526
|
refresh();
|
|
1217
1527
|
},
|
|
1218
1528
|
onError: (errors: any) => {
|
|
1219
|
-
//
|
|
1529
|
+
// Extract first error message for toast
|
|
1530
|
+
let errorMessage = 'Failed to update. Please check the form for errors.';
|
|
1531
|
+
if (errors && typeof errors === 'object') {
|
|
1532
|
+
const firstError = Object.values(errors).flat()[0];
|
|
1533
|
+
if (typeof firstError === 'string') {
|
|
1534
|
+
errorMessage = firstError;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Show error toast with specific message
|
|
1220
1539
|
createToast?.({
|
|
1221
1540
|
title: 'Error',
|
|
1222
|
-
body:
|
|
1541
|
+
body: errorMessage,
|
|
1223
1542
|
variant: 'danger',
|
|
1224
1543
|
modelValue: 5000, // Auto-dismiss after 5 seconds
|
|
1225
1544
|
});
|