@vc-shell/vc-app-skill 2.0.10 → 2.0.11-pr247.8c6c87f
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/core/composables/useAppInsights/useAppInsights.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +34 -9
- package/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md +1 -1
- package/runtime/knowledge/docs/core/notifications/notifications.docs.md +1 -1
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +67 -10
- package/runtime/knowledge/docs/ui/composables/useDataTablePagination.docs.md +44 -23
- package/runtime/knowledge/docs/ui/composables/useDataTableSort.docs.md +16 -8
- package/runtime/knowledge/migration-prompts/datatable-migration.md +27 -18
- package/runtime/knowledge/patterns/list-blade-pattern.md +85 -41
- package/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md +0 -34
- package/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md +0 -184
- package/runtime/knowledge/docs/core/notifications/composables/useBroadcastFilter.docs.md +0 -117
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationContext.docs.md +0 -150
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md +0 -113
- package/runtime/knowledge/docs/modules/assets/assets-details.docs.md +0 -123
- package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/dashboard-widget-skeleton.docs.md +0 -33
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.0.11-pr247.8c6c87f",
|
|
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.0.11
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit f2225cc27 on 2026-06-22T15:40:33.524Z
|
|
@@ -115,6 +115,6 @@ This results in page names like `[Operations Console] Dashboard` instead of just
|
|
|
115
115
|
## Related
|
|
116
116
|
|
|
117
117
|
- `vue3-application-insights` -- underlying plugin that initializes the Application Insights SDK
|
|
118
|
-
-
|
|
118
|
+
- `useUserManagement` -- provides current user context for telemetry enrichment
|
|
119
119
|
- [useErrorHandler](../useErrorHandler/useErrorHandler.docs.md) -- automatically tracks exceptions to Application Insights
|
|
120
120
|
- [useWebVitals](../useWebVitals/useWebVitals.docs.md) -- Core Web Vitals can be piped to Application Insights via a custom callback
|
|
@@ -103,15 +103,16 @@ Without the generic, `options.value` defaults to `Record<string, unknown> | unde
|
|
|
103
103
|
|
|
104
104
|
These are reactive `ComputedRef` values that reflect the current blade's state. Accessing them outside blade context throws a runtime error.
|
|
105
105
|
|
|
106
|
-
| Property
|
|
107
|
-
|
|
|
108
|
-
| `id`
|
|
109
|
-
| `param`
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
106
|
+
| Property | Type | Description |
|
|
107
|
+
| ------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `id` | `ComputedRef<string>` | The current blade's unique ID within the blade stack |
|
|
109
|
+
| `param` | `ComputedRef<string \| undefined>` | Route parameter passed when the blade was opened |
|
|
110
|
+
| `activeChildParam` | `ComputedRef<string \| undefined>` | The `param` of the direct child blade opened from this blade (the entity currently shown in the details pane), or `undefined`. Reactive and reload-safe. Bind it to a list table's `:active-item-id` to highlight the open entity's row — it stays correct across reloads, unlike a local ref. |
|
|
111
|
+
| `options` | `ComputedRef<TOptions \| undefined>` | Additional options object passed to the blade. Type is `Record<string, unknown>` by default; provide a generic to get a typed ref: `useBlade<MyOptions>()` |
|
|
112
|
+
| `query` | `ComputedRef<Record<string, string> \| undefined>` | URL query parameters scoped to this blade |
|
|
113
|
+
| `closable` | `ComputedRef<boolean>` | `true` when this blade has a parent (i.e., can be closed) |
|
|
114
|
+
| `expanded` | `ComputedRef<boolean>` | `true` when this blade is the active (rightmost) blade |
|
|
115
|
+
| `name` | `ComputedRef<string>` | The blade's registered component name |
|
|
115
116
|
|
|
116
117
|
#### Navigation Methods (work everywhere)
|
|
117
118
|
|
|
@@ -387,6 +388,30 @@ onDeactivated(() => {
|
|
|
387
388
|
});
|
|
388
389
|
```
|
|
389
390
|
|
|
391
|
+
### Highlight the row of the open child blade
|
|
392
|
+
|
|
393
|
+
Bind `activeChildParam` to the table's `active-item-id` so the row of the currently open details blade is highlighted — including after a page reload:
|
|
394
|
+
|
|
395
|
+
```vue
|
|
396
|
+
<script setup lang="ts">
|
|
397
|
+
const { openBlade, activeChildParam } = useBlade();
|
|
398
|
+
|
|
399
|
+
function onRowClick(e: { data: { id: string } }) {
|
|
400
|
+
openBlade({ name: "Details", param: e.data.id });
|
|
401
|
+
}
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<template>
|
|
405
|
+
<VcDataTable
|
|
406
|
+
:items="items"
|
|
407
|
+
:active-item-id="activeChildParam"
|
|
408
|
+
@row-click="onRowClick"
|
|
409
|
+
>
|
|
410
|
+
<!-- columns -->
|
|
411
|
+
</VcDataTable>
|
|
412
|
+
</template>
|
|
413
|
+
```
|
|
414
|
+
|
|
390
415
|
### Error Management
|
|
391
416
|
|
|
392
417
|
Display or clear error banners on the blade:
|
|
@@ -200,7 +200,7 @@ const { isMobile } = useResponsive();
|
|
|
200
200
|
|
|
201
201
|
## Migration
|
|
202
202
|
|
|
203
|
-
See [migration guide #36](
|
|
203
|
+
See [migration guide #36](https://github.com/VirtoCommerce/vc-shell/blob/main/migration/36-use-responsive.md) for automated codemod and manual migration instructions.
|
|
204
204
|
|
|
205
205
|
## Related
|
|
206
206
|
|
|
@@ -167,5 +167,5 @@ trackRequest("my-request-2");
|
|
|
167
167
|
## Related
|
|
168
168
|
|
|
169
169
|
- [`useConnectionStatus`](../useConnectionStatus/useConnectionStatus.docs.md) — offline detection (binary online/offline)
|
|
170
|
-
- [`registerInterceptors`](
|
|
170
|
+
- [`registerInterceptors`](https://github.com/VirtoCommerce/vc-shell/blob/main/framework/core/interceptors/index.ts) — the fetch wrapper that calls `trackRequest`/`untrackRequest`
|
|
171
171
|
- `notification` from `@shared/components/notifications` — the notification system used to display the warning
|
|
@@ -173,6 +173,6 @@ async function fetchCustomEndpoint(url: string) {
|
|
|
173
173
|
|
|
174
174
|
## Related
|
|
175
175
|
|
|
176
|
-
-
|
|
176
|
+
- `useUserManagement` -- extended API with sign-in, password reset, token validation (for auth pages)
|
|
177
177
|
- [usePermissions](../usePermissions/) -- permission checks based on user roles
|
|
178
178
|
- `UserDetail` from `@core/api/platform` -- the user detail type returned by the API
|
|
@@ -207,7 +207,7 @@ store.registerType("CatalogExportCompleted", {
|
|
|
207
207
|
});
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
Custom templates access the notification via `useNotificationContext()` composable — no props needed. See
|
|
210
|
+
Custom templates access the notification via `useNotificationContext()` composable — no props needed. See the `NotificationTemplate` component for template examples.
|
|
211
211
|
|
|
212
212
|
### Subscribing in a blade
|
|
213
213
|
|
|
@@ -35,7 +35,7 @@ The blade reads its configuration from `options` via `useBlade<AssetsManagerOpti
|
|
|
35
35
|
| `disabled` | `boolean?` | When true, hides upload/delete actions and disables reordering |
|
|
36
36
|
| `hiddenFields` | `string[]?` | Fields to hide in the detail view |
|
|
37
37
|
|
|
38
|
-
> **Breaking change:** The old options (`assets`, `loading`, `assetsUploadHandler`, `assetsEditHandler`, `assetsRemoveHandler`) have been removed. Pass a single `manager` instance instead. See [migration guide #32](
|
|
38
|
+
> **Breaking change:** The old options (`assets`, `loading`, `assetsUploadHandler`, `assetsEditHandler`, `assetsRemoveHandler`) have been removed. Pass a single `manager` instance instead. See [migration guide #32](https://github.com/VirtoCommerce/vc-shell/blob/main/migration/32-use-assets-manager.md).
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
@@ -376,6 +376,8 @@ For server-side "select all" that includes items not currently visible:
|
|
|
376
376
|
</VcDataTable>
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
+
> **Opt-in.** The "Select all N items" banner is shown only when you bind `selectAllActive` (via `v-model:selectAllActive` or a one-way `:selectAllActive`). Its default is `undefined`, so a table that passes `totalCount` only for pagination never surfaces the banner.
|
|
380
|
+
|
|
379
381
|
```ts
|
|
380
382
|
const allSelected = ref(false);
|
|
381
383
|
|
|
@@ -1030,7 +1032,7 @@ async function loadNextPage() {
|
|
|
1030
1032
|
!!! tip "Use unique state keys"
|
|
1031
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.
|
|
1032
1034
|
|
|
1033
|
-
Persist column widths, column order, and column visibility across page reloads.
|
|
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 separately — to the **URL query** — automatically when the table is rendered inside a URL-addressable blade (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:
|
|
1034
1036
|
|
|
1035
1037
|
```vue
|
|
1036
1038
|
<VcDataTable :items="products" state-key="product-list">
|
|
@@ -1063,6 +1065,61 @@ Listen to state events:
|
|
|
1063
1065
|
|
|
1064
1066
|
> **Tip:** Each table in your application should have a unique `state-key`. If two tables share the same key, they will overwrite each other's persisted state.
|
|
1065
1067
|
|
|
1068
|
+
### URL query persistence
|
|
1069
|
+
|
|
1070
|
+
When a table is rendered inside a URL-addressable blade (a workspace or routable blade whose address appears in the address bar), its sort, search, and current page are persisted to the URL query string. Reloading the page or sharing the link restores that view.
|
|
1071
|
+
|
|
1072
|
+
Writing the view state to the URL is automatic — you do not wire it up. Tables outside a blade (or in a blade with no URL) do not persist (no-op). Reading it back on reload is done by the page (see below).
|
|
1073
|
+
|
|
1074
|
+
- **Namespacing:** query keys are prefixed with the table's `state-key` if set, otherwise with the blade's URL segment — e.g. `?offers_sort=name:DESC&offers_search=foo&offers_page=2`. Give each table a unique `state-key` when several tables can be visible at once (the same rule as column-layout persistence).
|
|
1075
|
+
- **Keys:** `<ns>_sort` (`field:ASC` / `field:DESC`), `<ns>_search`, `<ns>_page` (1-based).
|
|
1076
|
+
- **History:** changes use `router.replace`, so they do not add browser-history entries.
|
|
1077
|
+
- **Scope:** only sort, search, and page. Column layout still uses `localStorage`/`sessionStorage` (above); column filters and selection are not persisted.
|
|
1078
|
+
|
|
1079
|
+
The page reads the persisted values with `useTableQueryState(stateKey)`, seeds its sort/search/page refs in `setup`, and loads once. The table does not push the restored values back into the page, so a reload runs a single request rather than one per restored field.
|
|
1080
|
+
|
|
1081
|
+
```ts
|
|
1082
|
+
import { ref, watch } from "vue";
|
|
1083
|
+
import { useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
|
|
1084
|
+
|
|
1085
|
+
const PAGE_SIZE = 20;
|
|
1086
|
+
const { debounce } = useFunctions();
|
|
1087
|
+
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
1088
|
+
initialField: "createdDate",
|
|
1089
|
+
initialDirection: "DESC",
|
|
1090
|
+
});
|
|
1091
|
+
const searchValue = ref<string>();
|
|
1092
|
+
const currentPage = ref(1);
|
|
1093
|
+
|
|
1094
|
+
// Restore from the URL, then load once.
|
|
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;
|
|
1103
|
+
|
|
1104
|
+
function load() {
|
|
1105
|
+
return loadItems({
|
|
1106
|
+
sort: sortExpression.value,
|
|
1107
|
+
keyword: searchValue.value || undefined,
|
|
1108
|
+
skip: (currentPage.value - 1) * PAGE_SIZE,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
load();
|
|
1113
|
+
watch(searchValue, () => (currentPage.value = 1));
|
|
1114
|
+
watch([sortExpression, searchValue, currentPage], debounce(load, 300));
|
|
1115
|
+
|
|
1116
|
+
function onPaginationClick(page: number) {
|
|
1117
|
+
currentPage.value = page;
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
If a blade cannot be reopened on reload (e.g. a non-routable blade, or an intermediate blade that is not on the restored URL path), its table query params have no owner — they are automatically dropped from the URL once the restored blades have mounted, so the address bar never accumulates stale query keys.
|
|
1122
|
+
|
|
1066
1123
|
---
|
|
1067
1124
|
|
|
1068
1125
|
## Row Actions
|
|
@@ -1345,15 +1402,15 @@ function onRowRemove(event: { data: Product; index: number; cancel: () => void }
|
|
|
1345
1402
|
|
|
1346
1403
|
### Selection
|
|
1347
1404
|
|
|
1348
|
-
| Prop | Type | Default
|
|
1349
|
-
| -------------------- | ------------------------ |
|
|
1350
|
-
| `selection` | `T \| T[]` | --
|
|
1351
|
-
| `selectionMode` | `"single" \| "multiple"` | --
|
|
1352
|
-
| `isRowSelectable` | `(data: T) => boolean` | --
|
|
1353
|
-
| `compareSelectionBy` | `"equals" \| "field"` | --
|
|
1354
|
-
| `selectAll` | `boolean` | `false`
|
|
1355
|
-
| `selectAllActive` | `boolean` | `
|
|
1356
|
-
| `activeItemId` | `string` | --
|
|
1405
|
+
| Prop | Type | Default | Description |
|
|
1406
|
+
| -------------------- | ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1407
|
+
| `selection` | `T \| T[]` | -- | Selected item(s). Use with `v-model:selection`. |
|
|
1408
|
+
| `selectionMode` | `"single" \| "multiple"` | -- | Row selection mode. |
|
|
1409
|
+
| `isRowSelectable` | `(data: T) => boolean` | -- | Per-row function to disable selection. |
|
|
1410
|
+
| `compareSelectionBy` | `"equals" \| "field"` | -- | Compare items by deep equality or by `dataKey` field. |
|
|
1411
|
+
| `selectAll` | `boolean` | `false` | Enable "select all" header checkbox. |
|
|
1412
|
+
| `selectAllActive` | `boolean` | `undefined` | "Select all" (including non-visible items) active state. Binding it (e.g. `v-model:selectAllActive`) opts the table into the cross-page "Select all N items" banner; left unbound, the banner never appears. |
|
|
1413
|
+
| `activeItemId` | `string` | -- | ID of the highlighted row. Use with `v-model:activeItemId`. |
|
|
1357
1414
|
|
|
1358
1415
|
### Sorting
|
|
1359
1416
|
|
|
@@ -29,7 +29,6 @@ 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 }),
|
|
33
32
|
});
|
|
34
33
|
```
|
|
35
34
|
|
|
@@ -37,6 +36,8 @@ const pagination = useDataTablePagination({
|
|
|
37
36
|
<VcDataTable :items="items" :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
38
37
|
```
|
|
39
38
|
|
|
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
|
+
|
|
40
41
|
## API
|
|
41
42
|
|
|
42
43
|
### Parameters (Options)
|
|
@@ -49,15 +50,16 @@ const pagination = useDataTablePagination({
|
|
|
49
50
|
|
|
50
51
|
### Returns (`reactive()` object)
|
|
51
52
|
|
|
52
|
-
| Property | Type | Description
|
|
53
|
-
| ------------- | ------------------------ |
|
|
54
|
-
| `currentPage` | `number` | Current 1-based page number (writable)
|
|
55
|
-
| `pages` | `number` (readonly) | Total number of pages
|
|
56
|
-
| `skip` | `number` (readonly) | Current skip offset for API calls
|
|
57
|
-
| `pageSize` | `number` (readonly) | Resolved page size
|
|
58
|
-
| `totalCount` | `number` (readonly) | Resolved total item count
|
|
59
|
-
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange`
|
|
60
|
-
| `
|
|
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` | Seed the page WITHOUT firing `onPageChange` — use for pull-restore seed |
|
|
62
|
+
| `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
|
|
61
63
|
|
|
62
64
|
All properties are auto-unwrapped by `reactive()` — no `.value` access needed in script or template.
|
|
63
65
|
|
|
@@ -65,30 +67,43 @@ All properties are auto-unwrapped by `reactive()` — no `.value` access needed
|
|
|
65
67
|
|
|
66
68
|
```vue
|
|
67
69
|
<script setup lang="ts">
|
|
68
|
-
import { ref, computed } from "vue";
|
|
69
|
-
import { useDataTablePagination, useDataTableSort } from "@vc-shell/framework";
|
|
70
|
+
import { ref, computed, watch } from "vue";
|
|
71
|
+
import { useDataTablePagination, useDataTableSort, useTableQueryState } from "@vc-shell/framework";
|
|
70
72
|
|
|
71
73
|
// Data layer
|
|
72
74
|
const searchResult = ref<{ results: Item[]; totalCount: number }>();
|
|
73
|
-
const searchQuery = ref({ take: 20, skip: 0 });
|
|
74
75
|
|
|
75
|
-
async function loadItems(query?:
|
|
76
|
-
|
|
77
|
-
searchResult.value = await api.search(searchQuery.value);
|
|
76
|
+
async function loadItems(query: { sort?: string; keyword?: string; skip?: number }) {
|
|
77
|
+
searchResult.value = await api.search({ take: 20, ...query });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// State only — no self-loading callback.
|
|
81
81
|
const pagination = useDataTablePagination({
|
|
82
82
|
pageSize: 20,
|
|
83
83
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
84
|
-
onPageChange: ({ skip }) => loadItems({ skip }),
|
|
85
84
|
});
|
|
86
|
-
|
|
87
|
-
// Sort state
|
|
88
85
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
89
86
|
initialField: "createdDate",
|
|
90
87
|
initialDirection: "DESC",
|
|
91
88
|
});
|
|
89
|
+
const searchValue = ref<string>();
|
|
90
|
+
|
|
91
|
+
// Restore from the URL, then load once. setPage seeds the page without loading.
|
|
92
|
+
const restored = useTableQueryState("product-list").read();
|
|
93
|
+
if (restored.sort) {
|
|
94
|
+
const [field, dir] = restored.sort.split(":");
|
|
95
|
+
sortField.value = field;
|
|
96
|
+
sortOrder.value = dir === "DESC" ? -1 : 1;
|
|
97
|
+
}
|
|
98
|
+
if (restored.search) searchValue.value = restored.search;
|
|
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
107
|
</script>
|
|
93
108
|
|
|
94
109
|
<template>
|
|
@@ -97,6 +112,12 @@ const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
|
97
112
|
:total-count="pagination.totalCount"
|
|
98
113
|
:pagination="pagination"
|
|
99
114
|
@pagination-click="pagination.goToPage"
|
|
115
|
+
@search="
|
|
116
|
+
(kw) => {
|
|
117
|
+
searchValue = kw;
|
|
118
|
+
pagination.setPage(1);
|
|
119
|
+
}
|
|
120
|
+
"
|
|
100
121
|
v-model:sort-field="sortField"
|
|
101
122
|
v-model:sort-order="sortOrder"
|
|
102
123
|
>
|
|
@@ -130,7 +151,6 @@ export function useOffers() {
|
|
|
130
151
|
const pagination = useDataTablePagination({
|
|
131
152
|
pageSize: 20,
|
|
132
153
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
133
|
-
onPageChange: ({ skip }) => loadOffers({ ...searchQuery.value, skip }),
|
|
134
154
|
});
|
|
135
155
|
|
|
136
156
|
return {
|
|
@@ -141,7 +161,7 @@ export function useOffers() {
|
|
|
141
161
|
}
|
|
142
162
|
```
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
The blade owns the single loader watch (reading `pagination.skip` with sort/search) and binds:
|
|
145
165
|
|
|
146
166
|
```vue
|
|
147
167
|
<VcDataTable :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
@@ -150,7 +170,8 @@ Blade then simply binds:
|
|
|
150
170
|
## Details
|
|
151
171
|
|
|
152
172
|
- **`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.
|
|
153
|
-
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not
|
|
173
|
+
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not fetch data. With a single loader watcher you usually omit it and read `pagination.skip` from the watcher instead.
|
|
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.
|
|
154
175
|
- **No auto-clamp**: When `totalCount` decreases (e.g. after deletion), `currentPage` is NOT automatically clamped. Call `reset()` or `goToPage(1)` explicitly after mutations.
|
|
155
176
|
- **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.
|
|
156
177
|
- **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.
|
|
@@ -84,14 +84,16 @@ const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
|
|
|
84
84
|
|
|
85
85
|
const items = ref([]);
|
|
86
86
|
const loading = ref(false);
|
|
87
|
+
const currentPage = ref(1);
|
|
88
|
+
const PAGE_SIZE = 20;
|
|
87
89
|
|
|
88
|
-
async function loadItems() {
|
|
90
|
+
async function loadItems(query: { sort?: string; skip?: number }) {
|
|
89
91
|
loading.value = true;
|
|
90
92
|
try {
|
|
91
93
|
const response = await api.searchProducts({
|
|
92
|
-
sort:
|
|
93
|
-
skip: 0,
|
|
94
|
-
take:
|
|
94
|
+
sort: query.sort, // e.g. "createdDate:DESC" or undefined
|
|
95
|
+
skip: query.skip ?? 0,
|
|
96
|
+
take: PAGE_SIZE,
|
|
95
97
|
});
|
|
96
98
|
items.value = response.results;
|
|
97
99
|
} finally {
|
|
@@ -99,8 +101,13 @@ async function loadItems() {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
+
// Load from one watcher that reads sort together with search and page, so changing
|
|
105
|
+
// several at once is a single request. { immediate: true } also does the first load.
|
|
106
|
+
watch(
|
|
107
|
+
() => ({ sort: sortExpression.value, skip: (currentPage.value - 1) * PAGE_SIZE }),
|
|
108
|
+
(query) => loadItems(query),
|
|
109
|
+
{ immediate: true },
|
|
110
|
+
);
|
|
104
111
|
</script>
|
|
105
112
|
|
|
106
113
|
<template>
|
|
@@ -134,12 +141,13 @@ watch(sortExpression, () => loadItems(), { immediate: true });
|
|
|
134
141
|
- **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.
|
|
135
142
|
- **Reset behavior**: `resetSort()` restores `sortField` and `sortOrder` to the values passed at construction time. If no initial options were provided, both are cleared.
|
|
136
143
|
- **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.
|
|
137
|
-
- **Stateless regarding data**: The composable only manages the sort field and direction.
|
|
144
|
+
- **Stateless regarding data**: The composable only manages the sort field and direction. Read `sortExpression` from the same loader watcher that reads search and page, instead of giving sort its own `watch(sortExpression, load)`. See VcDataTable → URL query persistence.
|
|
138
145
|
|
|
139
146
|
## Tips
|
|
140
147
|
|
|
141
148
|
- Always mark columns as `sortable` in your `<VcColumn>` definitions for the sort indicator icon to appear.
|
|
142
|
-
- Use `{ immediate: true }` on the
|
|
149
|
+
- Use `{ immediate: true }` on the combined loader watcher to trigger the initial (or URL-restored) data load with the correct sort on mount.
|
|
150
|
+
- On reload, seed `sortField`/`sortOrder` from `useTableQueryState(stateKey).read().sort` (`"field:DIR"`) before the loader watcher, so restore costs a single request.
|
|
143
151
|
- Call `resetSort()` alongside any "clear all filters" action to keep sort state consistent with the rest of the filter UI.
|
|
144
152
|
|
|
145
153
|
## Related
|
|
@@ -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
|
|