@vc-shell/vc-app-skill 2.0.11-pr247.0f1e741 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.11-pr247.0f1e741",
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": [
@@ -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"` | `:searchable="true"` (internal state) |
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="{ currentPage, pages }"` |
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 — Manual Calculation → useDataTablePagination
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="{ currentPage, pages }"
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="onPaginationClick"
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, searchQuery, loading, xxxList, currentPage, pages, totalCount } = useXxxs();
219
+ const { getXxxs, loading, xxxList, totalCount } = useXxxs();
215
220
 
216
- // 3. Sort — provides sortField, sortOrder (for VcDataTable v-model), sortExpression (string for API)
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. UI state
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
- // 5. Toolbar
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
- // 6. Status variant mapping (if status column exists)
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
- // 7. Sort reactivityreload when sort changes
260
- watch(sortExpression, async (value) => {
261
- await getXxxs({ ...searchQuery.value, sort: value });
262
- });
277
+ // 9. Load functionexplicit; 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
- // 8. Track selected row when param changes (e.g., navigating directly to URL)
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
- // 9. Load data on mount
274
- onMounted(async () => {
275
- await reload();
276
- });
302
+ // 13. Load data on mount
303
+ onMounted(() => load());
277
304
 
278
- // 10. Reload — reloads current page with current sort
305
+ // 14. Reload helper used by toolbar and exposed to child blades
279
306
  const reload = async () => {
280
- await getXxxs({
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
- // 12. Row click → open details blade
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
- // 13. Add new item — open details blade with no param (creates new)
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
- // 14. Expose reload to child blades so details can trigger list refresh after save/delete
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
- ### Sort watcher pattern
362
+ ### `useTableSearch` search with optional URL persistence
347
363
 
348
364
  ```ts
349
- watch(sortExpression, async (value) => {
350
- await getXxxs({ ...searchQuery.value, sort: value });
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
- Watch `sortExpression` (not `sortField`/`sortOrder` separately) to trigger a reload whenever sort changes.
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