@veristone/nuxt-v-app 0.2.5 → 0.2.7
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.
|
@@ -32,20 +32,26 @@ The primary table component. Wraps Nuxt UI's `<UTable>` with a toolbar, filter c
|
|
|
32
32
|
| `data` | `unknown[]` | **required** | Array of row data |
|
|
33
33
|
| `columns` | `TableColumn[]` | **required** | TanStack column definitions |
|
|
34
34
|
| `loading` | `boolean` | `false` | Shows loading skeleton on initial load |
|
|
35
|
-
| `initialPageLimit` | `number` | `10` | Rows per page |
|
|
35
|
+
| `initialPageLimit` | `number` | `10` | Rows per page (client-side mode) |
|
|
36
36
|
| `selectable` | `boolean` | `false` | Enable row selection checkboxes |
|
|
37
|
+
| `showSearch` | `boolean` | `false` | Show the built-in search input in the toolbar |
|
|
37
38
|
| `filters` | `FilterDefinition[]` | — | Filter dropdown definitions |
|
|
38
39
|
| `onRefresh` | `() => void \| Promise<void>` | — | Shows refresh button; called on click |
|
|
39
40
|
| `onRowClick` | `(row: unknown) => void` | — | Called when a row is clicked |
|
|
40
41
|
| `defaultSort` | `string` | — | Column accessorKey to sort by on mount |
|
|
41
42
|
| `defaultSortDesc` | `boolean` | `false` | Sort descending by default |
|
|
43
|
+
| `manualPagination` | `boolean` | `false` | Enable server-side pagination mode (disables client-side sorting and filtering) |
|
|
44
|
+
| `total` | `number` | — | Total row count from server (required for manualPagination) |
|
|
42
45
|
|
|
43
46
|
#### v-model Bindings
|
|
44
47
|
|
|
45
48
|
| Model | Type | Description |
|
|
46
49
|
|-------|------|-------------|
|
|
47
50
|
| `sorting` | `SortingState` | TanStack sorting state |
|
|
48
|
-
| `filterValues` | `Record<string, unknown>` | Active filter values |
|
|
51
|
+
| `filterValues` | `Record<string, unknown>` | Active filter values (for built-in filters) |
|
|
52
|
+
| `globalFilter` | `string` | Search/query filter (for built-in search) |
|
|
53
|
+
| `page` | `number` | Current page (1-based, server-side mode) |
|
|
54
|
+
| `itemsPerPage` | `number` | Page size (server-side mode) |
|
|
49
55
|
|
|
50
56
|
#### Slots
|
|
51
57
|
|
|
@@ -371,6 +377,75 @@ const columns = [
|
|
|
371
377
|
</template>
|
|
372
378
|
```
|
|
373
379
|
|
|
380
|
+
### Server-side pagination
|
|
381
|
+
|
|
382
|
+
When `manualPagination` is enabled, the table disables client-side sorting and filtering. The parent component owns all query logic — it watches the table's `sorting`, `page`, and `itemsPerPage` models, builds API params, and fetches data.
|
|
383
|
+
|
|
384
|
+
```vue
|
|
385
|
+
<script setup>
|
|
386
|
+
const columns = [
|
|
387
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
388
|
+
{ accessorKey: 'email', header: 'Email' },
|
|
389
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
const sorting = ref([])
|
|
393
|
+
const page = ref(1)
|
|
394
|
+
const itemsPerPage = ref(20)
|
|
395
|
+
const total = ref(0)
|
|
396
|
+
const users = ref([])
|
|
397
|
+
const loading = ref(false)
|
|
398
|
+
|
|
399
|
+
// Parent-owned search (not built into the table)
|
|
400
|
+
const search = ref('')
|
|
401
|
+
|
|
402
|
+
// Build API params from table state + parent state
|
|
403
|
+
const apiParams = computed(() => {
|
|
404
|
+
const params = { page: page.value, limit: itemsPerPage.value }
|
|
405
|
+
const sort = sorting.value[0]
|
|
406
|
+
if (sort?.id) {
|
|
407
|
+
params.sort_by = sort.id
|
|
408
|
+
params.sort_order = sort.desc ? 'desc' : 'asc'
|
|
409
|
+
}
|
|
410
|
+
if (search.value.trim()) {
|
|
411
|
+
params.search = search.value.trim()
|
|
412
|
+
}
|
|
413
|
+
return params
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
async function fetchUsers() {
|
|
417
|
+
loading.value = true
|
|
418
|
+
try {
|
|
419
|
+
const response = await $fetch('/api/users', { query: apiParams.value })
|
|
420
|
+
users.value = response.data
|
|
421
|
+
total.value = response.total
|
|
422
|
+
} finally {
|
|
423
|
+
loading.value = false
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
watch(apiParams, fetchUsers, { immediate: true, deep: true })
|
|
428
|
+
</script>
|
|
429
|
+
|
|
430
|
+
<template>
|
|
431
|
+
<!-- Parent-owned search input -->
|
|
432
|
+
<UInput v-model="search" placeholder="Search..." icon="i-lucide-search" class="mb-4 max-w-xs" />
|
|
433
|
+
|
|
434
|
+
<VATable
|
|
435
|
+
name="Users"
|
|
436
|
+
:data="users"
|
|
437
|
+
:columns="columns"
|
|
438
|
+
:loading="loading"
|
|
439
|
+
:manual-pagination="true"
|
|
440
|
+
:total="total"
|
|
441
|
+
v-model:sorting="sorting"
|
|
442
|
+
v-model:page="page"
|
|
443
|
+
v-model:items-per-page="itemsPerPage"
|
|
444
|
+
:on-refresh="fetchUsers"
|
|
445
|
+
/>
|
|
446
|
+
</template>
|
|
447
|
+
```
|
|
448
|
+
|
|
374
449
|
---
|
|
375
450
|
|
|
376
451
|
## Date Auto-Detection
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<!-- Normal Toolbar (shown when no items selected) -->
|
|
41
41
|
<div v-else class="flex items-center gap-2">
|
|
42
42
|
<UInput
|
|
43
|
+
v-if="showSearch"
|
|
43
44
|
v-model="globalFilter"
|
|
44
45
|
placeholder="Search..."
|
|
45
46
|
icon="i-lucide-search"
|
|
@@ -97,7 +98,7 @@
|
|
|
97
98
|
<UTable
|
|
98
99
|
ref="table"
|
|
99
100
|
v-model:sorting="sorting"
|
|
100
|
-
v-model:global-filter="
|
|
101
|
+
v-model:global-filter="tableGlobalFilter"
|
|
101
102
|
v-model:pagination="pagination"
|
|
102
103
|
v-model:column-visibility="columnVisibility"
|
|
103
104
|
v-model:row-selection="rowSelection"
|
|
@@ -105,8 +106,14 @@
|
|
|
105
106
|
:columns="tableColumns"
|
|
106
107
|
:loading="loading"
|
|
107
108
|
:row-selection-options="{ enableRowSelection: props.selectable }"
|
|
108
|
-
:sorting-options="
|
|
109
|
-
:pagination-options="{
|
|
109
|
+
:sorting-options="sortingOptions"
|
|
110
|
+
:pagination-options="{
|
|
111
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
112
|
+
manualPagination: props.manualPagination,
|
|
113
|
+
...(props.manualPagination && props.total !== undefined
|
|
114
|
+
? { pageCount: Math.ceil(props.total / pagination.pageSize) }
|
|
115
|
+
: {})
|
|
116
|
+
}"
|
|
110
117
|
:ui="{
|
|
111
118
|
th: 'px-3 py-2',
|
|
112
119
|
td: 'px-3 py-2',
|
|
@@ -148,7 +155,7 @@
|
|
|
148
155
|
:items-per-page="pagination.pageSize"
|
|
149
156
|
:total="totalRows"
|
|
150
157
|
size="sm"
|
|
151
|
-
@update:page="
|
|
158
|
+
@update:page="onPageChange"
|
|
152
159
|
/>
|
|
153
160
|
</div>
|
|
154
161
|
</div>
|
|
@@ -157,7 +164,7 @@
|
|
|
157
164
|
</template>
|
|
158
165
|
|
|
159
166
|
<script setup lang="ts">
|
|
160
|
-
import {
|
|
167
|
+
import { getPaginationRowModel } from "@tanstack/vue-table";
|
|
161
168
|
import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
|
|
162
169
|
import type { SortingState } from "@tanstack/vue-table";
|
|
163
170
|
import { h, resolveComponent, useSlots } from 'vue';
|
|
@@ -177,15 +184,18 @@ const props = withDefaults(defineProps<{
|
|
|
177
184
|
loading?: boolean;
|
|
178
185
|
initialPageLimit?: number;
|
|
179
186
|
selectable?: boolean;
|
|
187
|
+
showSearch?: boolean;
|
|
180
188
|
filters?: FilterDefinition[];
|
|
181
189
|
onRefresh?: () => void | Promise<void>;
|
|
182
190
|
onRowClick?: (row: unknown) => void;
|
|
183
|
-
/** Default column to sort by */
|
|
184
191
|
defaultSort?: string;
|
|
185
|
-
/** Default sort direction */
|
|
186
192
|
defaultSortDesc?: boolean;
|
|
193
|
+
manualPagination?: boolean;
|
|
194
|
+
total?: number;
|
|
187
195
|
}>(), {
|
|
188
196
|
selectable: false,
|
|
197
|
+
showSearch: false,
|
|
198
|
+
manualPagination: false,
|
|
189
199
|
});
|
|
190
200
|
|
|
191
201
|
// Sorting state - v-model support
|
|
@@ -197,6 +207,41 @@ const filterValues = defineModel<Record<string, unknown>>("filterValues", {
|
|
|
197
207
|
default: () => ({}),
|
|
198
208
|
});
|
|
199
209
|
|
|
210
|
+
// Server-side pagination - v-model support (1-based page)
|
|
211
|
+
const page = defineModel<number>("page", { default: 1 });
|
|
212
|
+
const itemsPerPage = defineModel<number>("itemsPerPage", { default: 10 });
|
|
213
|
+
|
|
214
|
+
// Internal pagination state (0-based pageIndex for TanStack)
|
|
215
|
+
const pagination = ref({
|
|
216
|
+
pageIndex: 0,
|
|
217
|
+
pageSize: 10,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Initialize pagination from props and sync with v-model
|
|
221
|
+
onMounted(() => {
|
|
222
|
+
const defaultPageSize = props.initialPageLimit ?? 10;
|
|
223
|
+
pagination.value.pageSize = defaultPageSize;
|
|
224
|
+
|
|
225
|
+
// Sync itemsPerPage if not already set by parent
|
|
226
|
+
if (itemsPerPage.value === 10 && defaultPageSize !== 10) {
|
|
227
|
+
itemsPerPage.value = defaultPageSize;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Sync internal pagination with v-model when in manual mode
|
|
232
|
+
watch(
|
|
233
|
+
[page, itemsPerPage],
|
|
234
|
+
([newPage, newItemsPerPage]) => {
|
|
235
|
+
if (props.manualPagination) {
|
|
236
|
+
pagination.value = {
|
|
237
|
+
pageIndex: newPage - 1, // Convert 1-based to 0-based
|
|
238
|
+
pageSize: newItemsPerPage,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{ immediate: true }
|
|
243
|
+
);
|
|
244
|
+
|
|
200
245
|
const refreshing = ref(false);
|
|
201
246
|
const slots = useSlots()
|
|
202
247
|
|
|
@@ -239,14 +284,6 @@ function clearSelection() {
|
|
|
239
284
|
rowSelection.value = {};
|
|
240
285
|
}
|
|
241
286
|
|
|
242
|
-
watch(
|
|
243
|
-
() => filterValues.value,
|
|
244
|
-
() => {
|
|
245
|
-
clearSelection();
|
|
246
|
-
},
|
|
247
|
-
{ deep: true }
|
|
248
|
-
);
|
|
249
|
-
|
|
250
287
|
// Table ref for accessing TanStack API
|
|
251
288
|
const table = ref<{ tableApi?: any } | null>(null);
|
|
252
289
|
|
|
@@ -282,6 +319,30 @@ watch(
|
|
|
282
319
|
// Global filter - v-model support
|
|
283
320
|
const globalFilter = defineModel<string>("globalFilter", { default: "" });
|
|
284
321
|
|
|
322
|
+
// In manual mode, prevent TanStack from filtering client-side
|
|
323
|
+
const tableGlobalFilter = computed({
|
|
324
|
+
get: () => props.manualPagination ? undefined : globalFilter.value,
|
|
325
|
+
set: (v: string) => { globalFilter.value = v ?? '' },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Reset page when sorting changes
|
|
329
|
+
watch(
|
|
330
|
+
sorting,
|
|
331
|
+
() => {
|
|
332
|
+
clearSelection();
|
|
333
|
+
if (props.manualPagination) {
|
|
334
|
+
page.value = 1;
|
|
335
|
+
} else {
|
|
336
|
+
pagination.value = { ...pagination.value, pageIndex: 0 };
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{ deep: true, flush: 'sync' }
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const sortingOptions = computed(() =>
|
|
343
|
+
props.manualPagination ? { manualSorting: true } : undefined
|
|
344
|
+
);
|
|
345
|
+
|
|
285
346
|
// Initialize default sorting if provided
|
|
286
347
|
onMounted(() => {
|
|
287
348
|
if (props.defaultSort && sorting.value.length === 0) {
|
|
@@ -289,12 +350,6 @@ onMounted(() => {
|
|
|
289
350
|
}
|
|
290
351
|
});
|
|
291
352
|
|
|
292
|
-
// Pagination state
|
|
293
|
-
const pagination = ref({
|
|
294
|
-
pageIndex: 0,
|
|
295
|
-
pageSize: props.initialPageLimit ?? 10,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
353
|
// Column visibility state
|
|
299
354
|
const columnVisibility = ref<Record<string, boolean>>({});
|
|
300
355
|
|
|
@@ -446,6 +501,9 @@ function handleRowClick(row: any) {
|
|
|
446
501
|
|
|
447
502
|
// Computed pagination info
|
|
448
503
|
const totalRows = computed(() => {
|
|
504
|
+
if (props.manualPagination && props.total !== undefined) {
|
|
505
|
+
return props.total;
|
|
506
|
+
}
|
|
449
507
|
return table.value?.tableApi?.getFilteredRowModel().rows.length ?? 0;
|
|
450
508
|
});
|
|
451
509
|
|
|
@@ -461,17 +519,35 @@ const endIndex = computed(() => {
|
|
|
461
519
|
|
|
462
520
|
// Handle page size change
|
|
463
521
|
const onPageSizeChange = (size: number) => {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
522
|
+
if (props.manualPagination) {
|
|
523
|
+
// Emit to parent for server-side handling
|
|
524
|
+
itemsPerPage.value = size;
|
|
525
|
+
page.value = 1; // Reset to first page
|
|
526
|
+
} else {
|
|
527
|
+
pagination.value = {
|
|
528
|
+
pageIndex: 0,
|
|
529
|
+
pageSize: size,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
468
532
|
};
|
|
469
533
|
|
|
470
|
-
//
|
|
534
|
+
// Handle page change from UPagination
|
|
535
|
+
const onPageChange = (newPage: number) => {
|
|
536
|
+
if (props.manualPagination) {
|
|
537
|
+
// Emit to parent for server-side handling
|
|
538
|
+
page.value = newPage;
|
|
539
|
+
} else {
|
|
540
|
+
pagination.value.pageIndex = newPage - 1;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Reset pagination when data changes (client-side only)
|
|
471
545
|
watch(
|
|
472
546
|
() => props.data,
|
|
473
547
|
() => {
|
|
474
|
-
|
|
548
|
+
if (!props.manualPagination) {
|
|
549
|
+
pagination.value.pageIndex = 0;
|
|
550
|
+
}
|
|
475
551
|
}
|
|
476
552
|
);
|
|
477
553
|
|
|
@@ -1,247 +1,459 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { TableColumn } from
|
|
2
|
+
import type { TableColumn } from "@nuxt/ui";
|
|
3
3
|
|
|
4
4
|
definePageMeta({
|
|
5
|
-
title:
|
|
6
|
-
})
|
|
5
|
+
title: "Table Components",
|
|
6
|
+
});
|
|
7
7
|
|
|
8
8
|
// Sample data
|
|
9
9
|
const users = ref([
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
{
|
|
11
|
+
id: 1,
|
|
12
|
+
name: "John Doe",
|
|
13
|
+
email: "john@example.com",
|
|
14
|
+
role: "Admin",
|
|
15
|
+
status: "active",
|
|
16
|
+
department: "Engineering",
|
|
17
|
+
salary: 95000,
|
|
18
|
+
createdAt: "2024-01-15T10:30:00Z",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 2,
|
|
22
|
+
name: "Jane Smith",
|
|
23
|
+
email: "jane@example.com",
|
|
24
|
+
role: "Editor",
|
|
25
|
+
status: "active",
|
|
26
|
+
department: "Marketing",
|
|
27
|
+
salary: 72000,
|
|
28
|
+
createdAt: "2024-02-20T14:00:00Z",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 3,
|
|
32
|
+
name: "Bob Wilson",
|
|
33
|
+
email: "bob@example.com",
|
|
34
|
+
role: "Viewer",
|
|
35
|
+
status: "pending",
|
|
36
|
+
department: "Sales",
|
|
37
|
+
salary: 68000,
|
|
38
|
+
createdAt: "2024-03-10T09:15:00Z",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 4,
|
|
42
|
+
name: "Alice Brown",
|
|
43
|
+
email: "alice@example.com",
|
|
44
|
+
role: "Admin",
|
|
45
|
+
status: "active",
|
|
46
|
+
department: "Engineering",
|
|
47
|
+
salary: 105000,
|
|
48
|
+
createdAt: "2024-04-05T11:45:00Z",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 5,
|
|
52
|
+
name: "Charlie Davis",
|
|
53
|
+
email: "charlie@example.com",
|
|
54
|
+
role: "Editor",
|
|
55
|
+
status: "inactive",
|
|
56
|
+
department: "Design",
|
|
57
|
+
salary: 78000,
|
|
58
|
+
createdAt: "2024-05-01T16:20:00Z",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 6,
|
|
62
|
+
name: "Diana Miller",
|
|
63
|
+
email: "diana@example.com",
|
|
64
|
+
role: "Viewer",
|
|
65
|
+
status: "active",
|
|
66
|
+
department: "HR",
|
|
67
|
+
salary: 62000,
|
|
68
|
+
createdAt: "2024-06-12T08:00:00Z",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 7,
|
|
72
|
+
name: "Edward Lee",
|
|
73
|
+
email: "edward@example.com",
|
|
74
|
+
role: "Admin",
|
|
75
|
+
status: "active",
|
|
76
|
+
department: "Engineering",
|
|
77
|
+
salary: 112000,
|
|
78
|
+
createdAt: "2024-07-18T13:30:00Z",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 8,
|
|
82
|
+
name: "Fiona Garcia",
|
|
83
|
+
email: "fiona@example.com",
|
|
84
|
+
role: "Editor",
|
|
85
|
+
status: "pending",
|
|
86
|
+
department: "Marketing",
|
|
87
|
+
salary: 75000,
|
|
88
|
+
createdAt: "2024-08-22T10:00:00Z",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 9,
|
|
92
|
+
name: "George Martinez",
|
|
93
|
+
email: "george@example.com",
|
|
94
|
+
role: "Viewer",
|
|
95
|
+
status: "active",
|
|
96
|
+
department: "Sales",
|
|
97
|
+
salary: 71000,
|
|
98
|
+
createdAt: "2024-09-03T15:45:00Z",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 10,
|
|
102
|
+
name: "Hannah Robinson",
|
|
103
|
+
email: "hannah@example.com",
|
|
104
|
+
role: "Editor",
|
|
105
|
+
status: "active",
|
|
106
|
+
department: "Design",
|
|
107
|
+
salary: 82000,
|
|
108
|
+
createdAt: "2024-10-14T12:10:00Z",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 11,
|
|
112
|
+
name: "Ivan Thompson",
|
|
113
|
+
email: "ivan@example.com",
|
|
114
|
+
role: "Admin",
|
|
115
|
+
status: "inactive",
|
|
116
|
+
department: "Engineering",
|
|
117
|
+
salary: 98000,
|
|
118
|
+
createdAt: "2024-11-25T09:30:00Z",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 12,
|
|
122
|
+
name: "Julia White",
|
|
123
|
+
email: "julia@example.com",
|
|
124
|
+
role: "Viewer",
|
|
125
|
+
status: "active",
|
|
126
|
+
department: "HR",
|
|
127
|
+
salary: 58000,
|
|
128
|
+
createdAt: "2024-12-01T17:00:00Z",
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
23
131
|
|
|
24
132
|
// Basic columns using the new Nuxt UI TableColumn format
|
|
25
|
-
const basicColumns: TableColumn<typeof users.value[0]>[] = [
|
|
26
|
-
{ accessorKey:
|
|
27
|
-
{ accessorKey:
|
|
28
|
-
{ accessorKey:
|
|
29
|
-
{ accessorKey:
|
|
30
|
-
]
|
|
133
|
+
const basicColumns: TableColumn<(typeof users.value)[0]>[] = [
|
|
134
|
+
{ accessorKey: "name", header: "Name" },
|
|
135
|
+
{ accessorKey: "email", header: "Email" },
|
|
136
|
+
{ accessorKey: "role", header: "Role" },
|
|
137
|
+
{ accessorKey: "status", header: "Status" },
|
|
138
|
+
];
|
|
31
139
|
|
|
32
140
|
// Full columns with more fields
|
|
33
|
-
const fullColumns: TableColumn<typeof users.value[0]>[] = [
|
|
34
|
-
{ accessorKey:
|
|
35
|
-
{ accessorKey:
|
|
36
|
-
{ accessorKey:
|
|
37
|
-
{ accessorKey:
|
|
38
|
-
{ accessorKey:
|
|
39
|
-
{ accessorKey:
|
|
40
|
-
{ accessorKey:
|
|
41
|
-
]
|
|
141
|
+
const fullColumns: TableColumn<(typeof users.value)[0]>[] = [
|
|
142
|
+
{ accessorKey: "name", header: "Name" },
|
|
143
|
+
{ accessorKey: "email", header: "Email" },
|
|
144
|
+
{ accessorKey: "role", header: "Role" },
|
|
145
|
+
{ accessorKey: "department", header: "Department" },
|
|
146
|
+
{ accessorKey: "salary", header: "Salary" },
|
|
147
|
+
{ accessorKey: "status", header: "Status" },
|
|
148
|
+
{ accessorKey: "createdAt", header: "Created" },
|
|
149
|
+
];
|
|
42
150
|
|
|
43
151
|
// Columns using presets
|
|
44
|
-
const { presets } = useXATableColumns()
|
|
152
|
+
const { presets } = useXATableColumns();
|
|
45
153
|
|
|
46
|
-
const presetColumns: TableColumn<typeof users.value[0]>[] = [
|
|
47
|
-
presets.text(
|
|
48
|
-
presets.email(
|
|
49
|
-
presets.badge(
|
|
50
|
-
colorMap: { active:
|
|
154
|
+
const presetColumns: TableColumn<(typeof users.value)[0]>[] = [
|
|
155
|
+
presets.text("name", "Name"),
|
|
156
|
+
presets.email("email", "Email"),
|
|
157
|
+
presets.badge("status", "Status", {
|
|
158
|
+
colorMap: { active: "success", pending: "warning", inactive: "neutral" },
|
|
51
159
|
}),
|
|
52
|
-
presets.currency(
|
|
53
|
-
presets.date(
|
|
54
|
-
]
|
|
160
|
+
presets.currency("salary", "Salary"),
|
|
161
|
+
presets.date("createdAt", "Joined", { format: "relative" }),
|
|
162
|
+
];
|
|
55
163
|
|
|
56
164
|
// Products data for variety
|
|
57
165
|
const products = ref([
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
166
|
+
{
|
|
167
|
+
id: 1,
|
|
168
|
+
name: 'MacBook Pro 14"',
|
|
169
|
+
category: "Laptops",
|
|
170
|
+
price: 1999,
|
|
171
|
+
stock: 45,
|
|
172
|
+
rating: 4.8,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 2,
|
|
176
|
+
name: "iPhone 15 Pro",
|
|
177
|
+
category: "Phones",
|
|
178
|
+
price: 999,
|
|
179
|
+
stock: 120,
|
|
180
|
+
rating: 4.9,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 3,
|
|
184
|
+
name: "AirPods Pro",
|
|
185
|
+
category: "Audio",
|
|
186
|
+
price: 249,
|
|
187
|
+
stock: 200,
|
|
188
|
+
rating: 4.7,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 4,
|
|
192
|
+
name: "iPad Air",
|
|
193
|
+
category: "Tablets",
|
|
194
|
+
price: 599,
|
|
195
|
+
stock: 80,
|
|
196
|
+
rating: 4.6,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 5,
|
|
200
|
+
name: "Apple Watch Ultra",
|
|
201
|
+
category: "Wearables",
|
|
202
|
+
price: 799,
|
|
203
|
+
stock: 35,
|
|
204
|
+
rating: 4.8,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 6,
|
|
208
|
+
name: "Mac Mini M2",
|
|
209
|
+
category: "Desktops",
|
|
210
|
+
price: 699,
|
|
211
|
+
stock: 60,
|
|
212
|
+
rating: 4.5,
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
const productColumns: TableColumn<(typeof products.value)[0]>[] = [
|
|
217
|
+
{ accessorKey: "name", header: "Product" },
|
|
218
|
+
{ accessorKey: "category", header: "Category" },
|
|
219
|
+
{ accessorKey: "price", header: "Price" },
|
|
220
|
+
{ accessorKey: "stock", header: "Stock" },
|
|
221
|
+
{ accessorKey: "rating", header: "Rating" },
|
|
222
|
+
];
|
|
73
223
|
|
|
74
224
|
// Filter definitions
|
|
75
225
|
const statusFilters = [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
226
|
+
{
|
|
227
|
+
label: "Status",
|
|
228
|
+
key: "status",
|
|
229
|
+
options: [
|
|
230
|
+
{ label: "Active", value: "active" },
|
|
231
|
+
{ label: "Pending", value: "pending" },
|
|
232
|
+
{ label: "Inactive", value: "inactive" },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
label: "Role",
|
|
237
|
+
key: "role",
|
|
238
|
+
options: [
|
|
239
|
+
{ label: "Admin", value: "Admin" },
|
|
240
|
+
{ label: "Editor", value: "Editor" },
|
|
241
|
+
{ label: "Viewer", value: "Viewer" },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
];
|
|
87
245
|
|
|
88
246
|
// Event handlers
|
|
89
247
|
const handleRowClick = (row: any) => {
|
|
90
|
-
console.log(
|
|
91
|
-
}
|
|
248
|
+
console.log("Row clicked:", row);
|
|
249
|
+
};
|
|
92
250
|
</script>
|
|
93
251
|
|
|
94
252
|
<template>
|
|
95
253
|
<UDashboardPanel grow>
|
|
96
254
|
<UContainer>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
255
|
+
<div class="py-8 space-y-12">
|
|
256
|
+
<div>
|
|
257
|
+
<h1 class="text-3xl font-bold mb-4">VATable Components Showcase</h1>
|
|
258
|
+
<p class="text-gray-600 dark:text-gray-400">
|
|
259
|
+
TanStack-powered data tables with search, sort, filter, export,
|
|
260
|
+
selection, and pagination.
|
|
261
|
+
</p>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Basic Table -->
|
|
265
|
+
<section>
|
|
266
|
+
<h2 class="text-2xl font-semibold mb-4">Basic Table</h2>
|
|
267
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
268
|
+
Simple table with minimal configuration.
|
|
269
|
+
</p>
|
|
270
|
+
<ClientOnly>
|
|
271
|
+
<VATable
|
|
272
|
+
name="Basic Users"
|
|
273
|
+
:data="users.slice(0, 5)"
|
|
274
|
+
:columns="basicColumns"
|
|
275
|
+
/>
|
|
276
|
+
</ClientOnly>
|
|
277
|
+
</section>
|
|
278
|
+
|
|
279
|
+
<!-- Preset-Based Columns -->
|
|
280
|
+
<section>
|
|
281
|
+
<h2 class="text-2xl font-semibold mb-4">Preset-Based Columns</h2>
|
|
282
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
283
|
+
Using useXATableColumns presets for automatic cell rendering (badge,
|
|
284
|
+
currency, date).
|
|
285
|
+
</p>
|
|
286
|
+
<ClientOnly>
|
|
287
|
+
<VATable
|
|
288
|
+
name="Users with Presets"
|
|
289
|
+
:data="users"
|
|
290
|
+
:columns="presetColumns"
|
|
291
|
+
/>
|
|
292
|
+
</ClientOnly>
|
|
293
|
+
</section>
|
|
294
|
+
|
|
295
|
+
<!-- Table with Filters -->
|
|
296
|
+
<section>
|
|
297
|
+
<h2 class="text-2xl font-semibold mb-4">Table with Filters</h2>
|
|
298
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
299
|
+
Inline filter dropdowns with active filter chips.
|
|
300
|
+
</p>
|
|
301
|
+
<ClientOnly>
|
|
302
|
+
<VATable
|
|
303
|
+
name="Filtered Users"
|
|
304
|
+
:data="users"
|
|
305
|
+
:show-search="true"
|
|
306
|
+
:columns="basicColumns"
|
|
307
|
+
:filters="statusFilters"
|
|
308
|
+
/>
|
|
309
|
+
</ClientOnly>
|
|
310
|
+
</section>
|
|
311
|
+
|
|
312
|
+
<!-- Selectable Table -->
|
|
313
|
+
<section>
|
|
314
|
+
<h2 class="text-2xl font-semibold mb-4">Selectable Table</h2>
|
|
315
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
316
|
+
Multi-select with checkbox column and bulk action support.
|
|
317
|
+
</p>
|
|
318
|
+
<ClientOnly>
|
|
319
|
+
<VATable
|
|
320
|
+
name="Select Users"
|
|
321
|
+
:data="users.slice(0, 6)"
|
|
322
|
+
:columns="basicColumns"
|
|
323
|
+
selectable
|
|
324
|
+
>
|
|
325
|
+
<template #bulk-actions="{ count, clear }">
|
|
326
|
+
<span class="text-sm text-muted">{{ count }} selected</span>
|
|
327
|
+
<UButton
|
|
328
|
+
size="xs"
|
|
329
|
+
color="neutral"
|
|
330
|
+
variant="outline"
|
|
331
|
+
label="Clear"
|
|
332
|
+
@click="clear"
|
|
333
|
+
/>
|
|
334
|
+
<UButton
|
|
335
|
+
size="xs"
|
|
336
|
+
color="error"
|
|
337
|
+
variant="soft"
|
|
338
|
+
label="Delete Selected"
|
|
339
|
+
/>
|
|
340
|
+
</template>
|
|
341
|
+
</VATable>
|
|
342
|
+
</ClientOnly>
|
|
343
|
+
</section>
|
|
104
344
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
</
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<section>
|
|
218
|
-
<h2 class="text-2xl font-semibold mb-4">Loading State</h2>
|
|
219
|
-
<p class="text-gray-600 dark:text-gray-400 mb-4">Full-screen loading state for initial data fetch.</p>
|
|
220
|
-
<ClientOnly>
|
|
221
|
-
<VATable
|
|
222
|
-
name="Loading Table"
|
|
223
|
-
:data="[]"
|
|
224
|
-
:columns="basicColumns"
|
|
225
|
-
:loading="true"
|
|
226
|
-
/>
|
|
227
|
-
</ClientOnly>
|
|
228
|
-
</section>
|
|
229
|
-
|
|
230
|
-
<!-- VADataTable Section -->
|
|
231
|
-
<section>
|
|
232
|
-
<h2 class="text-2xl font-semibold mb-4">VADataTable</h2>
|
|
233
|
-
<p class="text-gray-600 dark:text-gray-400 mb-4">Dashboard-integrated table with UDashboardPanel wrapper.</p>
|
|
234
|
-
<ClientOnly>
|
|
235
|
-
<VADataTable
|
|
236
|
-
title="Users (DataTable)"
|
|
237
|
-
:rows="users.slice(0, 5)"
|
|
238
|
-
:columns="basicColumns"
|
|
239
|
-
:show-search="false"
|
|
240
|
-
:show-pagination="false"
|
|
241
|
-
/>
|
|
242
|
-
</ClientOnly>
|
|
243
|
-
</section>
|
|
244
|
-
</div>
|
|
345
|
+
<!-- Table with Row Actions -->
|
|
346
|
+
<section>
|
|
347
|
+
<h2 class="text-2xl font-semibold mb-4">Table with Row Actions</h2>
|
|
348
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
349
|
+
Custom actions column via slot.
|
|
350
|
+
</p>
|
|
351
|
+
<ClientOnly>
|
|
352
|
+
<VATable
|
|
353
|
+
name="Users with Actions"
|
|
354
|
+
:data="users.slice(0, 5)"
|
|
355
|
+
:columns="basicColumns"
|
|
356
|
+
:on-row-click="handleRowClick"
|
|
357
|
+
>
|
|
358
|
+
<template #actions-cell="{ row }">
|
|
359
|
+
<div class="flex items-center justify-end gap-1">
|
|
360
|
+
<UButton
|
|
361
|
+
icon="i-lucide-eye"
|
|
362
|
+
color="neutral"
|
|
363
|
+
variant="ghost"
|
|
364
|
+
size="xs"
|
|
365
|
+
@click.stop="() => console.log('View', row.original)"
|
|
366
|
+
/>
|
|
367
|
+
<UButton
|
|
368
|
+
icon="i-lucide-pencil"
|
|
369
|
+
color="neutral"
|
|
370
|
+
variant="ghost"
|
|
371
|
+
size="xs"
|
|
372
|
+
@click.stop="() => console.log('Edit', row.original)"
|
|
373
|
+
/>
|
|
374
|
+
<UButton
|
|
375
|
+
icon="i-lucide-trash-2"
|
|
376
|
+
color="error"
|
|
377
|
+
variant="ghost"
|
|
378
|
+
size="xs"
|
|
379
|
+
@click.stop="() => console.log('Delete', row.original)"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
</template>
|
|
383
|
+
</VATable>
|
|
384
|
+
</ClientOnly>
|
|
385
|
+
</section>
|
|
386
|
+
|
|
387
|
+
<!-- Table with Refresh -->
|
|
388
|
+
<section>
|
|
389
|
+
<h2 class="text-2xl font-semibold mb-4">Table with Refresh</h2>
|
|
390
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
391
|
+
Refresh button with loading state.
|
|
392
|
+
</p>
|
|
393
|
+
<ClientOnly>
|
|
394
|
+
<VATable
|
|
395
|
+
name="Products"
|
|
396
|
+
:data="products"
|
|
397
|
+
:columns="productColumns"
|
|
398
|
+
:on-refresh="
|
|
399
|
+
async () => {
|
|
400
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
401
|
+
}
|
|
402
|
+
"
|
|
403
|
+
/>
|
|
404
|
+
</ClientOnly>
|
|
405
|
+
</section>
|
|
406
|
+
|
|
407
|
+
<!-- Default Sort -->
|
|
408
|
+
<section>
|
|
409
|
+
<h2 class="text-2xl font-semibold mb-4">Default Sort</h2>
|
|
410
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
411
|
+
Table pre-sorted by salary descending.
|
|
412
|
+
</p>
|
|
413
|
+
<ClientOnly>
|
|
414
|
+
<VATable
|
|
415
|
+
name="Sorted Users"
|
|
416
|
+
:data="users"
|
|
417
|
+
:columns="fullColumns"
|
|
418
|
+
default-sort="salary"
|
|
419
|
+
:default-sort-desc="true"
|
|
420
|
+
/>
|
|
421
|
+
</ClientOnly>
|
|
422
|
+
</section>
|
|
423
|
+
|
|
424
|
+
<!-- Loading State -->
|
|
425
|
+
<section>
|
|
426
|
+
<h2 class="text-2xl font-semibold mb-4">Loading State</h2>
|
|
427
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
428
|
+
Full-screen loading state for initial data fetch.
|
|
429
|
+
</p>
|
|
430
|
+
<ClientOnly>
|
|
431
|
+
<VATable
|
|
432
|
+
name="Loading Table"
|
|
433
|
+
:data="[]"
|
|
434
|
+
:columns="basicColumns"
|
|
435
|
+
:loading="true"
|
|
436
|
+
/>
|
|
437
|
+
</ClientOnly>
|
|
438
|
+
</section>
|
|
439
|
+
|
|
440
|
+
<!-- VADataTable Section -->
|
|
441
|
+
<section>
|
|
442
|
+
<h2 class="text-2xl font-semibold mb-4">VADataTable</h2>
|
|
443
|
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
444
|
+
Dashboard-integrated table with UDashboardPanel wrapper.
|
|
445
|
+
</p>
|
|
446
|
+
<ClientOnly>
|
|
447
|
+
<VADataTable
|
|
448
|
+
title="Users (DataTable)"
|
|
449
|
+
:rows="users.slice(0, 5)"
|
|
450
|
+
:columns="basicColumns"
|
|
451
|
+
:show-search="false"
|
|
452
|
+
:show-pagination="false"
|
|
453
|
+
/>
|
|
454
|
+
</ClientOnly>
|
|
455
|
+
</section>
|
|
456
|
+
</div>
|
|
245
457
|
</UContainer>
|
|
246
458
|
</UDashboardPanel>
|
|
247
459
|
</template>
|