@vc-shell/create-vc-app 2.0.10-pr244.35451aa → 2.0.10-pr246.07f48ce

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,28 @@
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
+ @row-click="onRowClick"
18
+ @pagination-click="onPaginationClick"
17
19
  >
18
20
  <!-- 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 />
21
+ <VcColumn id="name" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.NAME')" sortable />
22
+ <VcColumn id="createdDate" :title="$t('<%- ModuleNameScreamingSnake %>.PAGES.LIST.COLUMNS.CREATED_DATE')" type="datetime" sortable />
21
23
  </VcDataTable>
22
24
  </VcBlade>
23
25
  </template>
24
26
 
25
27
  <script setup lang="ts">
26
- import { useBlade, type IBladeToolbar } from "@vc-shell/framework";
28
+ import { useBlade, useDataTableSort, useTableQueryState, useFunctions, type IBladeToolbar } from "@vc-shell/framework";
27
29
  import { VcBlade, VcDataTable, VcColumn } from "@vc-shell/framework/ui";
28
- import { ref, onMounted } from "vue";
30
+ import { computed, ref, watch } from "vue";
29
31
  import use<%- ModuleNamePascalCase %>List from "../composables/useList";
30
32
  import { useI18n } from "vue-i18n";
31
33
 
@@ -42,22 +44,57 @@ defineBlade({
42
44
 
43
45
  const { t } = useI18n({ useScope: "global" });
44
46
  const { openBlade, exposeToChildren } = useBlade();
47
+ const { debounce } = useFunctions();
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>();
59
+ const currentPage = ref(1);
60
+ const pages = computed(() => Math.ceil(totalCount.value / PAGE_SIZE) || 0);
61
+
62
+ // Restore sort/search/page from the URL, then load once below.
63
+ const restored = useTableQueryState("<%- ModuleNameScreamingSnake %>").read();
64
+ if (restored.sort) {
65
+ const [field, direction] = restored.sort.split(":");
66
+ sortField.value = field;
67
+ sortOrder.value = direction === "DESC" ? -1 : 1;
68
+ }
69
+ if (restored.search) searchValue.value = restored.search;
70
+ if (restored.page) currentPage.value = restored.page;
71
+
72
+ function load() {
73
+ return getItems({
74
+ sort: sortExpression.value,
75
+ keyword: searchValue.value || undefined,
76
+ skip: (currentPage.value - 1) * PAGE_SIZE,
77
+ take: PAGE_SIZE,
78
+ });
79
+ }
80
+
81
+ // One loader for sort/search/page. Debounced so typing doesn't fetch per keystroke.
82
+ load();
83
+ watch(searchValue, () => (currentPage.value = 1));
84
+ watch([sortExpression, searchValue, currentPage], debounce(load, 300));
85
+
86
+ const onPaginationClick = (page: number) => {
87
+ currentPage.value = page;
88
+ };
89
+
90
+ const reload = () => load();
54
91
 
55
92
  const bladeToolbar = ref<IBladeToolbar[]>([
56
93
  {
57
94
  id: "refresh",
58
95
  title: t("<%- ModuleNameScreamingSnake %>.PAGES.LIST.TOOLBAR.REFRESH"),
59
96
  icon: "lucide-refresh-cw",
60
- clickHandler: () => getItems(),
97
+ clickHandler: () => reload(),
61
98
  },
62
99
  {
63
100
  id: "add",
@@ -72,15 +109,14 @@ function openDetails(item?: { id?: string }) {
72
109
  name: "<%- ModuleNamePascalCase %>Details",
73
110
  param: item?.id,
74
111
  async onClose() {
75
- await getItems();
112
+ await reload();
76
113
  },
77
114
  });
78
115
  }
79
116
 
80
- onMounted(async () => {
81
- await getItems();
82
- })
83
-
117
+ function onRowClick(event: { data: { id?: string } }) {
118
+ openDetails(event.data);
119
+ }
84
120
 
85
- exposeToChildren({ reload: getItems });
121
+ exposeToChildren({ reload });
86
122
  </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"
@@ -32,7 +34,6 @@
32
34
  :total-label="$t('SAMPLE_APP.PAGES.LIST.TABLE.TOTALS')"
33
35
  :total-count="totalCount"
34
36
  state-key="SAMPLE_APP"
35
- @search="onSearchList"
36
37
  @row-click="onItemClick"
37
38
  @pagination-click="onPaginationClick"
38
39
  >
@@ -73,8 +74,8 @@
73
74
  </template>
74
75
 
75
76
  <script lang="ts" setup>
76
- import { computed, ref, onMounted, watch } from "vue";
77
- import { IBladeToolbar, useBlade, usePopup, useTableSort, useFunctions } from "@vc-shell/framework";
77
+ import { computed, ref, watch } from "vue";
78
+ import { IBladeToolbar, useBlade, usePopup, useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
78
79
  import type { TableAction } from "@vc-shell/framework";
79
80
  import { VcColumn, VcDataTable, VcBlade } from "@vc-shell/framework/ui";
80
81
  import { useI18n } from "vue-i18n";
@@ -97,22 +98,35 @@ const { param, openBlade, exposeToChildren } = useBlade();
97
98
  const { showConfirmation } = usePopup();
98
99
  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>();
114
+ const currentPage = ref(1);
111
115
  const selectedItemId = ref<string>();
112
116
  const selectedItems = ref<MockedItem[]>([]);
113
117
 
114
118
  const selectedIds = computed(() => selectedItems.value.map((item) => item.id).filter(Boolean) as string[]);
115
119
 
120
+ // Restore sort/search/page from the URL, then load once below.
121
+ const restored = useTableQueryState("SAMPLE_APP").read();
122
+ if (restored.sort) {
123
+ const [field, direction] = restored.sort.split(":");
124
+ sortField.value = field;
125
+ sortOrder.value = direction === "DESC" ? -1 : 1;
126
+ }
127
+ if (restored.search) searchValue.value = restored.search;
128
+ if (restored.page) currentPage.value = restored.page;
129
+
116
130
  watch(
117
131
  param,
118
132
  (newVal) => {
@@ -121,47 +135,29 @@ watch(
121
135
  { immediate: true },
122
136
  );
123
137
 
124
- onMounted(async () => {
125
- await getItems({
126
- ...searchQuery.value,
138
+ function load() {
139
+ return getItems({
127
140
  sort: sortExpression.value,
141
+ keyword: searchValue.value || undefined,
142
+ skip: (currentPage.value - 1) * PAGE_SIZE,
128
143
  });
129
- });
130
-
131
- watch(sortExpression, async (value) => {
132
- await getItems({
133
- ...searchQuery.value,
134
- sort: value,
135
- });
136
- });
144
+ }
137
145
 
138
- const onSearchList = debounce(async (keyword: string) => {
139
- searchValue.value = keyword;
140
- await getItems({
141
- ...searchQuery.value,
142
- keyword,
143
- });
144
- }, 1000);
146
+ // One loader for sort/search/page. Debounced so typing doesn't fetch per keystroke.
147
+ load();
148
+ watch(searchValue, () => (currentPage.value = 1));
149
+ watch([sortExpression, searchValue, currentPage], debounce(load, 300));
145
150
 
146
- const clearSearch = async () => {
151
+ const clearSearch = () => {
147
152
  searchValue.value = "";
148
- await getItems({
149
- ...searchQuery.value,
150
- keyword: "",
151
- });
152
153
  };
153
154
 
154
155
  const addItem = () => {
155
- openBlade({
156
- name: "SampleDetails",
157
- });
156
+ openBlade({ name: "SampleDetails" });
158
157
  };
159
158
 
160
- const onPaginationClick = async (page: number) => {
161
- await getItems({
162
- ...searchQuery.value,
163
- skip: (page - 1) * (searchQuery.value.take ?? 20),
164
- });
159
+ const onPaginationClick = (page: number) => {
160
+ currentPage.value = page;
165
161
  };
166
162
 
167
163
  const bladeToolbar = ref<IBladeToolbar[]>([
@@ -188,11 +184,7 @@ const title = computed(() => t("SAMPLE_APP.PAGES.LIST.TITLE"));
188
184
 
189
185
  const reload = async () => {
190
186
  selectedItems.value = [];
191
- await getItems({
192
- ...searchQuery.value,
193
- skip: (currentPage.value - 1) * (searchQuery.value.take ?? 10),
194
- sort: sortExpression.value,
195
- });
187
+ await load();
196
188
  };
197
189
 
198
190
  const onItemClick = (event: { data: MockedItem; index: number; originalEvent: Event }) => {
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-pr244.35451aa",
4
+ "version": "2.0.10-pr246.07f48ce",
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-pr244.35451aa",
19
+ "@vc-shell/ts-config": "2.0.10-pr246.07f48ce",
20
20
  "copyfiles": "^2.4.1",
21
21
  "cross-env": "^7.0.3",
22
22
  "shx": "^0.3.4",