@vc-shell/create-vc-app 1.1.99-alpha.1 → 1.1.99-alpha.10

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.
Files changed (34) hide show
  1. package/README.md +41 -5
  2. package/dist/cli/argv.d.ts.map +1 -1
  3. package/dist/cli/help.d.ts.map +1 -1
  4. package/dist/cli/types.d.ts +5 -0
  5. package/dist/cli/types.d.ts.map +1 -1
  6. package/dist/commands/generate-blade.d.ts +5 -0
  7. package/dist/commands/generate-blade.d.ts.map +1 -1
  8. package/dist/index.js +940 -789
  9. package/dist/templates/base/_package.json +5 -5
  10. package/dist/templates/blades/grid/blade.vue +329 -340
  11. package/dist/templates/composables/details-composable.ts +2 -0
  12. package/dist/templates/composables/grid-composable.ts +3 -1
  13. package/dist/utils/naming.d.ts +1 -0
  14. package/dist/utils/naming.d.ts.map +1 -1
  15. package/dist/utils/register-module.d.ts +4 -0
  16. package/dist/utils/register-module.d.ts.map +1 -1
  17. package/dist/utils/templates.d.ts +10 -0
  18. package/dist/utils/templates.d.ts.map +1 -0
  19. package/dist/workflows/create-app.d.ts.map +1 -1
  20. package/package.json +3 -3
  21. package/dist/templates/base/ai-guides/.cursorrules-vc-shell +0 -529
  22. package/dist/templates/base/ai-guides/README.md +0 -360
  23. package/dist/templates/base/ai-guides/guides/AI_GUIDE.md +0 -195
  24. package/dist/templates/base/ai-guides/guides/blade-patterns.md +0 -384
  25. package/dist/templates/base/ai-guides/guides/complete-workflow.md +0 -781
  26. package/dist/templates/base/ai-guides/guides/composables-reference.md +0 -338
  27. package/dist/templates/base/ai-guides/guides/troubleshooting.md +0 -529
  28. package/dist/templates/base/ai-guides/guides/ui-components-reference.md +0 -903
  29. package/dist/templates/base/ai-guides/prompts/adapt-existing-module.md +0 -1026
  30. package/dist/templates/base/ai-guides/prompts/advanced-scenarios.md +0 -852
  31. package/dist/templates/base/ai-guides/prompts/api-client-generation.md +0 -877
  32. package/dist/templates/base/ai-guides/prompts/cli-usage.md +0 -640
  33. package/dist/templates/base/ai-guides/prompts/quick-start-scenarios.md +0 -773
  34. package/dist/templates/base/ai-guides/prompts/simple-modifications.md +0 -987
@@ -1,340 +1,329 @@
1
- <template>
2
- <VcBlade
3
- v-loading="loading"
4
- :title="bladeTitle"
5
- width="50%"
6
- :expanded="expanded"
7
- :closable="closable"
8
- :toolbar-items="bladeToolbar"
9
- @close="$emit('close:blade')"
10
- @expand="$emit('expand:blade')"
11
- @collapse="$emit('collapse:blade')"
12
- >
13
- <!-- @vue-generic {I{{EntityName}}} -->
14
- <VcTable
15
- :total-label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.TOTALS')"
16
- :items="items"
17
- :selected-item-id="selectedItemId"
18
- :search-value="searchValue"
19
- :columns="tableColumns"
20
- :sort="sortExpression"
21
- :pages="pages"
22
- :current-page="currentPage"
23
- :total-count="totalCount"
24
- :expanded="expanded"
25
- :active-filter-count="activeFilterCount"
26
- :empty="empty"
27
- :notfound="notfound"
28
- state-key="{{entity_name_plural}}_list"
29
- :multiselect="false"
30
- class="tw-grow tw-basis-0"
31
- @item-click="onItemClick"
32
- @header-click="onHeaderClick"
33
- @pagination-click="onPaginationClick"
34
- @search:change="onSearchList"
35
- @scroll:ptr="reload"
36
- >
37
- <template #filters>
38
- <div class="tw-p-4">
39
- <VcRow class="tw-gap-16">
40
- <div class="tw-flex tw-flex-col">
41
- <!-- Status Filter -->
42
- <h3 class="tw-text-sm tw-font-medium tw-mb-3">
43
- {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.STATUS.TITLE") }}
44
- </h3>
45
- <div class="tw-space-y-2">
46
- <VcRadioButton
47
- v-for="status in statuses"
48
- :key="status.value"
49
- :model-value="stagedFilters.status[0] || ''"
50
- :value="status.value"
51
- :label="status.displayValue"
52
- @update:model-value="(value) => toggleFilter('status', String(value), true)"
53
- >
54
- </VcRadioButton>
55
- </div>
56
- </div>
57
-
58
- <!-- Date Range Filter -->
59
- <div class="tw-flex tw-flex-col">
60
- <h3 class="tw-text-sm tw-font-medium tw-mb-3">
61
- {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.TITLE") }}
62
- </h3>
63
- <div class="tw-space-y-3">
64
- <VcInput
65
- v-model="stagedFilters.startDate"
66
- type="date"
67
- :label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.START_DATE')"
68
- @update:model-value="(value) => toggleFilter('startDate', String(value || ''), true)"
69
- />
70
- <VcInput
71
- v-model="stagedFilters.endDate"
72
- type="date"
73
- :label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.END_DATE')"
74
- @update:model-value="(value) => toggleFilter('endDate', String(value || ''), true)"
75
- />
76
- </div>
77
- </div>
78
- </VcRow>
79
-
80
- <!-- Filter Controls -->
81
- <div class="tw-flex tw-gap-2 tw-mt-4">
82
- <VcButton
83
- variant="primary"
84
- :disabled="!hasFilterChanges"
85
- @click="applyFilters"
86
- >
87
- {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.APPLY") }}
88
- </VcButton>
89
-
90
- <VcButton
91
- variant="secondary"
92
- :disabled="!hasFiltersApplied"
93
- @click="resetFilters"
94
- >
95
- {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.RESET") }}
96
- </VcButton>
97
- </div>
98
- </div>
99
- </template>
100
-
101
- <!-- Override column template example -->
102
- <template #item_name="itemData">
103
- <div class="tw-truncate">
104
- {{ itemData.item.name }}
105
- </div>
106
- </template>
107
-
108
- <!-- Status column template example -->
109
- <template #item_isActive="itemData">
110
- <VcStatusIcon :status="itemData.item.isActive" />
111
- </template>
112
- </VcTable>
113
- </VcBlade>
114
- </template>
115
-
116
- <script lang="ts" setup>
117
- import { computed, inject, onMounted, ref, watch, markRaw, Ref } from "vue";
118
- import {
119
- IBladeToolbar,
120
- IParentCallArgs,
121
- useFunctions,
122
- IActionBuilderResult,
123
- ITableColumns,
124
- useNotifications,
125
- notification,
126
- useBladeNavigation,
127
- usePopup,
128
- useTableSort,
129
- useBlade,
130
- } from "@vc-shell/framework";
131
- import { use{{EntityName}}List } from "../composables";
132
- import {{EntityName}}Details from "./{{entity-name}}-details.vue";
133
- import { useI18n } from "vue-i18n";
134
-
135
- // TODO: Replace with your actual types
136
- // Example: import { IProduct } from "@your-app/api/products";
137
-
138
- export interface Props {
139
- expanded?: boolean;
140
- closable?: boolean;
141
- param?: string;
142
- options?: Record<string, unknown>;
143
- }
144
-
145
- export interface Emits {
146
- (event: "parent:call", args: IParentCallArgs): void;
147
- (event: "close:blade"): void;
148
- (event: "collapse:blade"): void;
149
- (event: "expand:blade"): void;
150
- }
151
-
152
- defineOptions({
153
- name: "{{EntityName}}List",
154
- url: "/{{entity-name-plural}}",
155
- notifyType: "{{EntityName}}DeletedDomainEvent",
156
- isWorkspace: {{isWorkspace}},
157
- menuItem: {{menuItem}},
158
- });
159
-
160
- const props = withDefaults(defineProps<Props>(), {
161
- expanded: true,
162
- closable: true,
163
- });
164
-
165
- const emit = defineEmits<Emits>();
166
- const { openBlade, closeBlade } = useBladeNavigation();
167
- const { showConfirmation } = usePopup();
168
- const { t } = useI18n({ useScope: "global" });
169
- const { debounce } = useFunctions();
170
-
171
- const {
172
- searchQuery,
173
- items,
174
- totalCount,
175
- pages,
176
- currentPage,
177
- load{{EntityName}}s,
178
- loading,
179
- statuses,
180
- // Filters
181
- stagedFilters,
182
- appliedFilters,
183
- hasFilterChanges,
184
- hasFiltersApplied,
185
- activeFilterCount,
186
- toggleFilter,
187
- applyFilters,
188
- resetFilters,
189
- resetSearch,
190
- } = use{{EntityName}}List();
191
-
192
- const { markAsRead, setNotificationHandler } = useNotifications("{{EntityName}}DeletedDomainEvent");
193
- const { sortExpression, handleSortChange } = useTableSort({
194
- initialProperty: "createdDate",
195
- initialDirection: "DESC",
196
- });
197
- const blade = useBlade();
198
-
199
- const searchValue = ref();
200
- const selectedItemId = ref<string>();
201
- const isDesktop = inject<Ref<boolean>>("isDesktop");
202
- const bladeTitle = computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TITLE"));
203
-
204
- setNotificationHandler((message) => {
205
- if (message.title) {
206
- notification.success(message.title, {
207
- onClose() {
208
- markAsRead(message);
209
- },
210
- });
211
- }
212
- });
213
-
214
- watch(sortExpression, async (value) => {
215
- await load{{EntityName}}s({ ...searchQuery.value, sort: value });
216
- });
217
-
218
- watch(
219
- () => props.param,
220
- (newVal) => {
221
- if (newVal) {
222
- selectedItemId.value = newVal;
223
- }
224
- },
225
- { immediate: true, deep: true },
226
- );
227
-
228
- onMounted(async () => {
229
- await load{{EntityName}}s({ ...searchQuery.value, sort: sortExpression.value });
230
- });
231
-
232
- const reload = async () => {
233
- await load{{EntityName}}s({
234
- ...searchQuery.value,
235
- skip: (currentPage.value - 1) * (searchQuery.value.take ?? 20),
236
- sort: sortExpression.value,
237
- });
238
- emit("parent:call", { method: "reload" });
239
- };
240
-
241
- const onSearchList = debounce(async (keyword: string) => {
242
- searchValue.value = keyword;
243
- await load{{EntityName}}s({
244
- ...searchQuery.value,
245
- keyword,
246
- });
247
- }, 1000);
248
-
249
- const bladeToolbar = ref<IBladeToolbar[]>([
250
- {
251
- id: "refresh",
252
- title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TOOLBAR.REFRESH")),
253
- icon: "material-refresh",
254
- async clickHandler() {
255
- await reload();
256
- },
257
- },
258
- {
259
- id: "add",
260
- title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TOOLBAR.ADD")),
261
- icon: "material-add",
262
- clickHandler() {
263
- add{{EntityName}}();
264
- },
265
- },
266
- ]);
267
-
268
- const tableColumns = ref<ITableColumns[]>([
269
- {
270
- id: "name",
271
- field: "name",
272
- title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.HEADER.NAME")),
273
- sortable: true,
274
- alwaysVisible: true,
275
- mobilePosition: "top-left",
276
- },
277
- {
278
- id: "createdDate",
279
- title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.HEADER.CREATED_DATE")),
280
- sortable: true,
281
- type: "date-ago",
282
- mobilePosition: "bottom-right",
283
- },
284
- ]);
285
-
286
- const empty = {
287
- icon: "material-list",
288
- text: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.EMPTY.NO_ITEMS")),
289
- action: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.EMPTY.ADD")),
290
- clickHandler: () => {
291
- add{{EntityName}}();
292
- },
293
- };
294
-
295
- const notfound = {
296
- icon: "material-list",
297
- text: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.NOT_FOUND.EMPTY")),
298
- action: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.NOT_FOUND.RESET")),
299
- clickHandler: async () => {
300
- searchValue.value = "";
301
- await resetSearch();
302
- },
303
- };
304
-
305
- const onItemClick = (item: { id?: string }) => {
306
- openBlade({
307
- blade: markRaw({{EntityName}}Details),
308
- param: item.id,
309
- onOpen() {
310
- selectedItemId.value = item.id;
311
- },
312
- onClose() {
313
- selectedItemId.value = undefined;
314
- },
315
- });
316
- };
317
-
318
- const onHeaderClick = (item: ITableColumns) => {
319
- handleSortChange(item.id);
320
- };
321
-
322
- const add{{EntityName}} = () => {
323
- openBlade({
324
- blade: markRaw({{EntityName}}Details),
325
- });
326
- };
327
-
328
- const onPaginationClick = async (page: number) => {
329
- await load{{EntityName}}s({
330
- ...searchQuery.value,
331
- skip: (page - 1) * (searchQuery.value.take ?? 20),
332
- });
333
- };
334
-
335
- defineExpose({
336
- title: bladeTitle,
337
- reload,
338
- onItemClick,
339
- });
340
- </script>
1
+ <template>
2
+ <VcBlade
3
+ v-loading="loading"
4
+ :title="bladeTitle"
5
+ width="50%"
6
+ :expanded="expanded"
7
+ :closable="closable"
8
+ :toolbar-items="bladeToolbar"
9
+ @close="$emit('close:blade')"
10
+ @expand="$emit('expand:blade')"
11
+ @collapse="$emit('collapse:blade')"
12
+ >
13
+ <!-- @vue-generic {I{{EntityName}}} -->
14
+ <VcTable
15
+ :total-label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.TOTALS')"
16
+ :items="items"
17
+ :selected-item-id="selectedItemId"
18
+ :search-value="searchValue"
19
+ :columns="tableColumns"
20
+ :sort="sortExpression"
21
+ :pages="pages"
22
+ :current-page="currentPage"
23
+ :total-count="totalCount"
24
+ :expanded="expanded"
25
+ :active-filter-count="activeFilterCount"
26
+ :empty="empty"
27
+ :notfound="notfound"
28
+ state-key="{{entity_name_plural}}_list"
29
+ :multiselect="false"
30
+ class="tw-grow tw-basis-0"
31
+ @item-click="onItemClick"
32
+ @header-click="onHeaderClick"
33
+ @pagination-click="onPaginationClick"
34
+ @search:change="onSearchList"
35
+ @scroll:ptr="reload"
36
+ >
37
+ <template #filters>
38
+ <div class="tw-p-4">
39
+ <VcRow class="tw-gap-16">
40
+ <div class="tw-flex tw-flex-col">
41
+ <!-- Status Filter -->
42
+ <h3 class="tw-text-sm tw-font-medium tw-mb-3">
43
+ {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.STATUS.TITLE") }}
44
+ </h3>
45
+ <div class="tw-space-y-2">
46
+ <VcRadioButton
47
+ v-for="status in statuses"
48
+ :key="status.value"
49
+ :model-value="stagedFilters.status[0] || ''"
50
+ :value="status.value"
51
+ :label="status.displayValue"
52
+ @update:model-value="(value) => toggleFilter('status', String(value), true)"
53
+ >
54
+ </VcRadioButton>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Date Range Filter -->
59
+ <div class="tw-flex tw-flex-col">
60
+ <h3 class="tw-text-sm tw-font-medium tw-mb-3">
61
+ {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.TITLE") }}
62
+ </h3>
63
+ <div class="tw-space-y-3">
64
+ <VcInput
65
+ v-model="stagedFilters.startDate"
66
+ type="date"
67
+ :label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.START_DATE')"
68
+ @update:model-value="(value: string) => toggleFilter('startDate', String(value || ''), true)"
69
+ />
70
+ <VcInput
71
+ v-model="stagedFilters.endDate"
72
+ type="date"
73
+ :label="$t('{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.DATE.END_DATE')"
74
+ @update:model-value="(value: string) => toggleFilter('endDate', String(value || ''), true)"
75
+ />
76
+ </div>
77
+ </div>
78
+ </VcRow>
79
+
80
+ <!-- Filter Controls -->
81
+ <div class="tw-flex tw-gap-2 tw-mt-4">
82
+ <VcButton
83
+ variant="primary"
84
+ :disabled="!hasFilterChanges"
85
+ @click="applyFilters"
86
+ >
87
+ {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.APPLY") }}
88
+ </VcButton>
89
+
90
+ <VcButton
91
+ variant="secondary"
92
+ :disabled="!hasFiltersApplied"
93
+ @click="resetFilters"
94
+ >
95
+ {{ $t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.FILTER.RESET") }}
96
+ </VcButton>
97
+ </div>
98
+ </div>
99
+ </template>
100
+
101
+ <!-- Override column template example -->
102
+ <template #item_name="itemData">
103
+ <div class="tw-truncate">
104
+ {{ itemData.item.name }}
105
+ </div>
106
+ </template>
107
+
108
+ <!-- Status column template example -->
109
+ <template #item_isActive="itemData">
110
+ <VcStatusIcon :status="itemData.item.isActive" />
111
+ </template>
112
+ </VcTable>
113
+ </VcBlade>
114
+ </template>
115
+
116
+ <script lang="ts" setup>
117
+ import { computed, inject, onMounted, ref, watch, markRaw, Ref } from "vue";
118
+ import {
119
+ IBladeToolbar,
120
+ IParentCallArgs,
121
+ useFunctions,
122
+ IActionBuilderResult,
123
+ ITableColumns,
124
+ useNotifications,
125
+ notification,
126
+ useBladeNavigation,
127
+ usePopup,
128
+ useTableSort,
129
+ useBlade,
130
+ } from "@vc-shell/framework";
131
+ import { use{{EntityName}}List } from "../composables";
132
+ {{DETAILS_BLADE_IMPORT}}
133
+ import { useI18n } from "vue-i18n";
134
+
135
+ // TODO: Replace with your actual types
136
+ // Example: import { IProduct } from "@your-app/api/products";
137
+
138
+ export interface Props {
139
+ expanded?: boolean;
140
+ closable?: boolean;
141
+ param?: string;
142
+ options?: Record<string, unknown>;
143
+ }
144
+
145
+ export interface Emits {
146
+ (event: "parent:call", args: IParentCallArgs): void;
147
+ (event: "close:blade"): void;
148
+ (event: "collapse:blade"): void;
149
+ (event: "expand:blade"): void;
150
+ }
151
+
152
+ defineOptions({
153
+ name: "{{EntityName}}List",
154
+ url: "/{{entity-name-list}}",
155
+ notifyType: "{{EntityName}}DeletedDomainEvent",
156
+ isWorkspace: {{isWorkspace}},
157
+ menuItem: {{menuItem}},
158
+ });
159
+
160
+ const props = withDefaults(defineProps<Props>(), {
161
+ expanded: true,
162
+ closable: true,
163
+ });
164
+
165
+ const emit = defineEmits<Emits>();
166
+ const { openBlade, closeBlade } = useBladeNavigation();
167
+ const { showConfirmation } = usePopup();
168
+ const { t } = useI18n({ useScope: "global" });
169
+ const { debounce } = useFunctions();
170
+
171
+ const {
172
+ searchQuery,
173
+ items,
174
+ totalCount,
175
+ pages,
176
+ currentPage,
177
+ load{{EntityName}}s,
178
+ loading,
179
+ statuses,
180
+ // Filters
181
+ stagedFilters,
182
+ appliedFilters,
183
+ hasFilterChanges,
184
+ hasFiltersApplied,
185
+ activeFilterCount,
186
+ toggleFilter,
187
+ applyFilters,
188
+ resetFilters,
189
+ resetSearch,
190
+ } = use{{EntityName}}List();
191
+
192
+ const { markAsRead, setNotificationHandler } = useNotifications("{{EntityName}}DeletedDomainEvent");
193
+ const { sortExpression, handleSortChange } = useTableSort({
194
+ initialProperty: "createdDate",
195
+ initialDirection: "DESC",
196
+ });
197
+ const blade = useBlade();
198
+
199
+ const searchValue = ref();
200
+ const selectedItemId = ref<string>();
201
+ const isDesktop = inject<Ref<boolean>>("isDesktop");
202
+ const bladeTitle = computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TITLE"));
203
+
204
+ setNotificationHandler((message) => {
205
+ if (message.title) {
206
+ notification.success(message.title, {
207
+ onClose() {
208
+ markAsRead(message);
209
+ },
210
+ });
211
+ }
212
+ });
213
+
214
+ watch(sortExpression, async (value) => {
215
+ await load{{EntityName}}s({ ...searchQuery.value, sort: value });
216
+ });
217
+
218
+ watch(
219
+ () => props.param,
220
+ (newVal) => {
221
+ if (newVal) {
222
+ selectedItemId.value = newVal;
223
+ }
224
+ },
225
+ { immediate: true, deep: true },
226
+ );
227
+
228
+ onMounted(async () => {
229
+ await load{{EntityName}}s({ ...searchQuery.value, sort: sortExpression.value });
230
+ });
231
+
232
+ const reload = async () => {
233
+ await load{{EntityName}}s({
234
+ ...searchQuery.value,
235
+ skip: (currentPage.value - 1) * (searchQuery.value.take ?? 20),
236
+ sort: sortExpression.value,
237
+ });
238
+ emit("parent:call", { method: "reload" });
239
+ };
240
+
241
+ const onSearchList = debounce(async (keyword: string) => {
242
+ searchValue.value = keyword;
243
+ await load{{EntityName}}s({
244
+ ...searchQuery.value,
245
+ keyword,
246
+ });
247
+ }, 1000);
248
+
249
+ const bladeToolbar = ref<IBladeToolbar[]>([
250
+ {
251
+ id: "refresh",
252
+ title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TOOLBAR.REFRESH")),
253
+ icon: "material-refresh",
254
+ async clickHandler() {
255
+ await reload();
256
+ },
257
+ },
258
+ {
259
+ id: "add",
260
+ title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TOOLBAR.ADD")),
261
+ icon: "material-add",
262
+ clickHandler() {
263
+ add{{EntityName}}();
264
+ },
265
+ },
266
+ ]);
267
+
268
+ const tableColumns = ref<ITableColumns[]>([
269
+ {
270
+ id: "name",
271
+ field: "name",
272
+ title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.HEADER.NAME")),
273
+ sortable: true,
274
+ alwaysVisible: true,
275
+ mobilePosition: "top-left",
276
+ },
277
+ {
278
+ id: "createdDate",
279
+ title: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.TABLE.HEADER.CREATED_DATE")),
280
+ sortable: true,
281
+ type: "date-ago",
282
+ mobilePosition: "bottom-right",
283
+ },
284
+ ]);
285
+
286
+ const empty = {
287
+ icon: "material-list",
288
+ text: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.EMPTY.NO_ITEMS")),
289
+ action: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.EMPTY.ADD")),
290
+ clickHandler: () => {
291
+ add{{EntityName}}();
292
+ },
293
+ };
294
+
295
+ const notfound = {
296
+ icon: "material-list",
297
+ text: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.NOT_FOUND.EMPTY")),
298
+ action: computed(() => t("{{MODULE_NAME_UPPERCASE}}.PAGES.LIST.NOT_FOUND.RESET")),
299
+ clickHandler: async () => {
300
+ searchValue.value = "";
301
+ await resetSearch();
302
+ },
303
+ };
304
+
305
+ const onItemClick = (item: { id?: string }) => {
306
+ {{DETAILS_BLADE_OPEN_ITEM}}
307
+ };
308
+
309
+ const onHeaderClick = (item: ITableColumns) => {
310
+ handleSortChange(item.id);
311
+ };
312
+
313
+ const add{{EntityName}} = () => {
314
+ {{DETAILS_BLADE_OPEN_NEW}}
315
+ };
316
+
317
+ const onPaginationClick = async (page: number) => {
318
+ await load{{EntityName}}s({
319
+ ...searchQuery.value,
320
+ skip: (page - 1) * (searchQuery.value.take ?? 20),
321
+ });
322
+ };
323
+
324
+ defineExpose({
325
+ title: bladeTitle,
326
+ reload,
327
+ onItemClick,
328
+ });
329
+ </script>
@@ -84,7 +84,9 @@ export function use{{EntityName}}Details(): IUse{{EntityName}}Details {
84
84
  }
85
85
  );
86
86
 
87
+ // IMPORTANT: useAsync<string> means id is typed as string | undefined - always guard!
87
88
  const { action: delete{{EntityName}}, loading: deleting{{EntityName}} } = useAsync<string>(async (id) => {
89
+ if (!id) return; // Guard required - id can be undefined
88
90
  const apiClient = await getApiClient();
89
91
  await apiClient.delete{{EntityName}}(id);
90
92
  });