@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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2025-11-26T12:28:39.315Z",
2
+ "generated": "2025-12-02T13:23:09.065Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.4.7"
5
+ "version": "0.4.8"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2025-11-26T12:28:39.369Z
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, loading states, and error handling fo
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
- - [Forms](/guide/forms): The library includes a powerful type-safe form system with validation, auto-generated forms, and com
77
- - [Getting Started](/guide/getting-started): @omnitend/dashboard-for-laravel is a reusable full-stack component library for building Laravel dash
78
- - [Installation](/guide/installation): Install via npm:
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**: 58
96
+ **Total Pages**: 69
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
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",
@@ -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
- // Show error toast
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: 'Failed to update. Please check the form for errors.',
1541
+ body: errorMessage,
1223
1542
  variant: 'danger',
1224
1543
  modelValue: 5000, // Auto-dismiss after 5 seconds
1225
1544
  });