@vc-shell/create-vc-app 1.1.88 → 1.1.90

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [1.1.90](https://github.com/VirtoCommerce/vc-shell/compare/v1.1.89...v1.1.90) (2025-10-08)
2
+
3
+
4
+
5
+ ## [1.1.89](https://github.com/VirtoCommerce/vc-shell/compare/v1.1.88...v1.1.89) (2025-10-08)
6
+
7
+
8
+
1
9
  ## [1.1.88](https://github.com/VirtoCommerce/vc-shell/compare/v1.1.87...v1.1.88) (2025-10-07)
2
10
 
3
11
 
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import c from "node:path";
6
6
  import s from "node:fs";
7
7
  import { fileURLToPath as Oe } from "node:url";
8
8
  import { cwd as Me, exit as Ee, argv as Te } from "node:process";
9
- const Ue = "1.1.88", I = {
9
+ const Ue = "1.1.90", I = {
10
10
  version: Ue
11
11
  };
12
12
  var Pe = typeof global == "object" && global && global.Object === Object && global, Le = typeof self == "object" && self && self.Object === Object && self, _e = Pe || Le || Function("return this")(), b = _e.Symbol, K = Object.prototype, ze = K.hasOwnProperty, De = K.toString, $ = b ? b.toStringTag : void 0;
@@ -23,9 +23,9 @@
23
23
  "@types/node": "^20.10.5",
24
24
  "@typescript-eslint/eslint-plugin": "^7.4.0",
25
25
  "@typescript-eslint/parser": "^7.4.0",
26
- "@vc-shell/api-client-generator": "^1.1.88",
27
- "@vc-shell/release-config": "^1.1.87",
28
- "@vc-shell/ts-config": "^1.1.87",
26
+ "@vc-shell/api-client-generator": "^1.1.90",
27
+ "@vc-shell/release-config": "^1.1.89",
28
+ "@vc-shell/ts-config": "^1.1.89",
29
29
  "@vitejs/plugin-vue": "^5.2.3",
30
30
  "@vue/eslint-config-prettier": "^9.0.0",
31
31
  "@vue/eslint-config-typescript": "^13.0.0",
@@ -53,8 +53,8 @@
53
53
  "vue-tsc": "^2.2.10"
54
54
  },
55
55
  "dependencies": {
56
- "@vc-shell/config-generator": "^1.1.87",
57
- "@vc-shell/framework": "^1.1.88",
56
+ "@vc-shell/config-generator": "^1.1.89",
57
+ "@vc-shell/framework": "^1.1.90",
58
58
  "@vueuse/core": "^10.7.1",
59
59
  "@vueuse/integrations": "^10.7.1",
60
60
  "cross-spawn": "^7.0.3",
@@ -1,19 +1,24 @@
1
- import { computed, ref } from "vue";
2
- import { useAsync, useLoading } from "@vc-shell/framework";
3
-
4
- export default () => {
5
- const item = ref({});
6
-
7
- // Implement your own load function
8
- const { loading: itemLoading, action: getItem } = useAsync<{ id: string }>(async (payload) => {
9
- item.value = {};
10
- });
11
-
12
- const loading = useLoading(itemLoading);
13
-
14
- return {
15
- item: computed(() => item.value),
16
- loading: computed(() => loading.value),
17
- getItem,
18
- };
19
- };
1
+ import { computed, ref } from "vue";
2
+ import { useAsync, useLoading, useModificationTracker } from "@vc-shell/framework";
3
+
4
+ export default () => {
5
+ const item = ref({});
6
+
7
+ const { isModified, currentValue, resetModificationState } = useModificationTracker(item);
8
+
9
+ // Implement your own load function
10
+ const { loading: itemLoading, action: getItem } = useAsync<{ id: string }>(async (payload) => {
11
+ item.value = {};
12
+
13
+ resetModificationState();
14
+ });
15
+
16
+ const loading = useLoading(itemLoading);
17
+
18
+ return {
19
+ item: currentValue,
20
+ loading: computed(() => loading.value),
21
+ getItem,
22
+ isModified,
23
+ };
24
+ };
@@ -6,6 +6,7 @@ interface SearchQuery {
6
6
  take?: number;
7
7
  skip?: number;
8
8
  sort?: string;
9
+ keyword?: string;
9
10
  }
10
11
 
11
12
  export default (options?: { pageSize?: number, sort?: string }) => {
@@ -26,7 +27,12 @@ export default (options?: { pageSize?: number, sort?: string }) => {
26
27
  };
27
28
  });
28
29
 
29
- const loading = useLoading(itemLoading);
30
+ // Implement your own remove function
31
+ const { loading: removeLoading, action: removeItems } = useAsync<{ ids: string[] }>(async (payload) => {
32
+ return;
33
+ });
34
+
35
+ const loading = useLoading(itemLoading, removeLoading);
30
36
 
31
37
  return {
32
38
  data: computed(() => searchResult.value?.items),
@@ -35,6 +41,7 @@ export default (options?: { pageSize?: number, sort?: string }) => {
35
41
  pages: computed(() => Math.ceil((searchResult.value?.totalCount || 1) / pageSize)),
36
42
  currentPage: computed(() => Math.ceil((searchQuery.value?.skip || 0) / Math.max(1, pageSize) + 1)),
37
43
  getItems,
44
+ removeItems,
38
45
  searchQuery,
39
46
  };
40
47
  };
@@ -1,28 +1,37 @@
1
- {
2
- "{{ModuleNameUppercaseSnakeCase}}": {
3
- "MENU": {
4
- "TITLE": "{{ModuleNameSentenceCase}}"
5
- },
6
- "PAGES": {
7
- "LIST": {
8
- "TITLE": "{{ModuleNameSentenceCase}} list",
9
- "TOOLBAR": {
10
- "REFRESH": "Refresh"
11
- },
12
- "SEARCH": {
13
- "PLACEHOLDER": "Search keywords"
14
- },
15
- "TABLE": {
16
- "TOTALS": "Count:",
17
- "HEADER": {}
18
- }
19
- },
20
- "DETAILS": {
21
- "TITLE": "{{ModuleNameSentenceCase}} details"
22
- },
23
- "ALERTS": {
24
- "CLOSE_CONFIRMATION": "You have unsaved changes. Close anyway?"
25
- }
26
- }
27
- }
28
- }
1
+ {
2
+ "{{ModuleNameUppercaseSnakeCase}}": {
3
+ "MENU": {
4
+ "TITLE": "{{ModuleNameSentenceCase}}"
5
+ },
6
+ "PAGES": {
7
+ "LIST": {
8
+ "TITLE": "{{ModuleNameSentenceCase}} list",
9
+ "TOOLBAR": {
10
+ "REFRESH": "Refresh",
11
+ "ADD": "Add"
12
+ },
13
+ "SEARCH": {
14
+ "PLACEHOLDER": "Search keywords"
15
+ },
16
+ "TABLE": {
17
+ "TOTALS": "Count:",
18
+ "HEADER": {}
19
+ },
20
+ "EMPTY": {
21
+ "NO_ITEMS": "No items found",
22
+ "ADD": "Add items"
23
+ },
24
+ "NOT_FOUND": {
25
+ "EMPTY": "No items found",
26
+ "RESET": "Reset"
27
+ }
28
+ },
29
+ "DETAILS": {
30
+ "TITLE": "{{ModuleNameSentenceCase}} details"
31
+ },
32
+ "ALERTS": {
33
+ "CLOSE_CONFIRMATION": "You have unsaved changes. Close anyway?"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -19,10 +19,11 @@
19
19
  </template>
20
20
 
21
21
  <script lang="ts" setup>
22
- import { IBladeToolbar, IParentCallArgs } from "@vc-shell/framework";
22
+ import { IBladeToolbar, IParentCallArgs, useBladeNavigation, usePopup, useBeforeUnload } from "@vc-shell/framework";
23
23
  import { use{{ModuleNamePascalCase}}Details } from "./../composables";
24
24
  import { computed, onMounted, ref } from "vue";
25
25
  import { useI18n } from "vue-i18n";
26
+ import { useForm } from "vee-validate";
26
27
 
27
28
  export interface Props {
28
29
  expanded?: boolean;
@@ -50,9 +51,19 @@ const props = withDefaults(defineProps<Props>(), {
50
51
 
51
52
  defineEmits<Emits>();
52
53
 
53
- const { loading, getItem } = use{{ModuleNamePascalCase}}Details();
54
+ const { loading, getItem, isModified } = use{{ModuleNamePascalCase}}Details();
55
+ const { onBeforeClose } = useBladeNavigation();
56
+ const { showConfirmation } = usePopup();
54
57
  const { t } = useI18n({ useScope: "global" });
55
58
 
59
+ const { meta } = useForm({
60
+ validateOnMount: false,
61
+ });
62
+
63
+ const isDisabled = computed(() => {
64
+ return !meta.value.dirty || !meta.value.valid;
65
+ });
66
+
56
67
  const bladeToolbar = ref<IBladeToolbar[]>([]);
57
68
  const title = computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.DETAILS.TITLE"));
58
69
 
@@ -62,6 +73,14 @@ onMounted(async () => {
62
73
  }
63
74
  });
64
75
 
76
+ onBeforeClose(async () => {
77
+ if (!isDisabled.value && isModified.value) {
78
+ return await showConfirmation(t("SAMPLE_APP.PAGES.ALERTS.CLOSE_CONFIRMATION"));
79
+ }
80
+ });
81
+
82
+ useBeforeUnload(computed(() => !isDisabled.value && isModified.value));
83
+
65
84
  defineExpose({
66
85
  title,
67
86
  });
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <VcBlade
3
- :title="$t('{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.TITLE')"
3
+ :title="title"
4
4
  width="50%"
5
5
  :expanded="expanded"
6
6
  :closable="closable"
@@ -18,17 +18,24 @@
18
18
  :loading="loading"
19
19
  :columns="columns"
20
20
  :sort="sortExpression"
21
+ :current-page="currentPage"
22
+ :search-value="searchValue"
23
+ enable-item-actions
24
+ :item-action-builder="actionBuilder"
21
25
  :pages="pages"
26
+ :empty="empty"
27
+ :notfound="notfound"
22
28
  :total-count="totalCount"
23
- :search-value="searchValue"
24
- :current-page="currentPage"
29
+ :selected-item-id="selectedItemId"
25
30
  :search-placeholder="$t('{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.SEARCH.PLACEHOLDER')"
26
31
  :total-label="$t('{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.TABLE.TOTALS')"
27
- :selected-item-id="selectedItemId"
28
32
  state-key="{{ModuleNameUppercaseSnakeCase}}"
29
33
  :items="data"
30
34
  @item-click="onItemClick"
31
35
  @header-click="onHeaderClick"
36
+ @search:change="onSearchList"
37
+ @pagination-click="onPaginationClick"
38
+ @selection-changed="onSelectionChanged"
32
39
  >
33
40
  </VcTable>
34
41
  </VcBlade>
@@ -36,7 +43,7 @@
36
43
 
37
44
  <script lang="ts" setup>
38
45
  import { computed, ref, markRaw, onMounted, watch } from "vue";
39
- import { IBladeToolbar, IParentCallArgs, ITableColumns, useBladeNavigation, useTableSort } from "@vc-shell/framework";
46
+ import { IBladeToolbar, IParentCallArgs, ITableColumns, useBladeNavigation, useTableSort, IActionBuilderResult, useFunctions, usePopup } from "@vc-shell/framework";
40
47
  import { useI18n } from "vue-i18n";
41
48
  import { use{{ModuleNamePascalCase}}List } from "./../composables";
42
49
  import Details from "./details.vue";
@@ -75,19 +82,43 @@ defineEmits<Emits>();
75
82
 
76
83
  const { t } = useI18n({ useScope: "global" });
77
84
  const { openBlade } = useBladeNavigation();
78
-
85
+ const { debounce } = useFunctions();
86
+ const { showConfirmation } = usePopup();
79
87
  const { sortExpression, handleSortChange: tableSortHandler } = useTableSort({
80
88
  initialDirection: "DESC",
81
89
  initialProperty: "createdDate",
82
90
  });
83
91
 
84
- const { getItems, data, loading, totalCount, pages, currentPage, searchQuery } = use{{ModuleNamePascalCase}}List({
92
+ const { getItems, data, loading, totalCount, pages, currentPage, searchQuery, removeItems } = use{{ModuleNamePascalCase}}List({
85
93
  sort: sortExpression.value,
86
94
  pageSize: 20,
87
95
  });
88
96
 
89
97
  const searchValue = ref();
90
98
  const selectedItemId = ref<string>();
99
+ const selectedItemsIds = ref<string[]>([]);
100
+
101
+ const empty = {
102
+ icon: "lucide-file",
103
+ text: computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.EMPTY.NO_ITEMS")),
104
+ action: computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.EMPTY.ADD")),
105
+ clickHandler: () => {
106
+ addItem();
107
+ },
108
+ };
109
+
110
+ const notfound = {
111
+ icon: "lucide-file",
112
+ text: computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.NOT_FOUND.EMPTY")),
113
+ action: computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.NOT_FOUND.RESET")),
114
+ clickHandler: async () => {
115
+ searchValue.value = "";
116
+ await getItems({
117
+ ...searchQuery.value,
118
+ keyword: "",
119
+ });
120
+ },
121
+ };
91
122
 
92
123
  watch(
93
124
  () => props.param,
@@ -113,6 +144,14 @@ const bladeToolbar = ref<IBladeToolbar[]>([
113
144
  await reload();
114
145
  },
115
146
  },
147
+ {
148
+ id: "add",
149
+ icon: "material-add",
150
+ title: computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.TOOLBAR.ADD")),
151
+ clickHandler: () => {
152
+ addItem();
153
+ },
154
+ },
116
155
  ]);
117
156
 
118
157
  const columns = ref<ITableColumns[]>([
@@ -121,6 +160,14 @@ const columns = ref<ITableColumns[]>([
121
160
 
122
161
  const title = computed(() => t("{{ModuleNameUppercaseSnakeCase}}.PAGES.LIST.TITLE"));
123
162
 
163
+ const onSearchList = debounce(async (keyword: string) => {
164
+ searchValue.value = keyword;
165
+ await getItems({
166
+ ...searchQuery.value,
167
+ keyword,
168
+ });
169
+ }, 1000);
170
+
124
171
  const reload = async () => {
125
172
  await getItems({
126
173
  ...searchQuery.value,
@@ -129,6 +176,12 @@ const reload = async () => {
129
176
  });
130
177
  };
131
178
 
179
+ const addItem = () => {
180
+ openBlade({
181
+ blade: markRaw(Details),
182
+ });
183
+ };
184
+
132
185
  const onItemClick = (item: { id: string }) => {
133
186
  openBlade({
134
187
  blade: markRaw(Details),
@@ -146,6 +199,48 @@ const onHeaderClick = (item: ITableColumns) => {
146
199
  tableSortHandler(item.id);
147
200
  };
148
201
 
202
+ const actionBuilder = (): IActionBuilderResult[] => {
203
+ const result: IActionBuilderResult[] = [];
204
+ result.push({
205
+ icon: "material-delete",
206
+ title: "Delete",
207
+ type: "danger",
208
+ async clickHandler(_item: { id: string }) {
209
+ if (_item.id && !selectedItemsIds.value.includes(_item.id)) {
210
+ selectedItemsIds.value.push(_item.id);
211
+ }
212
+ await remove(selectedItemsIds.value);
213
+ selectedItemsIds.value = [];
214
+ },
215
+ });
216
+
217
+ return result;
218
+ };
219
+
220
+ const onPaginationClick = async (page: number) => {
221
+ await getItems({
222
+ ...searchQuery.value,
223
+ skip: (page - 1) * (searchQuery.value.take ?? 20),
224
+ });
225
+ };
226
+
227
+ const onSelectionChanged = (items: Record<string, any>[]) => {
228
+ selectedItemsIds.value = items.flatMap((item) => (item.id ? [item.id] : []));
229
+ };
230
+
231
+ async function remove(ids: string[]) {
232
+ if (
233
+ await showConfirmation(
234
+ t(`{{ModuleNameUppercaseSnakeCase}}.PAGES.ALERTS.DELETE_SELECTED_CONFIRMATION.MESSAGE`, {
235
+ count: selectedItemsIds.value.length,
236
+ }),
237
+ )
238
+ ) {
239
+ await removeItems({ ids });
240
+ await reload();
241
+ }
242
+ }
243
+
149
244
  watch(
150
245
  () => sortExpression.value,
151
246
  async (newVal) => {
@@ -1,59 +1,67 @@
1
- {
2
- "SAMPLE_APP": {
3
- "MENU": {
4
- "TITLE": "Sample"
5
- },
6
- "PAGES": {
7
- "LIST": {
8
- "TITLE": "Sample list",
9
- "TOOLBAR": {
10
- "REFRESH": "Refresh",
11
- "REMOVE": "Remove"
12
- },
13
- "SEARCH": {
14
- "PLACEHOLDER": "Search keywords"
15
- },
16
- "TABLE": {
17
- "TOTALS": "Count:",
18
- "HEADER": {
19
- "IMAGE": "Img",
20
- "PRODUCT_NAME": "Product name",
21
- "DESCRIPTION": "Description",
22
- "PRICE": "Price",
23
- "SALE_PRICE": "Sale price",
24
- "CURRENCY": "Currency"
25
- },
26
- "ACTIONS": {
27
- "DELETE": "Delete"
28
- }
29
- }
30
- },
31
- "DETAILS": {
32
- "TITLE": {
33
- "DETAILS": " details",
34
- "LOADING": "Loading..."
35
- },
36
- "TOOLBAR": {
37
- "SAVE": "Save",
38
- "DELETE": "Delete"
39
- },
40
- "FIELDS": {
41
- "NAME": "Name",
42
- "CONTENT": "Content",
43
- "GUID": "GUID",
44
- "DESCRIPTION": "Description",
45
- "PRICE": "Price",
46
- "SALE_PRICE": "Sale price"
47
- }
48
- },
49
- "ALERTS": {
50
- "CLOSE_CONFIRMATION": "You have unsaved changes. Close anyway?",
51
- "DELETE": "Are you sure you want to delete this item?",
52
- "DELETE_SELECTED_CONFIRMATION": {
53
- "MESSAGE": "Are you sure you want to delete {count} selected items?",
54
- "ALL": "all {totalCount}"
55
- }
56
- }
57
- }
58
- }
59
- }
1
+ {
2
+ "SAMPLE_APP": {
3
+ "MENU": {
4
+ "TITLE": "Sample"
5
+ },
6
+ "PAGES": {
7
+ "LIST": {
8
+ "TITLE": "Sample list",
9
+ "TOOLBAR": {
10
+ "REFRESH": "Refresh",
11
+ "REMOVE": "Remove"
12
+ },
13
+ "SEARCH": {
14
+ "PLACEHOLDER": "Search keywords"
15
+ },
16
+ "TABLE": {
17
+ "TOTALS": "Count:",
18
+ "HEADER": {
19
+ "IMAGE": "Img",
20
+ "PRODUCT_NAME": "Product name",
21
+ "DESCRIPTION": "Description",
22
+ "PRICE": "Price",
23
+ "SALE_PRICE": "Sale price",
24
+ "CURRENCY": "Currency"
25
+ },
26
+ "ACTIONS": {
27
+ "DELETE": "Delete"
28
+ }
29
+ },
30
+ "EMPTY": {
31
+ "NO_ITEMS": "No items found",
32
+ "ADD": "Add item"
33
+ },
34
+ "NOT_FOUND": {
35
+ "EMPTY": "No items found",
36
+ "RESET": "Reset"
37
+ }
38
+ },
39
+ "DETAILS": {
40
+ "TITLE": {
41
+ "DETAILS": " details",
42
+ "LOADING": "Loading..."
43
+ },
44
+ "TOOLBAR": {
45
+ "SAVE": "Save",
46
+ "DELETE": "Delete"
47
+ },
48
+ "FIELDS": {
49
+ "NAME": "Name",
50
+ "CONTENT": "Content",
51
+ "GUID": "GUID",
52
+ "DESCRIPTION": "Description",
53
+ "PRICE": "Price",
54
+ "SALE_PRICE": "Sale price"
55
+ }
56
+ },
57
+ "ALERTS": {
58
+ "CLOSE_CONFIRMATION": "You have unsaved changes. Close anyway?",
59
+ "DELETE": "Are you sure you want to delete this item?",
60
+ "DELETE_SELECTED_CONFIRMATION": {
61
+ "MESSAGE": "Are you sure you want to delete {count} selected items?",
62
+ "ALL": "all {totalCount}"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
@@ -11,7 +11,7 @@
11
11
  @collapse="$emit('collapse:blade')"
12
12
  >
13
13
  <VcContainer class="tw-p-2">
14
- <VcForm>
14
+ <VcForm class="tw-space-y-4">
15
15
  <Field
16
16
  v-slot="{ errorMessage, handleChange, errors }"
17
17
  name="name"
@@ -25,15 +25,11 @@
25
25
  required
26
26
  :error="!!errors.length"
27
27
  :error-message="errorMessage"
28
- class="tw-mb-4"
29
28
  @update:model-value="handleChange"
30
29
  ></VcInput>
31
30
  </Field>
32
- <VcCard
33
- header="Content"
34
- class="tw-mb-4"
35
- >
36
- <div class="tw-p-4">
31
+ <VcCard header="Content">
32
+ <div class="tw-p-4 tw-space-y-4">
37
33
  <Field
38
34
  v-slot="{ errorMessage, handleChange, errors }"
39
35
  name="guid"
@@ -50,7 +46,6 @@
50
46
  required
51
47
  :error="!!errors.length"
52
48
  :error-message="errorMessage"
53
- class="tw-mb-4"
54
49
  @update:model-value="handleChange"
55
50
  ></VcInput>
56
51
  </Field>
@@ -137,11 +132,10 @@
137
132
  <script lang="ts" setup>
138
133
  import { IBladeToolbar, IParentCallArgs, useBeforeUnload, useBladeNavigation, usePopup } from "@vc-shell/framework";
139
134
  import { useDetails } from "./../composables";
140
- import { computed, onMounted, ref, unref, watch } from "vue";
141
- import { Field, useForm, useIsFormDirty, useIsFormValid } from "vee-validate";
135
+ import { computed, onMounted, ref } from "vue";
136
+ import { Field, useForm } from "vee-validate";
142
137
  import { useI18n } from "vue-i18n";
143
138
  import * as _ from "lodash-es";
144
- import { MockedItem } from "../sample-data";
145
139
 
146
140
  export interface Props {
147
141
  expanded?: boolean;
@@ -210,7 +204,7 @@ const bladeToolbar = ref<IBladeToolbar[]>([
210
204
  icon: "material-delete",
211
205
  title: "Delete",
212
206
  async clickHandler() {
213
- if (await showConfirmation(computed(() => t(`SAMPLE_APP.PAGES.ALERTS.DELETE`)))) {
207
+ if (await showConfirmation(t(`SAMPLE_APP.PAGES.ALERTS.DELETE`))) {
214
208
  if (props.param) {
215
209
  await removeItem({ id: props.param });
216
210
  emit("parent:call", {
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <VcBlade
3
- :title="$t('SAMPLE_APP.PAGES.LIST.TITLE')"
3
+ :title="title"
4
4
  width="50%"
5
5
  :expanded="expanded"
6
6
  :closable="closable"
@@ -22,16 +22,20 @@
22
22
  :total-count="totalCount"
23
23
  :search-value="searchValue"
24
24
  :current-page="currentPage"
25
+ enable-item-actions
26
+ :item-action-builder="actionBuilder"
27
+ :empty="empty"
28
+ :notfound="notfound"
25
29
  :search-placeholder="$t('SAMPLE_APP.PAGES.LIST.SEARCH.PLACEHOLDER')"
26
30
  :total-label="$t('SAMPLE_APP.PAGES.LIST.TABLE.TOTALS')"
27
31
  :selected-item-id="selectedItemId"
28
32
  state-key="SAMPLE_APP"
29
- :items="data"
30
- :item-action-builder="actionBuilder"
31
- enable-item-actions
33
+ :items="data ?? []"
32
34
  @item-click="onItemClick"
33
35
  @header-click="onHeaderClick"
34
36
  @selection-changed="onSelectionChanged"
37
+ @search:change="onSearchList"
38
+ @pagination-click="onPaginationClick"
35
39
  >
36
40
  </VcTable>
37
41
  </VcBlade>
@@ -47,6 +51,7 @@ import {
47
51
  useBladeNavigation,
48
52
  usePopup,
49
53
  useTableSort,
54
+ useFunctions,
50
55
  } from "@vc-shell/framework";
51
56
  import { useI18n } from "vue-i18n";
52
57
  import { useList } from "./../composables";
@@ -88,6 +93,7 @@ defineEmits<Emits>();
88
93
  const { t } = useI18n({ useScope: "global" });
89
94
  const { openBlade } = useBladeNavigation();
90
95
  const { showConfirmation } = usePopup();
96
+ const { debounce } = useFunctions();
91
97
 
92
98
  const { sortExpression, handleSortChange: tableSortHandler } = useTableSort({
93
99
  initialDirection: "DESC",
@@ -103,10 +109,32 @@ const searchValue = ref();
103
109
  const selectedItemId = ref<string>();
104
110
  const selectedIds = ref<string[]>([]);
105
111
 
112
+ const empty = {
113
+ icon: "lucide-file",
114
+ text: computed(() => t("SAMPLE_APP.PAGES.LIST.EMPTY.NO_ITEMS")),
115
+ action: computed(() => t("SAMPLE_APP.PAGES.LIST.EMPTY.ADD")),
116
+ clickHandler: () => {
117
+ addItem();
118
+ },
119
+ };
120
+
121
+ const notfound = {
122
+ icon: "lucide-file",
123
+ text: computed(() => t("SAMPLE_APP.PAGES.LIST.NOT_FOUND.EMPTY")),
124
+ action: computed(() => t("SAMPLE_APP.PAGES.LIST.NOT_FOUND.RESET")),
125
+ clickHandler: async () => {
126
+ searchValue.value = "";
127
+ await getItems({
128
+ ...searchQuery.value,
129
+ keyword: "",
130
+ });
131
+ },
132
+ };
133
+
106
134
  watch(
107
135
  () => props.param,
108
136
  (newVal) => {
109
- selectedItemId.value = newVal;
137
+ selectedItemId.value = newVal;
110
138
  },
111
139
  { immediate: true },
112
140
  );
@@ -118,6 +146,27 @@ onMounted(async () => {
118
146
  });
119
147
  });
120
148
 
149
+ const onSearchList = debounce(async (keyword: string) => {
150
+ searchValue.value = keyword;
151
+ await getItems({
152
+ ...searchQuery.value,
153
+ keyword,
154
+ });
155
+ }, 1000);
156
+
157
+ const addItem = () => {
158
+ openBlade({
159
+ blade: markRaw(Details),
160
+ });
161
+ };
162
+
163
+ const onPaginationClick = async (page: number) => {
164
+ await getItems({
165
+ ...searchQuery.value,
166
+ skip: (page - 1) * (searchQuery.value.take ?? 20),
167
+ });
168
+ };
169
+
121
170
  const bladeToolbar = ref<IBladeToolbar[]>([
122
171
  {
123
172
  id: "refresh",
@@ -171,7 +220,7 @@ const columns = ref<ITableColumns[]>([
171
220
  },
172
221
  ]);
173
222
 
174
- const title = computed(() => t("SAMPLE_APP.PAGES.LIST.HEADER.TITLE"));
223
+ const title = computed(() => t("SAMPLE_APP.PAGES.LIST.TITLE"));
175
224
 
176
225
  const reload = async () => {
177
226
  await getItems({
@@ -208,12 +257,12 @@ const actionBuilder = (): IActionBuilderResult[] => {
208
257
  icon: "material-delete",
209
258
  title: computed(() => t("SAMPLE_APP.PAGES.LIST.TABLE.ACTIONS.DELETE")),
210
259
  type: "danger",
211
- clickHandler(item: MockedItem) {
212
- if (item.id) {
213
- if (!selectedIds.value.includes(item.id)) {
214
- selectedIds.value.push(item.id);
260
+ async clickHandler(_item: MockedItem) {
261
+ if (_item.id) {
262
+ if (!selectedIds.value.includes(_item.id)) {
263
+ selectedIds.value.push(_item.id);
215
264
  }
216
- remove(selectedIds.value);
265
+ await remove(selectedIds.value);
217
266
  selectedIds.value = [];
218
267
  }
219
268
  },
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": "1.1.88",
4
+ "version": "1.1.90",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",
7
7
  "files": [
@@ -9,11 +9,12 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "build": "vite build && yarn postbuild",
12
+ "type-check": "vue-tsc --noEmit",
12
13
  "postbuild": "shx cp -R src/templates dist/"
13
14
  },
14
15
  "devDependencies": {
15
16
  "@types/prompts": "^2.4.4",
16
- "@vc-shell/ts-config": "^1.1.88",
17
+ "@vc-shell/ts-config": "^1.1.90",
17
18
  "copyfiles": "^2.4.1",
18
19
  "cross-env": "^7.0.3",
19
20
  "shx": "^0.3.4",