@vc-shell/create-vc-app 2.0.10-pr240.41fca76 → 2.0.10-pr242.2e7f6b3

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.
@@ -1,43 +1,42 @@
1
- import { ref, type Ref } from "vue";
2
- import { useAsync, useLoading } from "@vc-shell/framework";
3
-
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
- export default function use<%- ModuleNamePascalCase %>List() {
6
- const data: Ref<Record<string, any>[]> = ref([]);
7
- const totalCount = ref(0);
8
- const currentPage = ref(1);
9
- const searchQuery = ref("");
10
-
11
- const { loading: itemsLoading, action: fetchItems } = useAsync(async () => {
12
- // TODO: Replace with real API call
13
- // const result = await apiClient.search({
14
- // keyword: searchQuery.value,
15
- // skip: (currentPage.value - 1) * 20,
16
- // take: 20,
17
- // });
18
- // data.value = result.results ?? [];
19
- // totalCount.value = result.totalCount ?? 0;
20
- });
21
-
22
- const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => {
23
- // TODO: Replace with real API call
24
- // await Promise.all((ids ?? []).map((id) => apiClient.delete(id)));
25
- // await fetchItems();
26
- });
27
-
28
- const loading = useLoading(itemsLoading, deleteLoading);
29
-
30
- async function getItems() {
31
- await fetchItems();
32
- }
33
-
34
- return {
35
- data,
36
- loading,
37
- totalCount,
38
- currentPage,
39
- searchQuery,
40
- getItems,
41
- removeItems,
42
- };
43
- }
1
+ import { ref, type Ref } from "vue";
2
+ import { useAsync, useLoading } from "@vc-shell/framework";
3
+
4
+ export interface <%- ModuleNamePascalCase %>ListQuery {
5
+ keyword?: string;
6
+ sort?: string;
7
+ skip?: number;
8
+ take?: number;
9
+ }
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ export default function use<%- ModuleNamePascalCase %>List() {
13
+ const data: Ref<Record<string, any>[]> = ref([]);
14
+ const totalCount = ref(0);
15
+
16
+ const { loading: itemsLoading, action: getItems } = useAsync<<%- ModuleNamePascalCase %>ListQuery>(async (query) => {
17
+ // TODO: Replace with real API call
18
+ // const result = await apiClient.search({
19
+ // keyword: query?.keyword,
20
+ // sort: query?.sort,
21
+ // skip: query?.skip ?? 0,
22
+ // take: query?.take ?? 20,
23
+ // });
24
+ // data.value = result.results ?? [];
25
+ // totalCount.value = result.totalCount ?? 0;
26
+ });
27
+
28
+ const { loading: deleteLoading, action: removeItems } = useAsync(async (ids?: string[]) => {
29
+ // TODO: Replace with real API call
30
+ // await Promise.all((ids ?? []).map((id) => apiClient.delete(id)));
31
+ });
32
+
33
+ const loading = useLoading(itemsLoading, deleteLoading);
34
+
35
+ return {
36
+ data,
37
+ loading,
38
+ totalCount,
39
+ getItems,
40
+ removeItems,
41
+ };
42
+ }
@@ -6,26 +6,29 @@
6
6
  width="50%"
7
7
  >
8
8
  <VcDataTable
9
+ v-model:search-value="searchValue"
10
+ v-model:sort-field="sortField"
11
+ v-model:sort-order="sortOrder"
9
12
  :items="data"
10
13
  :total-count="totalCount"
11
- :current-page="currentPage"
12
- :search-value="searchQuery"
14
+ :pagination="{ currentPage, pages }"
15
+ :searchable="true"
13
16
  :state-key="'<%- ModuleNameScreamingSnake %>'"
14
- @search:change="(val: string) => { searchQuery = val; getItems(); }"
15
- @item-click="openDetails"
16
- @pagination-click="(page: number) => { currentPage = page; getItems(); }"
17
+ @search="onSearch"
18
+ @row-click="onRowClick"
19
+ @pagination-click="onPaginationClick"
17
20
  >
18
21
  <!-- Add your columns here -->
19
- <VcColumn id="name" :header="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.NAME')" sortable />
20
- <VcColumn id="createdDate" :header="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.CREATED_DATE')" type="datetime" sortable />
22
+ <VcColumn id="name" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.NAME')" sortable />
23
+ <VcColumn id="createdDate" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.CREATED_DATE')" type="datetime" sortable />
21
24
  </VcDataTable>
22
25
  </VcBlade>
23
26
  </template>
24
27
 
25
28
  <script setup lang="ts">
26
- import { useBlade, type IBladeToolbar } from "@vc-shell/framework";
29
+ import { useBlade, useDataTableSort, useTableQueryState, type IBladeToolbar } from "@vc-shell/framework";
27
30
  import { VcBlade, VcDataTable, VcColumn } from "@vc-shell/framework/ui";
28
- import { ref, onMounted } from "vue";
31
+ import { computed, ref, watch } from "vue";
29
32
  import use<%- ModuleNamePascalCase %>List from "../composables/useList";
30
33
  import { useI18n } from "vue-i18n";
31
34
 
@@ -43,21 +46,75 @@ defineBlade({
43
46
  const { t } = useI18n({ useScope: "global" });
44
47
  const { openBlade, exposeToChildren } = useBlade();
45
48
 
46
- const {
47
- data,
48
- loading,
49
- totalCount,
50
- currentPage,
51
- searchQuery,
52
- getItems,
53
- } = use<%- ModuleNamePascalCase %>List();
49
+ const PAGE_SIZE = 20;
50
+
51
+ const { sortField, sortOrder, sortExpression } = useDataTableSort({
52
+ initialField: "createdDate",
53
+ initialDirection: "DESC",
54
+ });
55
+
56
+ const { data, loading, totalCount, getItems } = use<%- ModuleNamePascalCase %>List();
57
+
58
+ const searchValue = ref<string>(); // live search box (v-model)
59
+ const appliedKeyword = ref<string>(); // applied filter — drives the loader
60
+ const currentPage = ref(1);
61
+
62
+ const pages = computed(() => Math.ceil(totalCount.value / PAGE_SIZE) || 0);
63
+
64
+ // Pull-restore: read the URL-persisted view state and seed our refs synchronously,
65
+ // BEFORE the loader watch below — so a reload costs exactly one request.
66
+ const restored = useTableQueryState("<%- ModuleNameScreamingSnake %>").read();
67
+ if (restored.sort) {
68
+ const [field, direction] = restored.sort.split(":");
69
+ sortField.value = field;
70
+ sortOrder.value = direction === "DESC" ? -1 : 1;
71
+ }
72
+ if (restored.search !== undefined) {
73
+ searchValue.value = restored.search;
74
+ appliedKeyword.value = restored.search;
75
+ }
76
+ if (restored.page) {
77
+ currentPage.value = restored.page;
78
+ }
79
+
80
+ // Single loader — the ONLY trigger that calls the API. Vue batches synchronous
81
+ // changes to several criteria into one flush → one request, even on restore.
82
+ watch(
83
+ () => ({
84
+ sort: sortExpression.value,
85
+ keyword: appliedKeyword.value || undefined,
86
+ skip: (currentPage.value - 1) * PAGE_SIZE,
87
+ take: PAGE_SIZE,
88
+ }),
89
+ (query) => getItems(query),
90
+ { immediate: true },
91
+ );
92
+
93
+ // Handlers only update state — they never load. VcDataTable debounces @search, so
94
+ // reacting to appliedKeyword coalesces fast typing into a single request.
95
+ const onSearch = (keyword: string) => {
96
+ appliedKeyword.value = keyword;
97
+ currentPage.value = 1;
98
+ };
99
+
100
+ const onPaginationClick = (page: number) => {
101
+ currentPage.value = page;
102
+ };
103
+
104
+ const reload = () =>
105
+ getItems({
106
+ sort: sortExpression.value,
107
+ keyword: appliedKeyword.value || undefined,
108
+ skip: (currentPage.value - 1) * PAGE_SIZE,
109
+ take: PAGE_SIZE,
110
+ });
54
111
 
55
112
  const bladeToolbar = ref<IBladeToolbar[]>([
56
113
  {
57
114
  id: "refresh",
58
115
  title: t("<%- ModuleNameScreamingSnake %>.PAGES.LIST.TOOLBAR.REFRESH"),
59
116
  icon: "lucide-refresh-cw",
60
- clickHandler: () => getItems(),
117
+ clickHandler: () => reload(),
61
118
  },
62
119
  {
63
120
  id: "add",
@@ -72,15 +129,14 @@ function openDetails(item?: { id?: string }) {
72
129
  name: "<%- ModuleNamePascalCase %>Details",
73
130
  param: item?.id,
74
131
  async onClose() {
75
- await getItems();
132
+ await reload();
76
133
  },
77
134
  });
78
135
  }
79
136
 
80
- onMounted(async () => {
81
- await getItems();
82
- })
83
-
137
+ function onRowClick(event: { data: { id?: string } }) {
138
+ openDetails(event.data);
139
+ }
84
140
 
85
- exposeToChildren({ reload: getItems });
141
+ exposeToChildren({ reload });
86
142
  </script>
@@ -7,6 +7,8 @@
7
7
  <!-- Blade contents -->
8
8
  <VcDataTable
9
9
  v-model:search-value="searchValue"
10
+ v-model:sort-field="sortField"
11
+ v-model:sort-order="sortOrder"
10
12
  v-model:active-item-id="selectedItemId"
11
13
  v-model:selection="selectedItems"
12
14
  :loading="loading"
@@ -73,8 +75,8 @@
73
75
  </template>
74
76
 
75
77
  <script lang="ts" setup>
76
- import { computed, ref, onMounted, watch } from "vue";
77
- import { IBladeToolbar, useBlade, usePopup, useTableSort, useFunctions } from "@vc-shell/framework";
78
+ import { computed, ref, watch } from "vue";
79
+ import { IBladeToolbar, useBlade, usePopup, useDataTableSort, useTableQueryState } from "@vc-shell/framework";
78
80
  import type { TableAction } from "@vc-shell/framework";
79
81
  import { VcColumn, VcDataTable, VcBlade } from "@vc-shell/framework/ui";
80
82
  import { useI18n } from "vue-i18n";
@@ -95,24 +97,44 @@ defineBlade({
95
97
  const { t } = useI18n({ useScope: "global" });
96
98
  const { param, openBlade, exposeToChildren } = useBlade();
97
99
  const { showConfirmation } = usePopup();
98
- const { debounce } = useFunctions();
99
100
 
100
- const { sortExpression } = useTableSort({
101
- initialProperty: "createdDate",
101
+ const PAGE_SIZE = 20;
102
+
103
+ const { sortField, sortOrder, sortExpression } = useDataTableSort({
104
+ initialField: "createdDate",
102
105
  initialDirection: "DESC",
103
106
  });
104
107
 
105
- const { getItems, removeItems, data, loading, totalCount, pages, currentPage, searchQuery } = useList({
108
+ const { getItems, removeItems, data, loading, totalCount, pages } = useList({
106
109
  sort: sortExpression.value,
107
- pageSize: 20,
110
+ pageSize: PAGE_SIZE,
108
111
  });
109
112
 
110
- const searchValue = ref();
113
+ const searchValue = ref<string>(); // live search box (v-model)
114
+ const appliedKeyword = ref<string>(); // applied filter — drives the loader
115
+ const currentPage = ref(1);
111
116
  const selectedItemId = ref<string>();
112
117
  const selectedItems = ref<MockedItem[]>([]);
113
118
 
114
119
  const selectedIds = computed(() => selectedItems.value.map((item) => item.id).filter(Boolean) as string[]);
115
120
 
121
+ // Pull-restore: read the URL-persisted view state and seed our refs synchronously,
122
+ // BEFORE the loader watch below — so a page reload costs exactly one request with
123
+ // the full criteria, instead of several uncoordinated loads.
124
+ const restored = useTableQueryState("SAMPLE_APP").read();
125
+ if (restored.sort) {
126
+ const [field, direction] = restored.sort.split(":");
127
+ sortField.value = field;
128
+ sortOrder.value = direction === "DESC" ? -1 : 1;
129
+ }
130
+ if (restored.search !== undefined) {
131
+ searchValue.value = restored.search;
132
+ appliedKeyword.value = restored.search;
133
+ }
134
+ if (restored.page) {
135
+ currentPage.value = restored.page;
136
+ }
137
+
116
138
  watch(
117
139
  param,
118
140
  (newVal) => {
@@ -121,34 +143,30 @@ watch(
121
143
  { immediate: true },
122
144
  );
123
145
 
124
- onMounted(async () => {
125
- await getItems({
126
- ...searchQuery.value,
146
+ // Single loader — the ONLY place that calls the API. Vue batches synchronous changes
147
+ // to several criteria into one flush, so sort + search + page changing together still
148
+ // yields one request.
149
+ watch(
150
+ () => ({
127
151
  sort: sortExpression.value,
128
- });
129
- });
130
-
131
- watch(sortExpression, async (value) => {
132
- await getItems({
133
- ...searchQuery.value,
134
- sort: value,
135
- });
136
- });
152
+ keyword: appliedKeyword.value || undefined,
153
+ skip: (currentPage.value - 1) * PAGE_SIZE,
154
+ }),
155
+ (query) => getItems(query),
156
+ { immediate: true },
157
+ );
137
158
 
138
- const onSearchList = debounce(async (keyword: string) => {
139
- searchValue.value = keyword;
140
- await getItems({
141
- ...searchQuery.value,
142
- keyword,
143
- });
144
- }, 1000);
159
+ // Handlers only update state they never load. VcDataTable debounces @search, so
160
+ // reacting to appliedKeyword coalesces fast typing into a single request.
161
+ const onSearchList = (keyword: string) => {
162
+ appliedKeyword.value = keyword;
163
+ currentPage.value = 1;
164
+ };
145
165
 
146
- const clearSearch = async () => {
166
+ const clearSearch = () => {
147
167
  searchValue.value = "";
148
- await getItems({
149
- ...searchQuery.value,
150
- keyword: "",
151
- });
168
+ appliedKeyword.value = "";
169
+ currentPage.value = 1;
152
170
  };
153
171
 
154
172
  const addItem = () => {
@@ -157,11 +175,8 @@ const addItem = () => {
157
175
  });
158
176
  };
159
177
 
160
- const onPaginationClick = async (page: number) => {
161
- await getItems({
162
- ...searchQuery.value,
163
- skip: (page - 1) * (searchQuery.value.take ?? 20),
164
- });
178
+ const onPaginationClick = (page: number) => {
179
+ currentPage.value = page;
165
180
  };
166
181
 
167
182
  const bladeToolbar = ref<IBladeToolbar[]>([
@@ -188,10 +203,12 @@ const title = computed(() => t("SAMPLE_APP.PAGES.LIST.TITLE"));
188
203
 
189
204
  const reload = async () => {
190
205
  selectedItems.value = [];
206
+ // Explicit user refresh — the criteria are unchanged, so the loader watch would
207
+ // not fire; reload calls the API directly with the current combined criteria.
191
208
  await getItems({
192
- ...searchQuery.value,
193
- skip: (currentPage.value - 1) * (searchQuery.value.take ?? 10),
194
209
  sort: sortExpression.value,
210
+ keyword: appliedKeyword.value || undefined,
211
+ skip: (currentPage.value - 1) * PAGE_SIZE,
195
212
  });
196
213
  };
197
214
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vc-shell/create-vc-app",
3
3
  "description": "Application scaffolding",
4
- "version": "2.0.10-pr240.41fca76",
4
+ "version": "2.0.10-pr242.2e7f6b3",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",
7
7
  "files": [
@@ -16,7 +16,7 @@
16
16
  "devDependencies": {
17
17
  "@types/ejs": "^3.1.5",
18
18
  "@types/prompts": "^2.4.4",
19
- "@vc-shell/ts-config": "2.0.10-pr240.41fca76",
19
+ "@vc-shell/ts-config": "2.0.10-pr242.2e7f6b3",
20
20
  "copyfiles": "^2.4.1",
21
21
  "cross-env": "^7.0.3",
22
22
  "shx": "^0.3.4",