@vc-shell/create-vc-app 2.0.10-pr241.e655447 → 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const loading =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
:
|
|
12
|
-
:
|
|
14
|
+
:pagination="{ currentPage, pages }"
|
|
15
|
+
:searchable="true"
|
|
13
16
|
:state-key="'<%- ModuleNameScreamingSnake %>'"
|
|
14
|
-
@search
|
|
15
|
-
@
|
|
16
|
-
@pagination-click="
|
|
17
|
+
@search="onSearch"
|
|
18
|
+
@row-click="onRowClick"
|
|
19
|
+
@pagination-click="onPaginationClick"
|
|
17
20
|
>
|
|
18
21
|
<!-- Add your columns here -->
|
|
19
|
-
<VcColumn id="name" :
|
|
20
|
-
<VcColumn id="createdDate" :
|
|
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,
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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: () =>
|
|
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
|
|
132
|
+
await reload();
|
|
76
133
|
},
|
|
77
134
|
});
|
|
78
135
|
}
|
|
79
136
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
137
|
+
function onRowClick(event: { data: { id?: string } }) {
|
|
138
|
+
openDetails(event.data);
|
|
139
|
+
}
|
|
84
140
|
|
|
85
|
-
exposeToChildren({ reload
|
|
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,
|
|
77
|
-
import { IBladeToolbar, useBlade, usePopup,
|
|
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
|
|
101
|
-
|
|
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
|
|
108
|
+
const { getItems, removeItems, data, loading, totalCount, pages } = useList({
|
|
106
109
|
sort: sortExpression.value,
|
|
107
|
-
pageSize:
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 =
|
|
166
|
+
const clearSearch = () => {
|
|
147
167
|
searchValue.value = "";
|
|
148
|
-
|
|
149
|
-
|
|
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 =
|
|
161
|
-
|
|
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-
|
|
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-
|
|
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",
|