@omnitend/dashboard-for-laravel 0.4.8 → 0.4.9
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 +5094 -4901
- 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 +304 -3
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 -->
|
|
@@ -620,6 +757,9 @@ export interface Props<TItem = any> {
|
|
|
620
757
|
|
|
621
758
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
622
759
|
deleteUrl?: string;
|
|
760
|
+
|
|
761
|
+
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
762
|
+
clientSide?: boolean;
|
|
623
763
|
}
|
|
624
764
|
|
|
625
765
|
const props = withDefaults(defineProps<Props<T>>(), {
|
|
@@ -666,10 +806,16 @@ const emit = defineEmits<{
|
|
|
666
806
|
}>();
|
|
667
807
|
|
|
668
808
|
// Mode detection
|
|
669
|
-
const isProviderMode = computed(() => !!props.provider || !!props.apiUrl);
|
|
670
|
-
const isInertiaMode = computed(() => !props.provider && !props.apiUrl && !!props.items);
|
|
809
|
+
const isProviderMode = computed(() => !props.clientSide && (!!props.provider || !!props.apiUrl));
|
|
810
|
+
const isInertiaMode = computed(() => !props.clientSide && !props.provider && !props.apiUrl && !!props.items);
|
|
811
|
+
const isClientSideMode = computed(() => props.clientSide === true && !!props.items);
|
|
671
812
|
const hasInertiaUrl = computed(() => !!props.inertiaUrl);
|
|
672
813
|
|
|
814
|
+
// Warn about invalid prop combinations in client-side mode
|
|
815
|
+
if (props.clientSide && (props.apiUrl || props.inertiaUrl)) {
|
|
816
|
+
console.warn('[DXTable] clientSide mode ignores apiUrl and inertiaUrl props. Data is processed locally from items.');
|
|
817
|
+
}
|
|
818
|
+
|
|
673
819
|
// Computed for effective busy state (provider mode uses 'busy', inertia uses 'loading')
|
|
674
820
|
const effectiveBusy = computed(() => isProviderMode.value ? props.busy : props.loading);
|
|
675
821
|
|
|
@@ -697,6 +843,94 @@ const hasActiveFilters = computed(() => {
|
|
|
697
843
|
// API mode pagination metadata (extracted from responses)
|
|
698
844
|
const apiPaginationMeta = ref<PaginationData | null>(null);
|
|
699
845
|
|
|
846
|
+
// ============================================
|
|
847
|
+
// Client-Side Mode: Filtering, Sorting, Pagination
|
|
848
|
+
// ============================================
|
|
849
|
+
|
|
850
|
+
// Client-side current page
|
|
851
|
+
const clientSideCurrentPage = ref(1);
|
|
852
|
+
|
|
853
|
+
// Client-side filtered items
|
|
854
|
+
const clientSideFilteredItems = computed<T[]>(() => {
|
|
855
|
+
if (!isClientSideMode.value || !props.items) return [];
|
|
856
|
+
|
|
857
|
+
const filters = effectiveFilters.value;
|
|
858
|
+
const filterKeys = Object.keys(filters).filter(key => filters[key] && filters[key].trim() !== '');
|
|
859
|
+
|
|
860
|
+
if (filterKeys.length === 0) {
|
|
861
|
+
return props.items;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return props.items.filter(item => {
|
|
865
|
+
return filterKeys.every(key => {
|
|
866
|
+
const filterValue = filters[key].trim().toLowerCase();
|
|
867
|
+
const field = props.fields.find(f => f.key === key);
|
|
868
|
+
const itemValue = (item as any)[key];
|
|
869
|
+
|
|
870
|
+
if (itemValue === null || itemValue === undefined) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const filterType = field?.filter;
|
|
875
|
+
|
|
876
|
+
switch (filterType) {
|
|
877
|
+
case 'text':
|
|
878
|
+
// Case-insensitive contains search
|
|
879
|
+
return String(itemValue).toLowerCase().includes(filterValue);
|
|
880
|
+
|
|
881
|
+
case 'select':
|
|
882
|
+
// Exact match
|
|
883
|
+
return String(itemValue) === filters[key];
|
|
884
|
+
|
|
885
|
+
case 'number':
|
|
886
|
+
// Exact numeric match
|
|
887
|
+
return Number(itemValue) === Number(filters[key]);
|
|
888
|
+
|
|
889
|
+
case 'date':
|
|
890
|
+
// Exact date match
|
|
891
|
+
return String(itemValue) === filters[key];
|
|
892
|
+
|
|
893
|
+
default:
|
|
894
|
+
// Default: case-insensitive contains
|
|
895
|
+
return String(itemValue).toLowerCase().includes(filterValue);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Client-side sorted items
|
|
902
|
+
const clientSideSortedItems = computed<T[]>(() => {
|
|
903
|
+
if (!isClientSideMode.value) return [];
|
|
904
|
+
|
|
905
|
+
const items = [...clientSideFilteredItems.value];
|
|
906
|
+
const sortBy = effectiveSortBy.value;
|
|
907
|
+
|
|
908
|
+
if (!sortBy || sortBy.length === 0 || !sortBy[0].key) {
|
|
909
|
+
return items;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const { key, order } = sortBy[0];
|
|
913
|
+
const direction = order === 'desc' ? -1 : 1;
|
|
914
|
+
|
|
915
|
+
return items.sort((a, b) => {
|
|
916
|
+
const aVal = (a as any)[key];
|
|
917
|
+
const bVal = (b as any)[key];
|
|
918
|
+
|
|
919
|
+
// Handle null/undefined
|
|
920
|
+
if (aVal == null && bVal == null) return 0;
|
|
921
|
+
if (aVal == null) return direction;
|
|
922
|
+
if (bVal == null) return -direction;
|
|
923
|
+
|
|
924
|
+
// Numeric comparison
|
|
925
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
926
|
+
return (aVal - bVal) * direction;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// String comparison (case-insensitive)
|
|
930
|
+
return String(aVal).localeCompare(String(bVal), undefined, { sensitivity: 'base' }) * direction;
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
700
934
|
// API mode filter values (extracted from responses)
|
|
701
935
|
const apiFilterValues = ref<Record<string, string[]>>({});
|
|
702
936
|
|
|
@@ -819,6 +1053,66 @@ const effectivePerPage = computed(() => {
|
|
|
819
1053
|
return internalPerPage.value;
|
|
820
1054
|
});
|
|
821
1055
|
|
|
1056
|
+
// ============================================
|
|
1057
|
+
// Client-Side Mode: Pagination (requires effectivePerPage)
|
|
1058
|
+
// ============================================
|
|
1059
|
+
|
|
1060
|
+
// Client-side paginated items (final output)
|
|
1061
|
+
const clientSidePaginatedItems = computed<T[]>(() => {
|
|
1062
|
+
if (!isClientSideMode.value) return [];
|
|
1063
|
+
|
|
1064
|
+
const perPage = effectivePerPage.value;
|
|
1065
|
+
const start = (clientSideCurrentPage.value - 1) * perPage;
|
|
1066
|
+
const end = start + perPage;
|
|
1067
|
+
|
|
1068
|
+
return clientSideSortedItems.value.slice(start, end);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// Client-side pagination metadata
|
|
1072
|
+
const clientSidePagination = computed<PaginationData>(() => {
|
|
1073
|
+
const total = clientSideFilteredItems.value.length;
|
|
1074
|
+
const totalUnfiltered = props.items?.length || 0;
|
|
1075
|
+
const perPage = effectivePerPage.value;
|
|
1076
|
+
const currentPage = clientSideCurrentPage.value;
|
|
1077
|
+
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
1078
|
+
|
|
1079
|
+
// Ensure current page is valid
|
|
1080
|
+
const validPage = Math.min(Math.max(1, currentPage), lastPage);
|
|
1081
|
+
|
|
1082
|
+
const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
|
|
1083
|
+
const to = Math.min(validPage * perPage, total);
|
|
1084
|
+
|
|
1085
|
+
return {
|
|
1086
|
+
current_page: validPage,
|
|
1087
|
+
per_page: perPage,
|
|
1088
|
+
total,
|
|
1089
|
+
total_unfiltered: totalUnfiltered !== total ? totalUnfiltered : undefined,
|
|
1090
|
+
from,
|
|
1091
|
+
to,
|
|
1092
|
+
last_page: lastPage,
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Reset to page 1 when filters change in client-side mode
|
|
1097
|
+
watch(effectiveFilters, () => {
|
|
1098
|
+
if (isClientSideMode.value) {
|
|
1099
|
+
clientSideCurrentPage.value = 1;
|
|
1100
|
+
}
|
|
1101
|
+
}, { deep: true });
|
|
1102
|
+
|
|
1103
|
+
// Reset to page 1 when perPage changes in client-side mode
|
|
1104
|
+
watch(effectivePerPage, () => {
|
|
1105
|
+
if (isClientSideMode.value) {
|
|
1106
|
+
clientSideCurrentPage.value = 1;
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// Handle client-side page change
|
|
1111
|
+
const handleClientSidePageChange = (page: number) => {
|
|
1112
|
+
clientSideCurrentPage.value = page;
|
|
1113
|
+
emit('pageChange', page);
|
|
1114
|
+
};
|
|
1115
|
+
|
|
822
1116
|
// Computed: determine if per-page selector should be shown
|
|
823
1117
|
// Hide it when total items is less than the smallest page size option
|
|
824
1118
|
const shouldShowPerPageSelector = computed(() => {
|
|
@@ -1046,6 +1340,13 @@ const handleFilterChange = (fieldKey: string, value: string) => {
|
|
|
1046
1340
|
// Emit v-model update
|
|
1047
1341
|
emit('update:filters', newFilters);
|
|
1048
1342
|
|
|
1343
|
+
// Client-side mode: filtering happens reactively via computed properties
|
|
1344
|
+
// No server requests needed, just emit the event
|
|
1345
|
+
if (isClientSideMode.value) {
|
|
1346
|
+
emit('filterChange', newFilters);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1049
1350
|
// Debounce server requests for text inputs
|
|
1050
1351
|
if (filterDebounceTimer) {
|
|
1051
1352
|
clearTimeout(filterDebounceTimer);
|