From bfb451ef7174067ae167b5a3d6517f74c4627366 Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 18 Jun 2026 16:19:40 +0300 Subject: [PATCH] feat(vc-data-table): single-load restore for list views Restoring sort/search/page from the URL used to trigger several loads at once, so the data could disagree with the indicators after a reload. Move restore from push to pull: - useTableQueryPersistence no longer emits restore events on init; it keeps the URL write-back and only seeds the table's search box for display. - Add useTableQueryState(stateKey).read() so a page seeds its own sort/search/page refs in setup and loads once. - Add useDataTablePagination.setPage(n): set the page without firing onPageChange. Update the create-vc-app list templates and the table docs to the single-loader pattern. Unit tests cover read(), setPage, and the removed restore push. --- .../module/composables/useList.ts.ejs | 85 ++++++++--------- .../src/templates/module/pages/list.vue.ejs | 84 +++++++++++----- .../templates/sample-module/pages/list.vue | 80 +++++++--------- framework/core/blade-navigation/index.ts | 5 + .../table-query-state/index.ts | 1 + .../useTableQueryState.docs.md | 95 +++++++++++++++++++ .../useTableQueryState.test.ts | 37 ++++++++ .../table-query-state/useTableQueryState.ts | 28 ++++++ .../organisms/vc-data-table/VcDataTable.vue | 1 - .../useTableQueryPersistence.test.ts | 30 +++--- .../composables/useTableQueryPersistence.ts | 41 +++----- .../vc-data-table/vc-data-table.docs.md | 46 ++++++++- .../useDataTablePagination.docs.md | 67 ++++++++----- .../useDataTablePagination.test.ts | 18 ++++ .../ui/composables/useDataTablePagination.ts | 11 ++- .../ui/composables/useDataTableSort.docs.md | 24 +++-- 16 files changed, 462 insertions(+), 191 deletions(-) create mode 100644 framework/core/blade-navigation/table-query-state/useTableQueryState.docs.md create mode 100644 framework/core/blade-navigation/table-query-state/useTableQueryState.test.ts create mode 100644 framework/core/blade-navigation/table-query-state/useTableQueryState.ts diff --git a/cli/create-vc-app/src/templates/module/composables/useList.ts.ejs b/cli/create-vc-app/src/templates/module/composables/useList.ts.ejs index 6997695db..4bd296abf 100644 --- a/cli/create-vc-app/src/templates/module/composables/useList.ts.ejs +++ b/cli/create-vc-app/src/templates/module/composables/useList.ts.ejs @@ -1,43 +1,42 @@ -import { ref, type Ref } from "vue"; -import { useAsync, useLoading } from "@vc-shell/framework"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function use<%- ModuleNamePascalCase %>List() { - const data: Ref[]> = ref([]); - const totalCount = ref(0); - const currentPage = ref(1); - const searchQuery = ref(""); - - const { loading: itemsLoading, action: fetchItems } = useAsync(async () => { - // TODO: Replace with real API call - // const result = await apiClient.search({ - // keyword: searchQuery.value, - // skip: (currentPage.value - 1) * 20, - // take: 20, - // }); - // data.value = result.results ?? []; - // totalCount.value = result.totalCount ?? 0; - }); - - const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => { - // TODO: Replace with real API call - // await Promise.all((ids ?? []).map((id) => apiClient.delete(id))); - // await fetchItems(); - }); - - const loading = useLoading(itemsLoading, deleteLoading); - - async function getItems() { - await fetchItems(); - } - - return { - data, - loading, - totalCount, - currentPage, - searchQuery, - getItems, - removeItems, - }; -} +import { ref, type Ref } from "vue"; +import { useAsync, useLoading } from "@vc-shell/framework"; + +export interface <%- ModuleNamePascalCase %>ListQuery { + keyword?: string; + sort?: string; + skip?: number; + take?: number; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default function use<%- ModuleNamePascalCase %>List() { + const data: Ref[]> = ref([]); + const totalCount = ref(0); + + const { loading: itemsLoading, action: getItems } = useAsync<<%- ModuleNamePascalCase %>ListQuery>(async (query) => { + // TODO: Replace with real API call + // const result = await apiClient.search({ + // keyword: query?.keyword, + // sort: query?.sort, + // skip: query?.skip ?? 0, + // take: query?.take ?? 20, + // }); + // data.value = result.results ?? []; + // totalCount.value = result.totalCount ?? 0; + }); + + const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => { + // TODO: Replace with real API call + // await Promise.all((ids ?? []).map((id) => apiClient.delete(id))); + }); + + const loading = useLoading(itemsLoading, deleteLoading); + + return { + data, + loading, + totalCount, + getItems, + removeItems, + }; +} diff --git a/cli/create-vc-app/src/templates/module/pages/list.vue.ejs b/cli/create-vc-app/src/templates/module/pages/list.vue.ejs index 882d2737a..23fedd93a 100644 --- a/cli/create-vc-app/src/templates/module/pages/list.vue.ejs +++ b/cli/create-vc-app/src/templates/module/pages/list.vue.ejs @@ -6,26 +6,28 @@ width="50%" > - - + + diff --git a/cli/create-vc-app/src/templates/sample-module/pages/list.vue b/cli/create-vc-app/src/templates/sample-module/pages/list.vue index a00f878ca..8516fb2a0 100644 --- a/cli/create-vc-app/src/templates/sample-module/pages/list.vue +++ b/cli/create-vc-app/src/templates/sample-module/pages/list.vue @@ -7,6 +7,8 @@ @@ -73,8 +74,8 @@