@vc-shell/create-vc-app 2.0.10-pr242.7d8e7c3 → 2.0.10-pr243.88977c0
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,42 +1,43 @@
|
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
@@ -6,29 +6,26 @@
|
|
|
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"
|
|
12
9
|
:items="data"
|
|
13
10
|
:total-count="totalCount"
|
|
14
|
-
:
|
|
15
|
-
:
|
|
11
|
+
:current-page="currentPage"
|
|
12
|
+
:search-value="searchQuery"
|
|
16
13
|
:state-key="'<%- ModuleNameScreamingSnake %>'"
|
|
17
|
-
@search="
|
|
18
|
-
@
|
|
19
|
-
@pagination-click="
|
|
14
|
+
@search:change="(val: string) => { searchQuery = val; getItems(); }"
|
|
15
|
+
@item-click="openDetails"
|
|
16
|
+
@pagination-click="(page: number) => { currentPage = page; getItems(); }"
|
|
20
17
|
>
|
|
21
18
|
<!-- Add your columns here -->
|
|
22
|
-
<VcColumn id="name" :
|
|
23
|
-
<VcColumn id="createdDate" :
|
|
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 />
|
|
24
21
|
</VcDataTable>
|
|
25
22
|
</VcBlade>
|
|
26
23
|
</template>
|
|
27
24
|
|
|
28
25
|
<script setup lang="ts">
|
|
29
|
-
import { useBlade,
|
|
26
|
+
import { useBlade, type IBladeToolbar } from "@vc-shell/framework";
|
|
30
27
|
import { VcBlade, VcDataTable, VcColumn } from "@vc-shell/framework/ui";
|
|
31
|
-
import {
|
|
28
|
+
import { ref, onMounted } from "vue";
|
|
32
29
|
import use<%- ModuleNamePascalCase %>List from "../composables/useList";
|
|
33
30
|
import { useI18n } from "vue-i18n";
|
|
34
31
|
|
|
@@ -46,75 +43,21 @@ defineBlade({
|
|
|
46
43
|
const { t } = useI18n({ useScope: "global" });
|
|
47
44
|
const { openBlade, exposeToChildren } = useBlade();
|
|
48
45
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
});
|
|
46
|
+
const {
|
|
47
|
+
data,
|
|
48
|
+
loading,
|
|
49
|
+
totalCount,
|
|
50
|
+
currentPage,
|
|
51
|
+
searchQuery,
|
|
52
|
+
getItems,
|
|
53
|
+
} = use<%- ModuleNamePascalCase %>List();
|
|
111
54
|
|
|
112
55
|
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
113
56
|
{
|
|
114
57
|
id: "refresh",
|
|
115
58
|
title: t("<%- ModuleNameScreamingSnake %>.PAGES.LIST.TOOLBAR.REFRESH"),
|
|
116
59
|
icon: "lucide-refresh-cw",
|
|
117
|
-
clickHandler: () =>
|
|
60
|
+
clickHandler: () => getItems(),
|
|
118
61
|
},
|
|
119
62
|
{
|
|
120
63
|
id: "add",
|
|
@@ -129,14 +72,15 @@ function openDetails(item?: { id?: string }) {
|
|
|
129
72
|
name: "<%- ModuleNamePascalCase %>Details",
|
|
130
73
|
param: item?.id,
|
|
131
74
|
async onClose() {
|
|
132
|
-
await
|
|
75
|
+
await getItems();
|
|
133
76
|
},
|
|
134
77
|
});
|
|
135
78
|
}
|
|
136
79
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
80
|
+
onMounted(async () => {
|
|
81
|
+
await getItems();
|
|
82
|
+
})
|
|
83
|
+
|
|
140
84
|
|
|
141
|
-
exposeToChildren({ reload });
|
|
85
|
+
exposeToChildren({ reload: getItems });
|
|
142
86
|
</script>
|
|
@@ -7,8 +7,6 @@
|
|
|
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"
|
|
12
10
|
v-model:active-item-id="selectedItemId"
|
|
13
11
|
v-model:selection="selectedItems"
|
|
14
12
|
:loading="loading"
|
|
@@ -75,8 +73,8 @@
|
|
|
75
73
|
</template>
|
|
76
74
|
|
|
77
75
|
<script lang="ts" setup>
|
|
78
|
-
import { computed, ref, watch } from "vue";
|
|
79
|
-
import { IBladeToolbar, useBlade, usePopup,
|
|
76
|
+
import { computed, ref, onMounted, watch } from "vue";
|
|
77
|
+
import { IBladeToolbar, useBlade, usePopup, useTableSort, useFunctions } from "@vc-shell/framework";
|
|
80
78
|
import type { TableAction } from "@vc-shell/framework";
|
|
81
79
|
import { VcColumn, VcDataTable, VcBlade } from "@vc-shell/framework/ui";
|
|
82
80
|
import { useI18n } from "vue-i18n";
|
|
@@ -97,44 +95,24 @@ defineBlade({
|
|
|
97
95
|
const { t } = useI18n({ useScope: "global" });
|
|
98
96
|
const { param, openBlade, exposeToChildren } = useBlade();
|
|
99
97
|
const { showConfirmation } = usePopup();
|
|
98
|
+
const { debounce } = useFunctions();
|
|
100
99
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
104
|
-
initialField: "createdDate",
|
|
100
|
+
const { sortExpression } = useTableSort({
|
|
101
|
+
initialProperty: "createdDate",
|
|
105
102
|
initialDirection: "DESC",
|
|
106
103
|
});
|
|
107
104
|
|
|
108
|
-
const { getItems, removeItems, data, loading, totalCount, pages } = useList({
|
|
105
|
+
const { getItems, removeItems, data, loading, totalCount, pages, currentPage, searchQuery } = useList({
|
|
109
106
|
sort: sortExpression.value,
|
|
110
|
-
pageSize:
|
|
107
|
+
pageSize: 20,
|
|
111
108
|
});
|
|
112
109
|
|
|
113
|
-
const searchValue = ref
|
|
114
|
-
const appliedKeyword = ref<string>(); // applied filter — drives the loader
|
|
115
|
-
const currentPage = ref(1);
|
|
110
|
+
const searchValue = ref();
|
|
116
111
|
const selectedItemId = ref<string>();
|
|
117
112
|
const selectedItems = ref<MockedItem[]>([]);
|
|
118
113
|
|
|
119
114
|
const selectedIds = computed(() => selectedItems.value.map((item) => item.id).filter(Boolean) as string[]);
|
|
120
115
|
|
|
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
|
-
|
|
138
116
|
watch(
|
|
139
117
|
param,
|
|
140
118
|
(newVal) => {
|
|
@@ -143,30 +121,34 @@ watch(
|
|
|
143
121
|
{ immediate: true },
|
|
144
122
|
);
|
|
145
123
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
watch(
|
|
150
|
-
() => ({
|
|
124
|
+
onMounted(async () => {
|
|
125
|
+
await getItems({
|
|
126
|
+
...searchQuery.value,
|
|
151
127
|
sort: sortExpression.value,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}),
|
|
155
|
-
(query) => getItems(query),
|
|
156
|
-
{ immediate: true },
|
|
157
|
-
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
158
130
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
};
|
|
131
|
+
watch(sortExpression, async (value) => {
|
|
132
|
+
await getItems({
|
|
133
|
+
...searchQuery.value,
|
|
134
|
+
sort: value,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
165
137
|
|
|
166
|
-
const
|
|
138
|
+
const onSearchList = debounce(async (keyword: string) => {
|
|
139
|
+
searchValue.value = keyword;
|
|
140
|
+
await getItems({
|
|
141
|
+
...searchQuery.value,
|
|
142
|
+
keyword,
|
|
143
|
+
});
|
|
144
|
+
}, 1000);
|
|
145
|
+
|
|
146
|
+
const clearSearch = async () => {
|
|
167
147
|
searchValue.value = "";
|
|
168
|
-
|
|
169
|
-
|
|
148
|
+
await getItems({
|
|
149
|
+
...searchQuery.value,
|
|
150
|
+
keyword: "",
|
|
151
|
+
});
|
|
170
152
|
};
|
|
171
153
|
|
|
172
154
|
const addItem = () => {
|
|
@@ -175,8 +157,11 @@ const addItem = () => {
|
|
|
175
157
|
});
|
|
176
158
|
};
|
|
177
159
|
|
|
178
|
-
const onPaginationClick = (page: number) => {
|
|
179
|
-
|
|
160
|
+
const onPaginationClick = async (page: number) => {
|
|
161
|
+
await getItems({
|
|
162
|
+
...searchQuery.value,
|
|
163
|
+
skip: (page - 1) * (searchQuery.value.take ?? 20),
|
|
164
|
+
});
|
|
180
165
|
};
|
|
181
166
|
|
|
182
167
|
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
@@ -203,12 +188,10 @@ const title = computed(() => t("SAMPLE_APP.PAGES.LIST.TITLE"));
|
|
|
203
188
|
|
|
204
189
|
const reload = async () => {
|
|
205
190
|
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.
|
|
208
191
|
await getItems({
|
|
192
|
+
...searchQuery.value,
|
|
193
|
+
skip: (currentPage.value - 1) * (searchQuery.value.take ?? 10),
|
|
209
194
|
sort: sortExpression.value,
|
|
210
|
-
keyword: appliedKeyword.value || undefined,
|
|
211
|
-
skip: (currentPage.value - 1) * PAGE_SIZE,
|
|
212
195
|
});
|
|
213
196
|
};
|
|
214
197
|
|
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-pr243.88977c0",
|
|
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-pr243.88977c0",
|
|
20
20
|
"copyfiles": "^2.4.1",
|
|
21
21
|
"cross-env": "^7.0.3",
|
|
22
22
|
"shx": "^0.3.4",
|