@vc-shell/vc-app-skill 2.0.11 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +56 -36
- package/runtime/knowledge/docs/ui/composables/useDataTablePagination.docs.md +44 -54
- package/runtime/knowledge/docs/ui/composables/useDataTableSort.docs.md +16 -21
- package/runtime/knowledge/docs/ui/composables/useTableSearch.docs.md +130 -0
- package/runtime/knowledge/migration-prompts/datatable-migration.md +27 -18
- package/runtime/knowledge/patterns/list-blade-pattern.md +85 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
|
|
5
5
|
"bin": "./bin/install.cjs",
|
|
6
6
|
"files": [
|
package/runtime/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0
|
|
1
|
+
2.1.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 3a32dc437 on 2026-06-22T17:27:53.738Z
|
|
@@ -1032,7 +1032,7 @@ async function loadNextPage() {
|
|
|
1032
1032
|
!!! tip "Use unique state keys"
|
|
1033
1033
|
Every table in your application must have a distinct `state-key`. Two tables sharing the same key will silently overwrite each other's persisted column widths, order, and hidden/shown column lists.
|
|
1034
1034
|
|
|
1035
|
-
Persist column widths, column order, and column visibility across page reloads. Column layout is stored in `localStorage`/`sessionStorage` (keyed by `state-key`). Sort, search, and the current page are persisted
|
|
1035
|
+
Persist column widths, column order, and column visibility across page reloads. Column layout is stored in `localStorage`/`sessionStorage` (keyed by `state-key`). Sort, search, and the current page are NOT persisted by the table itself -- use the state composables with a matching `stateKey` instead (see [URL query persistence](#url-query-persistence) below). Selection, column filters, and other transient state are session-scoped and are deliberately excluded from any persistence:
|
|
1036
1036
|
|
|
1037
1037
|
```vue
|
|
1038
1038
|
<VcDataTable :items="products" state-key="product-list">
|
|
@@ -1067,58 +1067,78 @@ Listen to state events:
|
|
|
1067
1067
|
|
|
1068
1068
|
### URL query persistence
|
|
1069
1069
|
|
|
1070
|
-
|
|
1070
|
+
Sort, search, and the current page are owned by the state composables, not by `VcDataTable`. The `state-key` prop on `VcDataTable` controls column-layout persistence (localStorage/sessionStorage) only.
|
|
1071
1071
|
|
|
1072
|
-
|
|
1072
|
+
To persist query state to the blade URL, use `useDataTableSort`, `useTableSearch`, and `useDataTablePagination` with a matching `stateKey`. Each composable restores its slice from the URL on creation and persists changes via `router.replace` (no history entries added).
|
|
1073
1073
|
|
|
1074
|
-
- **Namespacing:**
|
|
1075
|
-
- **Keys:** `<
|
|
1076
|
-
- **
|
|
1077
|
-
- **Scope:** only sort, search, and page. Column layout still uses `localStorage`/`sessionStorage` (above); column filters and selection are not persisted.
|
|
1074
|
+
- **Namespacing:** URL keys are prefixed with the `stateKey` value -- e.g. `?offers_list_sort=name:DESC&offers_list_search=foo&offers_list_page=2`. Use the same string for all three composables on a given page.
|
|
1075
|
+
- **Keys:** `<stateKey>_sort` (`field:ASC` / `field:DESC`), `<stateKey>_search`, `<stateKey>_page` (1-based).
|
|
1076
|
+
- **Scope:** sort, search, and page only. Column layout still uses localStorage/sessionStorage; column filters and selection are not persisted.
|
|
1078
1077
|
|
|
1079
|
-
|
|
1078
|
+
Canonical list-page pattern:
|
|
1080
1079
|
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
import {
|
|
1080
|
+
```vue
|
|
1081
|
+
<script setup lang="ts">
|
|
1082
|
+
import { ref, computed, watch, onMounted } from "vue";
|
|
1083
|
+
import { useDataTableSort, useDataTablePagination, useTableSearch } from "@vc-shell/framework";
|
|
1084
|
+
|
|
1085
|
+
const STATE_KEY = "offers_list";
|
|
1084
1086
|
|
|
1085
|
-
const PAGE_SIZE = 20;
|
|
1086
|
-
const { debounce } = useFunctions();
|
|
1087
1087
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
1088
|
+
stateKey: STATE_KEY,
|
|
1088
1089
|
initialField: "createdDate",
|
|
1089
1090
|
initialDirection: "DESC",
|
|
1090
1091
|
});
|
|
1091
|
-
const searchValue =
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const restored = useTableQueryState("offers_list").read();
|
|
1096
|
-
if (restored.sort) {
|
|
1097
|
-
const [field, dir] = restored.sort.split(":");
|
|
1098
|
-
sortField.value = field;
|
|
1099
|
-
sortOrder.value = dir === "DESC" ? -1 : 1;
|
|
1100
|
-
}
|
|
1101
|
-
if (restored.search) searchValue.value = restored.search;
|
|
1102
|
-
if (restored.page) currentPage.value = restored.page;
|
|
1092
|
+
const { searchValue } = useTableSearch({ stateKey: STATE_KEY });
|
|
1093
|
+
|
|
1094
|
+
const searchResult = ref<{ results: Offer[]; totalCount: number }>();
|
|
1095
|
+
const totalCount = computed(() => searchResult.value?.totalCount ?? 0);
|
|
1103
1096
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1097
|
+
const pagination = useDataTablePagination({
|
|
1098
|
+
stateKey: STATE_KEY,
|
|
1099
|
+
pageSize: 20,
|
|
1100
|
+
totalCount,
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
async function load() {
|
|
1104
|
+
searchResult.value = await api.searchOffers({
|
|
1106
1105
|
sort: sortExpression.value,
|
|
1107
|
-
keyword: searchValue.value
|
|
1108
|
-
skip:
|
|
1106
|
+
keyword: searchValue.value,
|
|
1107
|
+
skip: pagination.skip,
|
|
1108
|
+
take: pagination.pageSize,
|
|
1109
1109
|
});
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
|
-
load();
|
|
1113
|
-
watch(
|
|
1114
|
-
|
|
1112
|
+
onMounted(() => load());
|
|
1113
|
+
watch(sortExpression, () => load());
|
|
1114
|
+
</script>
|
|
1115
1115
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1116
|
+
<template>
|
|
1117
|
+
<VcDataTable
|
|
1118
|
+
:items="searchResult?.results ?? []"
|
|
1119
|
+
:total-count="pagination.totalCount"
|
|
1120
|
+
:pagination="pagination"
|
|
1121
|
+
v-model:sort-field="sortField"
|
|
1122
|
+
v-model:sort-order="sortOrder"
|
|
1123
|
+
v-model:search-value="searchValue"
|
|
1124
|
+
@pagination-click="pagination.goToPage"
|
|
1125
|
+
state-key="offers_list"
|
|
1126
|
+
>
|
|
1127
|
+
<VcColumn
|
|
1128
|
+
id="name"
|
|
1129
|
+
header="Name"
|
|
1130
|
+
sortable
|
|
1131
|
+
/>
|
|
1132
|
+
<VcColumn
|
|
1133
|
+
id="createdDate"
|
|
1134
|
+
header="Created"
|
|
1135
|
+
sortable
|
|
1136
|
+
/>
|
|
1137
|
+
</VcDataTable>
|
|
1138
|
+
</template>
|
|
1119
1139
|
```
|
|
1120
1140
|
|
|
1121
|
-
|
|
1141
|
+
The `state-key` on `VcDataTable` here persists column layout to localStorage; the `stateKey` passed to the composables persists query state to the URL. Both use the same string value but serve different purposes.
|
|
1122
1142
|
|
|
1123
1143
|
---
|
|
1124
1144
|
|
|
@@ -29,6 +29,7 @@ import { useDataTablePagination } from "@vc-shell/framework";
|
|
|
29
29
|
const pagination = useDataTablePagination({
|
|
30
30
|
pageSize: 20,
|
|
31
31
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
32
|
+
onPageChange: ({ skip }) => loadItems({ ...searchQuery.value, skip }),
|
|
32
33
|
});
|
|
33
34
|
```
|
|
34
35
|
|
|
@@ -36,30 +37,29 @@ const pagination = useDataTablePagination({
|
|
|
36
37
|
<VcDataTable :items="items" :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
> **Note:** Let `@pagination-click` only update the page (`goToPage`) and load from one watcher that reads `pagination.skip` together with sort and search — see the recipe below. `onPageChange` (load on each page change) still works for simple tables, but do not combine it with that watcher or the page loads twice.
|
|
40
|
-
|
|
41
40
|
## API
|
|
42
41
|
|
|
43
42
|
### Parameters (Options)
|
|
44
43
|
|
|
45
|
-
| Option | Type | Default | Description
|
|
46
|
-
| -------------- | ------------------------------------------------- | ----------- |
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
| -------------- | ------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------- |
|
|
46
|
+
| `stateKey` | `string \| undefined` | `undefined` | When set, restores and persists the current page to the blade URL query under this key |
|
|
47
|
+
| `totalCount` | `MaybeRefOrGetter<number>` | _required_ | Total item count from API response |
|
|
48
|
+
| `pageSize` | `MaybeRefOrGetter<number>` | `20` | Items per page |
|
|
49
|
+
| `onPageChange` | `(state: { page: number; skip: number }) => void` | `undefined` | Event callback fired when `goToPage()` is called |
|
|
50
50
|
|
|
51
51
|
### Returns (`reactive()` object)
|
|
52
52
|
|
|
53
|
-
| Property | Type | Description
|
|
54
|
-
| ------------- | ------------------------ |
|
|
55
|
-
| `currentPage` | `number` | Current 1-based page number (writable)
|
|
56
|
-
| `pages` | `number` (readonly) | Total number of pages
|
|
57
|
-
| `skip` | `number` (readonly) | Current skip offset for API calls
|
|
58
|
-
| `pageSize` | `number` (readonly) | Resolved page size
|
|
59
|
-
| `totalCount` | `number` (readonly) | Resolved total item count
|
|
60
|
-
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange`
|
|
61
|
-
| `setPage` | `(page: number) => void` |
|
|
62
|
-
| `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange`
|
|
53
|
+
| Property | Type | Description |
|
|
54
|
+
| ------------- | ------------------------ | ---------------------------------------------------------------------------- |
|
|
55
|
+
| `currentPage` | `number` | Current 1-based page number (writable) |
|
|
56
|
+
| `pages` | `number` (readonly) | Total number of pages |
|
|
57
|
+
| `skip` | `number` (readonly) | Current skip offset for API calls |
|
|
58
|
+
| `pageSize` | `number` (readonly) | Resolved page size |
|
|
59
|
+
| `totalCount` | `number` (readonly) | Resolved total item count |
|
|
60
|
+
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange` |
|
|
61
|
+
| `setPage` | `(page: number) => void` | Set the page without firing `onPageChange` (used to seed from a URL restore) |
|
|
62
|
+
| `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
|
|
63
63
|
|
|
64
64
|
All properties are auto-unwrapped by `reactive()` — no `.value` access needed in script or template.
|
|
65
65
|
|
|
@@ -67,43 +67,38 @@ All properties are auto-unwrapped by `reactive()` — no `.value` access needed
|
|
|
67
67
|
|
|
68
68
|
```vue
|
|
69
69
|
<script setup lang="ts">
|
|
70
|
-
import { ref, computed, watch } from "vue";
|
|
71
|
-
import { useDataTablePagination, useDataTableSort
|
|
70
|
+
import { ref, computed, watch, onMounted } from "vue";
|
|
71
|
+
import { useDataTablePagination, useDataTableSort } from "@vc-shell/framework";
|
|
72
|
+
|
|
73
|
+
const STATE_KEY = "products_list";
|
|
72
74
|
|
|
73
75
|
// Data layer
|
|
74
76
|
const searchResult = ref<{ results: Item[]; totalCount: number }>();
|
|
75
77
|
|
|
76
|
-
async function loadItems(
|
|
77
|
-
searchResult.value = await api.search({
|
|
78
|
+
async function loadItems() {
|
|
79
|
+
searchResult.value = await api.search({
|
|
80
|
+
sort: sortExpression.value,
|
|
81
|
+
skip: pagination.skip,
|
|
82
|
+
take: pagination.pageSize,
|
|
83
|
+
});
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
//
|
|
81
|
-
const pagination = useDataTablePagination({
|
|
82
|
-
pageSize: 20,
|
|
83
|
-
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
84
|
-
});
|
|
86
|
+
// Sort state
|
|
85
87
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
88
|
+
stateKey: STATE_KEY,
|
|
86
89
|
initialField: "createdDate",
|
|
87
90
|
initialDirection: "DESC",
|
|
88
91
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (restored.page) pagination.setPage(restored.page);
|
|
100
|
-
|
|
101
|
-
// One loader reading sort + search + page.
|
|
102
|
-
watch(
|
|
103
|
-
() => ({ sort: sortExpression.value, keyword: searchValue.value || undefined, skip: pagination.skip }),
|
|
104
|
-
(query) => loadItems(query),
|
|
105
|
-
{ immediate: true },
|
|
106
|
-
);
|
|
92
|
+
|
|
93
|
+
// Pagination state
|
|
94
|
+
const pagination = useDataTablePagination({
|
|
95
|
+
stateKey: STATE_KEY,
|
|
96
|
+
pageSize: 20,
|
|
97
|
+
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
onMounted(() => loadItems());
|
|
101
|
+
watch(sortExpression, () => loadItems());
|
|
107
102
|
</script>
|
|
108
103
|
|
|
109
104
|
<template>
|
|
@@ -112,12 +107,6 @@ watch(
|
|
|
112
107
|
:total-count="pagination.totalCount"
|
|
113
108
|
:pagination="pagination"
|
|
114
109
|
@pagination-click="pagination.goToPage"
|
|
115
|
-
@search="
|
|
116
|
-
(kw) => {
|
|
117
|
-
searchValue = kw;
|
|
118
|
-
pagination.setPage(1);
|
|
119
|
-
}
|
|
120
|
-
"
|
|
121
110
|
v-model:sort-field="sortField"
|
|
122
111
|
v-model:sort-order="sortOrder"
|
|
123
112
|
>
|
|
@@ -151,6 +140,7 @@ export function useOffers() {
|
|
|
151
140
|
const pagination = useDataTablePagination({
|
|
152
141
|
pageSize: 20,
|
|
153
142
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
143
|
+
onPageChange: ({ skip }) => loadOffers({ ...searchQuery.value, skip }),
|
|
154
144
|
});
|
|
155
145
|
|
|
156
146
|
return {
|
|
@@ -161,7 +151,7 @@ export function useOffers() {
|
|
|
161
151
|
}
|
|
162
152
|
```
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
Blade then simply binds:
|
|
165
155
|
|
|
166
156
|
```vue
|
|
167
157
|
<VcDataTable :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
@@ -170,12 +160,12 @@ The blade owns the single loader watch (reading `pagination.skip` with sort/sear
|
|
|
170
160
|
## Details
|
|
171
161
|
|
|
172
162
|
- **`reactive()` return**: The composable wraps its return in `reactive()`, so all `Ref`/`ComputedRef` properties are auto-unwrapped. Access with `pagination.pages` (not `pagination.pages.value`). This allows the object to be passed directly as `:pagination` prop to VcDataTable without intermediate conversion.
|
|
173
|
-
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not
|
|
174
|
-
- **`setPage` vs `goToPage`**: `goToPage(n)` updates the page and fires `onPageChange`; `setPage(n)` updates it without the callback. Use `setPage` to seed the page from a URL restore so the seed itself does not load.
|
|
163
|
+
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not know about data fetching -- it just notifies.
|
|
175
164
|
- **No auto-clamp**: When `totalCount` decreases (e.g. after deletion), `currentPage` is NOT automatically clamped. Call `reset()` or `goToPage(1)` explicitly after mutations.
|
|
176
165
|
- **reset() is silent**: `reset()` sets `currentPage = 1` but does NOT fire `onPageChange`. This prevents accidental double-fetches when the consumer resets pagination during a reload.
|
|
177
|
-
- **Pure without callback**: Omit `onPageChange` and the composable works as pure state
|
|
166
|
+
- **Pure without callback**: Omit `onPageChange` and the composable works as pure state -- useful for unit tests or when the consumer prefers to watch properties reactively.
|
|
178
167
|
- **Why `reactive()` and not `ref()`**: Pagination is a cohesive group of properties always used together (`pagination.xxx`). `reactive()` is the Vue-idiomatic choice for such objects. `useDataTableSort` returns `ref()`s because its properties are destructured and used with `v-model` individually.
|
|
168
|
+
- **URL state (stateKey)**: When `stateKey` is provided, the composable reads the current page from the blade URL query on creation (via `setPage`, which does not fire `onPageChange`) and persists it on every `goToPage` call. Without `stateKey`, behavior is unchanged.
|
|
179
169
|
|
|
180
170
|
## Tips
|
|
181
171
|
|
|
@@ -46,10 +46,11 @@ const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
|
|
|
46
46
|
|
|
47
47
|
### Parameters (Options)
|
|
48
48
|
|
|
49
|
-
| Option | Type
|
|
50
|
-
| ------------------ |
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
| ------------------ | --------------------- | ----------- | -------------------------------------------------------------------------- |
|
|
51
|
+
| `stateKey` | `string \| undefined` | `undefined` | When set, restores and persists sort to the blade URL query under this key |
|
|
52
|
+
| `initialField` | `string` | `undefined` | Column field to sort by initially |
|
|
53
|
+
| `initialDirection` | `"ASC" \| "DESC"` | `undefined` | Initial sort direction |
|
|
53
54
|
|
|
54
55
|
### Returns
|
|
55
56
|
|
|
@@ -74,26 +75,25 @@ When `sortOrder` is `0`, `sortExpression` returns `undefined` regardless of `sor
|
|
|
74
75
|
|
|
75
76
|
```vue
|
|
76
77
|
<script setup lang="ts">
|
|
77
|
-
import { ref, watch } from "vue";
|
|
78
|
+
import { ref, watch, onMounted } from "vue";
|
|
78
79
|
import { useDataTableSort } from "@vc-shell/framework";
|
|
79
80
|
|
|
80
81
|
const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
|
|
82
|
+
stateKey: "products_list",
|
|
81
83
|
initialField: "createdDate",
|
|
82
84
|
initialDirection: "DESC",
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
const items = ref([]);
|
|
86
88
|
const loading = ref(false);
|
|
87
|
-
const currentPage = ref(1);
|
|
88
|
-
const PAGE_SIZE = 20;
|
|
89
89
|
|
|
90
|
-
async function loadItems(
|
|
90
|
+
async function loadItems() {
|
|
91
91
|
loading.value = true;
|
|
92
92
|
try {
|
|
93
93
|
const response = await api.searchProducts({
|
|
94
|
-
sort:
|
|
95
|
-
skip:
|
|
96
|
-
take:
|
|
94
|
+
sort: sortExpression.value, // e.g. "createdDate:DESC" or undefined
|
|
95
|
+
skip: 0,
|
|
96
|
+
take: 20,
|
|
97
97
|
});
|
|
98
98
|
items.value = response.results;
|
|
99
99
|
} finally {
|
|
@@ -101,13 +101,8 @@ async function loadItems(query: { sort?: string; skip?: number }) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
watch(
|
|
107
|
-
() => ({ sort: sortExpression.value, skip: (currentPage.value - 1) * PAGE_SIZE }),
|
|
108
|
-
(query) => loadItems(query),
|
|
109
|
-
{ immediate: true },
|
|
110
|
-
);
|
|
104
|
+
onMounted(() => loadItems());
|
|
105
|
+
watch(sortExpression, () => loadItems());
|
|
111
106
|
</script>
|
|
112
107
|
|
|
113
108
|
<template>
|
|
@@ -141,13 +136,13 @@ watch(
|
|
|
141
136
|
- **Sort expression format**: `sortExpression` returns `"field:DIRECTION"` when both `sortField` and a non-zero `sortOrder` are set, otherwise `undefined`. This format is directly compatible with VirtoCommerce Platform search endpoints.
|
|
142
137
|
- **Reset behavior**: `resetSort()` restores `sortField` and `sortOrder` to the values passed at construction time. If no initial options were provided, both are cleared.
|
|
143
138
|
- **Numeric order convention**: `VcDataTable` uses PrimeVue's numeric sort order convention (`1`/`-1`/`0`). This composable encapsulates the mapping so call sites never need to handle the numeric values directly.
|
|
144
|
-
- **Stateless regarding data**: The composable only manages the sort field and direction.
|
|
139
|
+
- **Stateless regarding data**: The composable only manages the sort field and direction. Combine it with your own data-fetching logic using `watch(sortExpression, ...)`.
|
|
140
|
+
- **URL state (stateKey)**: When `stateKey` is provided, the composable reads its slice from the blade URL query on creation and writes back on every change (via `router.replace`). Without `stateKey`, behavior is unchanged -- `sortField` and `sortOrder` are plain refs with no URL interaction.
|
|
145
141
|
|
|
146
142
|
## Tips
|
|
147
143
|
|
|
148
144
|
- Always mark columns as `sortable` in your `<VcColumn>` definitions for the sort indicator icon to appear.
|
|
149
|
-
- Use `{ immediate: true }` on the
|
|
150
|
-
- On reload, seed `sortField`/`sortOrder` from `useTableQueryState(stateKey).read().sort` (`"field:DIR"`) before the loader watcher, so restore costs a single request.
|
|
145
|
+
- Use `{ immediate: true }` on the `watch(sortExpression, ...)` call to trigger the initial data load with the correct sort on mount.
|
|
151
146
|
- Call `resetSort()` alongside any "clear all filters" action to keep sort state consistent with the rest of the filter UI.
|
|
152
147
|
|
|
153
148
|
## Related
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useTableSearch
|
|
3
|
+
category: composables
|
|
4
|
+
group: data
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# useTableSearch
|
|
8
|
+
|
|
9
|
+
Manages page-level search state for `VcDataTable`. Returns a `searchValue` ref for `v-model:search-value` binding. When `stateKey` is provided, the keyword is restored from the blade URL on creation and persisted to the URL on change.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- You are using `VcDataTable` with `v-model:search-value`
|
|
14
|
+
- You want to share the search keyword with `useDataTableSort` and `useDataTablePagination` under the same `stateKey`
|
|
15
|
+
- You want URL-persistent search without wiring blade query params manually
|
|
16
|
+
|
|
17
|
+
## When NOT to Use
|
|
18
|
+
|
|
19
|
+
- The table filters client-side only and you do not need external search state
|
|
20
|
+
- You are using a custom search input unrelated to `VcDataTable`
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { useTableSearch } from "@vc-shell/framework";
|
|
26
|
+
|
|
27
|
+
const { searchValue } = useTableSearch({
|
|
28
|
+
stateKey: "offers_list",
|
|
29
|
+
initial: "",
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<VcDataTable v-model:search-value="searchValue" :items="items" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### Parameters (Options)
|
|
40
|
+
|
|
41
|
+
| Option | Type | Default | Description |
|
|
42
|
+
| ---------- | --------------------- | ----------- | ---------------------------------------------------------------------------------------- |
|
|
43
|
+
| `stateKey` | `string \| undefined` | `undefined` | When set, restores and persists the search keyword to the blade URL query under this key |
|
|
44
|
+
| `initial` | `string \| undefined` | `undefined` | Initial search value used when no URL state is present |
|
|
45
|
+
|
|
46
|
+
### Returns
|
|
47
|
+
|
|
48
|
+
| Property | Type | Description |
|
|
49
|
+
| ------------- | ------------- | -------------------------------------------------------- |
|
|
50
|
+
| `searchValue` | `Ref<string>` | Current search keyword; bind with `v-model:search-value` |
|
|
51
|
+
|
|
52
|
+
## Details
|
|
53
|
+
|
|
54
|
+
- With `stateKey`, the composable reads the current URL query on creation and sets `searchValue` from it. Each write to `searchValue` updates the URL query (using `router.replace`, no history entry added). The URL key used is `<stateKey>_search`.
|
|
55
|
+
- Without `stateKey`, `searchValue` is a plain `ref` with no URL interaction.
|
|
56
|
+
- Use the same `stateKey` value for `useDataTableSort`, `useDataTablePagination`, and `useTableSearch` on the same list page so all three share a consistent URL namespace.
|
|
57
|
+
|
|
58
|
+
## Tips
|
|
59
|
+
|
|
60
|
+
- Reset `searchValue` to `""` alongside `pagination.reset()` when clearing filters so the URL stays clean.
|
|
61
|
+
- The composable does not trigger data loads on its own. Watch `searchValue` or combine it in a unified `watch` with `sortExpression` and `pagination.currentPage`.
|
|
62
|
+
|
|
63
|
+
## Recipe: List Blade with URL State
|
|
64
|
+
|
|
65
|
+
```vue
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { ref, computed, watch, onMounted } from "vue";
|
|
68
|
+
import { useDataTableSort, useDataTablePagination, useTableSearch } from "@vc-shell/framework";
|
|
69
|
+
|
|
70
|
+
const STATE_KEY = "offers_list";
|
|
71
|
+
|
|
72
|
+
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
73
|
+
stateKey: STATE_KEY,
|
|
74
|
+
initialField: "createdDate",
|
|
75
|
+
initialDirection: "DESC",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const { searchValue } = useTableSearch({ stateKey: STATE_KEY });
|
|
79
|
+
|
|
80
|
+
const searchResult = ref<{ results: Offer[]; totalCount: number }>();
|
|
81
|
+
const totalCount = computed(() => searchResult.value?.totalCount ?? 0);
|
|
82
|
+
|
|
83
|
+
const pagination = useDataTablePagination({
|
|
84
|
+
stateKey: STATE_KEY,
|
|
85
|
+
pageSize: 20,
|
|
86
|
+
totalCount,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
async function load() {
|
|
90
|
+
searchResult.value = await api.searchOffers({
|
|
91
|
+
sort: sortExpression.value,
|
|
92
|
+
keyword: searchValue.value,
|
|
93
|
+
skip: pagination.skip,
|
|
94
|
+
take: pagination.pageSize,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onMounted(() => load());
|
|
99
|
+
watch(sortExpression, () => load());
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<VcDataTable
|
|
104
|
+
:items="searchResult?.results ?? []"
|
|
105
|
+
:total-count="pagination.totalCount"
|
|
106
|
+
:pagination="pagination"
|
|
107
|
+
v-model:sort-field="sortField"
|
|
108
|
+
v-model:sort-order="sortOrder"
|
|
109
|
+
v-model:search-value="searchValue"
|
|
110
|
+
@pagination-click="pagination.goToPage"
|
|
111
|
+
>
|
|
112
|
+
<VcColumn
|
|
113
|
+
id="name"
|
|
114
|
+
header="Name"
|
|
115
|
+
sortable
|
|
116
|
+
/>
|
|
117
|
+
<VcColumn
|
|
118
|
+
id="createdDate"
|
|
119
|
+
header="Created"
|
|
120
|
+
sortable
|
|
121
|
+
/>
|
|
122
|
+
</VcDataTable>
|
|
123
|
+
</template>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Related
|
|
127
|
+
|
|
128
|
+
- [`useDataTableSort`](./useDataTableSort.docs.md) -- sort state composable for VcDataTable
|
|
129
|
+
- [`useDataTablePagination`](./useDataTablePagination.docs.md) -- pagination state composable for VcDataTable
|
|
130
|
+
- `VcDataTable` -- table component that accepts `v-model:search-value`
|
|
@@ -11,21 +11,21 @@ Replace the imperative `VcTable` component (columns array, manual sort handlers,
|
|
|
11
11
|
|
|
12
12
|
Full prop/event mapping:
|
|
13
13
|
|
|
14
|
-
| VcTable prop/event | VcDataTable equivalent
|
|
15
|
-
| -------------------------- |
|
|
16
|
-
| `:columns="columns"` | `<VcColumn>` child components (see RULE 2)
|
|
17
|
-
| `:selected-item-id="id"` | `v-model:active-item-id="id"`
|
|
18
|
-
| `:sort="sortExpression"` | `v-model:sort-field` + `v-model:sort-order`
|
|
19
|
-
| `:multiselect="true"` | `:selection-mode="'multiple'"`
|
|
20
|
-
| `@selection-changed="fn"` | `v-model:selection="ref"`
|
|
21
|
-
| `:search-value="val"` |
|
|
22
|
-
| `@search:change="fn"` | `@search="fn"`
|
|
23
|
-
| `@item-click="fn"` | `@row-click="fn"` (different signature)
|
|
24
|
-
| `@header-click="fn"` | Remove (sort is declarative via v-model)
|
|
25
|
-
| `@scroll:ptr="fn"` | `:pull-to-refresh="true"` + `@pull-refresh="fn"`
|
|
26
|
-
| `:pages` + `:current-page` | `:pagination="{
|
|
27
|
-
| `:active-filter-count` | Remove (managed by global-filters internally)
|
|
28
|
-
| `<!--@vue-generic {T}-->` | Remove (no longer needed)
|
|
14
|
+
| VcTable prop/event | VcDataTable equivalent |
|
|
15
|
+
| -------------------------- | ----------------------------------------------------------------------- |
|
|
16
|
+
| `:columns="columns"` | `<VcColumn>` child components (see RULE 2) |
|
|
17
|
+
| `:selected-item-id="id"` | `v-model:active-item-id="id"` |
|
|
18
|
+
| `:sort="sortExpression"` | `v-model:sort-field` + `v-model:sort-order` |
|
|
19
|
+
| `:multiselect="true"` | `:selection-mode="'multiple'"` |
|
|
20
|
+
| `@selection-changed="fn"` | `v-model:selection="ref"` |
|
|
21
|
+
| `:search-value="val"` | `v-model:search-value` bound to `useTableSearch({ stateKey? })` |
|
|
22
|
+
| `@search:change="fn"` | `@search="fn"` (or drop; use watcher on `searchValue`) |
|
|
23
|
+
| `@item-click="fn"` | `@row-click="fn"` (different signature) |
|
|
24
|
+
| `@header-click="fn"` | Remove (sort is declarative via v-model) |
|
|
25
|
+
| `@scroll:ptr="fn"` | `:pull-to-refresh="true"` + `@pull-refresh="fn"` |
|
|
26
|
+
| `:pages` + `:current-page` | `:pagination="pagination"` from `useDataTablePagination({ stateKey? })` |
|
|
27
|
+
| `:active-filter-count` | Remove (managed by global-filters internally) |
|
|
28
|
+
| `<!--@vue-generic {T}-->` | Remove (no longer needed) |
|
|
29
29
|
|
|
30
30
|
**BEFORE:**
|
|
31
31
|
|
|
@@ -310,7 +310,7 @@ The Props interface keeps `columns` — the parent still passes column definitio
|
|
|
310
310
|
|
|
311
311
|
## RULE 3: Sort Composable Replacement
|
|
312
312
|
|
|
313
|
-
Replace `useTableSort` with `useDataTableSort`. The new composable returns `sortField` and `sortOrder` as separate refs that bind directly to `v-model:sort-field` and `v-model:sort-order`. Remove the `onHeaderClick` handler entirely.
|
|
313
|
+
Replace `useTableSort` with `useDataTableSort`. The new composable returns `sortField` and `sortOrder` as separate refs that bind directly to `v-model:sort-field` and `v-model:sort-order`. Remove the `onHeaderClick` handler entirely. Pass `stateKey` to persist sort to the blade URL (opt-in; omit for no URL persistence).
|
|
314
314
|
|
|
315
315
|
**BEFORE:**
|
|
316
316
|
|
|
@@ -339,6 +339,7 @@ watch(sortExpression, async () => {
|
|
|
339
339
|
import { useDataTableSort } from "@vc-shell/framework";
|
|
340
340
|
|
|
341
341
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
342
|
+
stateKey: "my_list", // optional: persists sort to blade URL
|
|
342
343
|
initialField: "createdDate",
|
|
343
344
|
initialDirection: "DESC",
|
|
344
345
|
});
|
|
@@ -684,10 +685,14 @@ Key points:
|
|
|
684
685
|
- The `@filter` event fires when the user applies filters. The `event.filters` object is keyed by filter `id`, with values matching the selected option values.
|
|
685
686
|
- The active filter count badge is managed internally by VcDataTable — no need for a manual `activeFilterCount` computed.
|
|
686
687
|
|
|
687
|
-
## RULE 8: Pagination —
|
|
688
|
+
## RULE 8: Pagination and Search — useDataTablePagination + useTableSearch
|
|
688
689
|
|
|
689
690
|
Replace manual `pages`/`currentPage` computed properties and `@pagination-click` handlers with the `useDataTablePagination` composable. The composable manages skip/page math internally and returns a reactive pagination object that VcDataTable consumes directly.
|
|
690
691
|
|
|
692
|
+
Pass `stateKey` to persist the current page to the blade URL (opt-in). The table `state-key` prop is column-layout persistence only and does not control URL query state.
|
|
693
|
+
|
|
694
|
+
For search, replace a bare `ref("")` with `useTableSearch({ stateKey? })`. The returned `searchValue` ref binds to `v-model:search-value` on VcDataTable. With `stateKey`, the keyword is persisted to the blade URL. Without `stateKey`, it behaves like a plain ref.
|
|
695
|
+
|
|
691
696
|
**BEFORE (composable):**
|
|
692
697
|
|
|
693
698
|
```typescript
|
|
@@ -733,6 +738,7 @@ export function useMyList(options?: { pageSize?: number }) {
|
|
|
733
738
|
});
|
|
734
739
|
|
|
735
740
|
const pagination = useDataTablePagination({
|
|
741
|
+
stateKey: "my_list", // optional: persists current page to blade URL
|
|
736
742
|
pageSize,
|
|
737
743
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
738
744
|
onPageChange: ({ skip }) => loadItems({ ...searchQuery.value, skip }),
|
|
@@ -778,12 +784,15 @@ const { items, pagination, loadItems, loading } = useMyList();
|
|
|
778
784
|
- `totalCount`, `pages`, `currentPage` computed from composable return
|
|
779
785
|
- Manual `onPaginationClick` function in blade pages
|
|
780
786
|
- Manual `skip` calculation `(page - 1) * pageSize`
|
|
787
|
+
- Any `useTableQueryPersistence` or `useTableQueryState().read()` calls — URL query persistence is now handled by `stateKey` on the composables
|
|
781
788
|
|
|
782
789
|
**What to add:**
|
|
783
790
|
|
|
784
|
-
- `useDataTablePagination` import in composable
|
|
791
|
+
- `useDataTablePagination` import in composable (with optional `stateKey` for URL persistence)
|
|
792
|
+
- `useTableSearch({ stateKey? })` in the blade to replace a bare `ref("")` for search
|
|
785
793
|
- `pagination` object in composable return
|
|
786
794
|
- `:pagination="pagination"` and `@pagination-click="pagination.goToPage"` in template
|
|
795
|
+
- `v-model:search-value="searchValue"` from `useTableSearch` in template
|
|
787
796
|
|
|
788
797
|
## Verification
|
|
789
798
|
|
|
@@ -20,12 +20,14 @@ Generic worked example for a typical list blade (search, sort, pagination, row a
|
|
|
20
20
|
class="tw-grow tw-basis-0"
|
|
21
21
|
:loading="loading"
|
|
22
22
|
:items="xxxList"
|
|
23
|
-
:total-count="totalCount"
|
|
24
|
-
:pagination="
|
|
23
|
+
:total-count="pagination.totalCount"
|
|
24
|
+
:pagination="pagination"
|
|
25
25
|
v-model:active-item-id="selectedItemId"
|
|
26
26
|
state-key="xxx_list"
|
|
27
27
|
v-model:sort-field="sortField"
|
|
28
28
|
v-model:sort-order="sortOrder"
|
|
29
|
+
v-model:search-value="searchValue"
|
|
30
|
+
:searchable="true"
|
|
29
31
|
:pull-to-refresh="true"
|
|
30
32
|
:empty-state="{
|
|
31
33
|
icon: 'lucide-<icon>',
|
|
@@ -34,7 +36,7 @@ Generic worked example for a typical list blade (search, sort, pagination, row a
|
|
|
34
36
|
actionHandler: onAddItem,
|
|
35
37
|
}"
|
|
36
38
|
@row-click="onItemClick"
|
|
37
|
-
@pagination-click="
|
|
39
|
+
@pagination-click="pagination.goToPage"
|
|
38
40
|
@pull-refresh="reload"
|
|
39
41
|
>
|
|
40
42
|
<!-- Each VcColumn is declared explicitly in the template (NOT via v-for).
|
|
@@ -87,9 +89,11 @@ Generic worked example for a typical list blade (search, sort, pagination, row a
|
|
|
87
89
|
|
|
88
90
|
**Key VcDataTable props:**
|
|
89
91
|
|
|
90
|
-
- `state-key` — unique string key for persisting column widths/order/visibility to localStorage. Use snake_case module name (e.g., `"team_list"`, `"catalog_list"`).
|
|
92
|
+
- `state-key` — unique string key for persisting column widths/order/visibility to localStorage. Use snake_case module name (e.g., `"team_list"`, `"catalog_list"`). This prop controls column layout only — URL query persistence (sort/search/page) comes from the composables' `stateKey` option.
|
|
91
93
|
- `v-model:active-item-id` — highlights the currently open row. Bind to `selectedItemId` ref.
|
|
92
94
|
- `v-model:sort-field` and `v-model:sort-order` — two-way sort binding from `useDataTableSort`.
|
|
95
|
+
- `v-model:search-value` and `:searchable="true"` — two-way search binding from `useTableSearch`.
|
|
96
|
+
- `:pagination="pagination"` and `@pagination-click="pagination.goToPage"` — pass the reactive object from `useDataTablePagination` directly.
|
|
93
97
|
- `:pull-to-refresh="true"` — enables mobile pull-to-refresh gesture.
|
|
94
98
|
- `:empty-state` — object with `icon`, `title`, `actionLabel`, `actionHandler`.
|
|
95
99
|
|
|
@@ -191,7 +195,8 @@ If the API defines an enum of statuses, populate the map from those values. If s
|
|
|
191
195
|
```vue
|
|
192
196
|
<script lang="ts" setup>
|
|
193
197
|
import { ref, computed, watch, onMounted } from "vue";
|
|
194
|
-
import { IBladeToolbar, useBlade, useDataTableSort } from "@vc-shell/framework";
|
|
198
|
+
import { IBladeToolbar, useBlade, useDataTableSort, useDataTablePagination, useTableSearch } from "@vc-shell/framework";
|
|
199
|
+
import { debounce } from "lodash-es";
|
|
195
200
|
import useXxxs from "../composables/useXxxs";
|
|
196
201
|
import { useI18n } from "vue-i18n";
|
|
197
202
|
|
|
@@ -211,19 +216,32 @@ defineBlade({
|
|
|
211
216
|
// 2. Core composables
|
|
212
217
|
const { openBlade, exposeToChildren, param } = useBlade();
|
|
213
218
|
const { t } = useI18n({ useScope: "global" });
|
|
214
|
-
const { getXxxs,
|
|
219
|
+
const { getXxxs, loading, xxxList, totalCount } = useXxxs();
|
|
215
220
|
|
|
216
|
-
// 3. Sort —
|
|
221
|
+
// 3. Sort — stateKey persists sort to blade URL; omit stateKey to opt out
|
|
217
222
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
223
|
+
stateKey: "xxx_list",
|
|
218
224
|
initialField: "createdDate",
|
|
219
225
|
initialDirection: "DESC",
|
|
220
226
|
});
|
|
221
227
|
|
|
222
|
-
// 4.
|
|
228
|
+
// 4. Search — stateKey persists keyword to blade URL; omit stateKey to opt out
|
|
229
|
+
const { searchValue } = useTableSearch({ stateKey: "xxx_list" });
|
|
230
|
+
|
|
231
|
+
// 5. Pagination — stateKey persists current page to blade URL; omit stateKey to opt out
|
|
232
|
+
// Note: state-key on <VcDataTable> is column-layout persistence only (localStorage).
|
|
233
|
+
// URL query persistence (sort/search/page) is controlled by the stateKey option here.
|
|
234
|
+
const pagination = useDataTablePagination({
|
|
235
|
+
stateKey: "xxx_list",
|
|
236
|
+
pageSize: 20,
|
|
237
|
+
totalCount,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 6. UI state
|
|
223
241
|
const selectedItemId = ref<string | undefined>();
|
|
224
242
|
const title = computed(() => t("XXX.PAGES.LIST.TITLE"));
|
|
225
243
|
|
|
226
|
-
//
|
|
244
|
+
// 7. Toolbar
|
|
227
245
|
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
228
246
|
{
|
|
229
247
|
id: "refresh",
|
|
@@ -241,7 +259,7 @@ const bladeToolbar = ref<IBladeToolbar[]>([
|
|
|
241
259
|
},
|
|
242
260
|
]);
|
|
243
261
|
|
|
244
|
-
//
|
|
262
|
+
// 8. Status variant mapping (if status column exists)
|
|
245
263
|
const statusVariant = (status: string | undefined): "info" | "warning" | "danger" | "success" | "primary" => {
|
|
246
264
|
const map: Record<string, "info" | "warning" | "danger" | "success" | "primary"> = {
|
|
247
265
|
// TODO: adjust status variants to match your API statuses
|
|
@@ -256,12 +274,23 @@ const statusVariant = (status: string | undefined): "info" | "warning" | "danger
|
|
|
256
274
|
return map[status ?? ""] ?? "info";
|
|
257
275
|
};
|
|
258
276
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
await getXxxs({
|
|
262
|
-
|
|
277
|
+
// 9. Load function — explicit; called on mount and by watchers
|
|
278
|
+
const load = async () => {
|
|
279
|
+
await getXxxs({
|
|
280
|
+
sort: sortExpression.value,
|
|
281
|
+
keyword: searchValue.value || undefined,
|
|
282
|
+
skip: pagination.skip,
|
|
283
|
+
take: 20,
|
|
284
|
+
});
|
|
285
|
+
};
|
|
263
286
|
|
|
264
|
-
//
|
|
287
|
+
// 10. Reset to page 1 when search changes, then reload
|
|
288
|
+
watch(searchValue, () => pagination.setPage(1));
|
|
289
|
+
|
|
290
|
+
// 11. Reload when sort, search, or page changes (debounced to coalesce rapid changes)
|
|
291
|
+
watch([sortExpression, searchValue, () => pagination.skip], debounce(load, 300));
|
|
292
|
+
|
|
293
|
+
// 12. Track selected row when param changes (e.g., navigating directly to URL)
|
|
265
294
|
watch(
|
|
266
295
|
() => param.value,
|
|
267
296
|
() => {
|
|
@@ -270,30 +299,15 @@ watch(
|
|
|
270
299
|
{ immediate: true },
|
|
271
300
|
);
|
|
272
301
|
|
|
273
|
-
//
|
|
274
|
-
onMounted(
|
|
275
|
-
await reload();
|
|
276
|
-
});
|
|
302
|
+
// 13. Load data on mount
|
|
303
|
+
onMounted(() => load());
|
|
277
304
|
|
|
278
|
-
//
|
|
305
|
+
// 14. Reload helper — used by toolbar and exposed to child blades
|
|
279
306
|
const reload = async () => {
|
|
280
|
-
await
|
|
281
|
-
...searchQuery.value,
|
|
282
|
-
skip: (currentPage.value - 1) * (searchQuery.value?.take ?? 20),
|
|
283
|
-
sort: sortExpression.value,
|
|
284
|
-
});
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// 11. Pagination
|
|
288
|
-
const onPaginationClick = async (page: number) => {
|
|
289
|
-
await getXxxs({
|
|
290
|
-
...searchQuery.value,
|
|
291
|
-
skip: (page - 1) * (searchQuery.value?.take ?? 20),
|
|
292
|
-
sort: sortExpression.value,
|
|
293
|
-
});
|
|
307
|
+
await load();
|
|
294
308
|
};
|
|
295
309
|
|
|
296
|
-
//
|
|
310
|
+
// 15. Row click → open details blade
|
|
297
311
|
const onItemClick = (event: { data: { id?: string } }) => {
|
|
298
312
|
openBlade({
|
|
299
313
|
name: "XxxDetails",
|
|
@@ -310,14 +324,14 @@ const onItemClick = (event: { data: { id?: string } }) => {
|
|
|
310
324
|
});
|
|
311
325
|
};
|
|
312
326
|
|
|
313
|
-
//
|
|
327
|
+
// 16. Add new item — open details blade with no param (creates new)
|
|
314
328
|
function onAddItem() {
|
|
315
329
|
openBlade({
|
|
316
330
|
name: "XxxDetails",
|
|
317
331
|
});
|
|
318
332
|
}
|
|
319
333
|
|
|
320
|
-
//
|
|
334
|
+
// 17. Expose reload to child blades so details can trigger list refresh after save/delete
|
|
321
335
|
exposeToChildren({ reload });
|
|
322
336
|
</script>
|
|
323
337
|
```
|
|
@@ -334,6 +348,7 @@ exposeToChildren({ reload });
|
|
|
334
348
|
|
|
335
349
|
```ts
|
|
336
350
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
351
|
+
stateKey: "xxx_list", // optional: persists sort to blade URL
|
|
337
352
|
initialField: "createdDate",
|
|
338
353
|
initialDirection: "DESC",
|
|
339
354
|
});
|
|
@@ -342,16 +357,45 @@ const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
|
342
357
|
- `sortField` — reactive ref, two-way bound to `VcDataTable` via `v-model:sort-field`
|
|
343
358
|
- `sortOrder` — reactive ref (`"ASC"` | `"DESC"`), two-way bound via `v-model:sort-order`
|
|
344
359
|
- `sortExpression` — computed string `"field:DIR"` (e.g., `"createdDate:DESC"`), passed to the API
|
|
360
|
+
- `stateKey` — when provided, the composable reads the initial sort from the URL and writes back on change
|
|
345
361
|
|
|
346
|
-
###
|
|
362
|
+
### `useTableSearch` — search with optional URL persistence
|
|
347
363
|
|
|
348
364
|
```ts
|
|
349
|
-
|
|
350
|
-
|
|
365
|
+
const { searchValue } = useTableSearch({ stateKey: "xxx_list" });
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- Returns `searchValue` ref, two-way bound via `v-model:search-value` on `VcDataTable`.
|
|
369
|
+
- With `stateKey`, restores and persists the keyword to the blade URL.
|
|
370
|
+
- Without `stateKey`, behaves like a plain `ref("")`.
|
|
371
|
+
|
|
372
|
+
### `useDataTablePagination` — pagination with optional URL persistence
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const pagination = useDataTablePagination({
|
|
376
|
+
stateKey: "xxx_list", // optional: persists current page to blade URL
|
|
377
|
+
pageSize: 20,
|
|
378
|
+
totalCount,
|
|
351
379
|
});
|
|
352
380
|
```
|
|
353
381
|
|
|
354
|
-
|
|
382
|
+
- Pass `pagination` directly as `:pagination="pagination"` and `@pagination-click="pagination.goToPage"`.
|
|
383
|
+
- Access `pagination.skip` for API calls, `pagination.setPage(1)` to reset on search change.
|
|
384
|
+
- `stateKey` persists the current page to the blade URL; omit to opt out.
|
|
385
|
+
- The table `state-key` prop is column-layout persistence (localStorage) only and is independent.
|
|
386
|
+
|
|
387
|
+
### Load + watcher pattern
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const load = async () => {
|
|
391
|
+
await getXxxs({ sort: sortExpression.value, keyword: searchValue.value || undefined, skip: pagination.skip, take: 20 });
|
|
392
|
+
};
|
|
393
|
+
watch(searchValue, () => pagination.setPage(1));
|
|
394
|
+
watch([sortExpression, searchValue, () => pagination.skip], debounce(load, 300));
|
|
395
|
+
onMounted(() => load());
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Watch `[sortExpression, searchValue, () => pagination.skip]` to reload whenever any query dimension changes. Reset to page 1 when search changes before the reload watcher fires.
|
|
355
399
|
|
|
356
400
|
### `exposeToChildren`
|
|
357
401
|
|