@vc-shell/vc-app-skill 2.0.0-alpha.23 → 2.0.0-alpha.24

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/knowledge-stats.cjs +135 -0
  3. package/bin/sync-docs.cjs +62 -0
  4. package/package.json +5 -1
  5. package/runtime/VERSION +1 -1
  6. package/runtime/agents/details-blade-generator.md +75 -14
  7. package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
  8. package/runtime/knowledge/docs/core/api/platform.docs.md +14 -7
  9. package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +6 -0
  10. package/runtime/knowledge/docs/core/composables/useApiClient/useApiClient.docs.md +6 -0
  11. package/runtime/knowledge/docs/core/composables/useAssetsManager/useAssetsManager.docs.md +149 -0
  12. package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +7 -0
  13. package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +7 -0
  14. package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +6 -0
  15. package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +6 -0
  16. package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +6 -0
  17. package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +6 -0
  18. package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +6 -0
  19. package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +6 -0
  20. package/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +6 -0
  21. package/runtime/knowledge/docs/core/plugins/i18n/i18n.docs.md +74 -0
  22. package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +6 -0
  23. package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +6 -0
  24. package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +6 -0
  25. package/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +6 -0
  26. package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +29 -33
  27. package/runtime/knowledge/docs/shell/components/logout-button/logout-button.docs.md +3 -3
  28. package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +11 -0
  29. package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +11 -0
  30. package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +11 -0
  31. package/runtime/knowledge/docs/ui/components/organisms/vc-table/vc-data-table.docs.md +12 -0
  32. package/runtime/knowledge/index.md +60 -0
  33. package/runtime/knowledge/patterns/assets-management.md +213 -0
  34. package/runtime/knowledge/patterns/child-blade-flow.md +277 -0
  35. package/runtime/knowledge/patterns/details-blade-pattern.md +350 -3
  36. package/runtime/knowledge/patterns/extension-points-usage.md +308 -0
  37. package/runtime/knowledge/patterns/form-validation.md +377 -0
  38. package/runtime/knowledge/patterns/multilanguage-fields.md +239 -0
  39. package/runtime/knowledge/patterns/signalr-notifications.md +237 -0
  40. package/runtime/vc-app.md +44 -5
  41. package/runtime/knowledge/docs/shell/components/app-switcher/app-switcher.docs.md +0 -104
@@ -0,0 +1,213 @@
1
+ # Assets Management Pattern
2
+
3
+ Integrating file/image assets into a details blade. Covers two scenarios: inline gallery on the blade and an AssetsWidget opening the AssetsManager as a child blade.
4
+
5
+ Both scenarios use `useAssetsManager` — a composable that wraps `useAssets` with higher-level operations (upload with deduplication, remove with confirmation, reorder). The manager operates on a **writable computed** bound to the parent entity's asset array.
6
+
7
+ ---
8
+
9
+ ## Core Setup — Writable Computed + useAssetsManager
10
+
11
+ The key is creating a two-way binding between the entity's asset array and the manager:
12
+
13
+ ```ts
14
+ import { computed, markRaw } from "vue";
15
+ import { useAssetsManager, usePopup } from "@vc-shell/framework";
16
+ import type { Asset } from "../api_client/...";
17
+
18
+ // `item` is a Ref to the parent entity (e.g., product, category, order)
19
+ const entityAssets = computed({
20
+ get: () => (item.value?.assets ?? []) as Asset[],
21
+ set: (val) => {
22
+ if (item.value) item.value.assets = val;
23
+ },
24
+ });
25
+
26
+ const { showConfirmation } = usePopup();
27
+
28
+ const assetsManager = useAssetsManager(entityAssets, {
29
+ // Dynamic upload path — resolved at upload time
30
+ uploadPath: () => `/catalog/${item.value?.id}`,
31
+ // Optional: confirmation dialog before remove
32
+ confirmRemove: () => showConfirmation("Delete selected assets?"),
33
+ });
34
+ ```
35
+
36
+ **Why writable computed?** `useAssetsManager` reads and writes the asset array reactively. A writable computed bridges the gap between the manager and the nested entity field (`item.value.assets`). When the manager sets a new array after upload/remove/reorder, Vue's reactivity propagates the change to the entity — making it trackable by `useModificationTracker`.
37
+
38
+ ---
39
+
40
+ ## Scenario 1 — Inline Gallery (VcGallery on the blade)
41
+
42
+ Use when images are a primary field of the entity (e.g., product images shown directly on the details blade).
43
+
44
+ ```vue
45
+ <template>
46
+ <VcCard header="Images">
47
+ <div class="tw-p-2">
48
+ <VcGallery
49
+ :images="item.images"
50
+ :multiple="!disabled"
51
+ :disabled="disabled"
52
+ :loading="assets.loading.value"
53
+ @upload="assets.upload"
54
+ @remove="assets.remove"
55
+ @sort="assets.reorder"
56
+ @edit="onEditAsset"
57
+ />
58
+ </div>
59
+ </VcCard>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import { computed } from "vue";
64
+ import { useBlade, useAssetsManager, usePopup } from "@vc-shell/framework";
65
+
66
+ const { openBlade } = useBlade();
67
+ const { showConfirmation } = usePopup();
68
+
69
+ const images = computed({
70
+ get: () => item.value?.images ?? [],
71
+ set: (val) => { if (item.value) item.value.images = val; },
72
+ });
73
+
74
+ const assets = useAssetsManager(images, {
75
+ uploadPath: () => `/catalog/${item.value?.id}`,
76
+ confirmRemove: () => showConfirmation("Delete selected images?"),
77
+ });
78
+
79
+ // Open AssetsDetails blade for editing a single asset (alt text, crop, etc.)
80
+ function onEditAsset(asset: Asset) {
81
+ openBlade({
82
+ name: "AssetsDetails",
83
+ options: {
84
+ asset,
85
+ assetEditHandler: (updated: Asset) => assets.updateItem(updated),
86
+ assetRemoveHandler: (toRemove: Asset) => assets.remove(toRemove),
87
+ },
88
+ });
89
+ }
90
+ </script>
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Scenario 2 — AssetsWidget (child blade)
96
+
97
+ Use when assets are a secondary concern shown as a widget badge. Clicking the widget opens the built-in `AssetsManager` blade.
98
+
99
+ ### Widget composable
100
+
101
+ ```ts
102
+ // widgets/useEntityWidgets.ts
103
+ import { computed, markRaw, type Ref, type ComputedRef } from "vue";
104
+ import {
105
+ useBlade,
106
+ useBladeWidgets,
107
+ useAssetsManager,
108
+ usePopup,
109
+ type UseBladeWidgetsReturn,
110
+ } from "@vc-shell/framework";
111
+
112
+ interface Options {
113
+ item: Ref<Entity | undefined>;
114
+ disabled: ComputedRef<boolean>;
115
+ isVisible: ComputedRef<boolean>;
116
+ }
117
+
118
+ export function useEntityWidgets({ item, disabled, isVisible }: Options): UseBladeWidgetsReturn {
119
+ const { openBlade } = useBlade();
120
+ const { showConfirmation } = usePopup();
121
+
122
+ // Assets manager for the widget
123
+ const entityAssets = computed({
124
+ get: () => (item.value?.assets ?? []) as Asset[],
125
+ set: (val) => {
126
+ if (item.value) item.value.assets = val;
127
+ },
128
+ });
129
+
130
+ const assetsManager = useAssetsManager(entityAssets, {
131
+ uploadPath: () => `/files/${item.value?.id}`,
132
+ confirmRemove: () => showConfirmation("Delete selected assets?"),
133
+ });
134
+
135
+ const assetsCount = computed(() => item.value?.assets?.length ?? 0);
136
+
137
+ return useBladeWidgets([
138
+ {
139
+ id: "AssetsWidget",
140
+ icon: "lucide-file",
141
+ title: "MODULE.WIDGETS.ASSETS",
142
+ badge: assetsCount,
143
+ isVisible,
144
+ onClick: () =>
145
+ openBlade({
146
+ name: "AssetsManager",
147
+ options: {
148
+ manager: markRaw(assetsManager), // markRaw prevents Vue from making it reactive
149
+ disabled: disabled.value,
150
+ },
151
+ }),
152
+ },
153
+ // ... other widgets
154
+ ]);
155
+ }
156
+ ```
157
+
158
+ ### Usage in the details blade
159
+
160
+ ```vue
161
+ <script setup lang="ts">
162
+ import { useEntityWidgets } from "../widgets";
163
+
164
+ const { item, loading } = useEntityDetails();
165
+
166
+ useEntityWidgets({
167
+ item,
168
+ disabled: computed(() => !!param.value && !item.value?.canBeModified),
169
+ isVisible: computed(() => !!param.value), // hide widgets on "create new"
170
+ });
171
+ </script>
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Combining Both Scenarios
177
+
178
+ A blade can use **both** an inline gallery and an assets widget. The product details blade demonstrates this:
179
+
180
+ - **Inline gallery** — `useAssetsManager` for product images, bound to `item.productData.images`, rendered via `VcGallery`
181
+ - **Assets widget** — a separate `useAssetsManager` for product file assets, bound to `item.productData.assets`, opened via `AssetsManager` blade
182
+
183
+ Each manager operates on a different writable computed pointing to a different array on the entity. They are fully independent.
184
+
185
+ ```ts
186
+ // Gallery assets (images)
187
+ const productImages = computed({
188
+ get: () => item.value.productData?.images ?? [],
189
+ set: (val) => { if (item.value.productData) item.value.productData.images = val; },
190
+ });
191
+ const imageAssets = useAssetsManager(productImages, { uploadPath: () => `/catalog/${item.value.id}` });
192
+
193
+ // Widget assets (files)
194
+ const productFiles = computed({
195
+ get: () => (item.value?.productData?.assets ?? []) as Asset[],
196
+ set: (val) => { if (item.value?.productData) item.value.productData.assets = val; },
197
+ });
198
+ const fileAssets = useAssetsManager(productFiles, { uploadPath: () => `/catalog/${item.value.id}` });
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Key Points
204
+
205
+ | Concern | Approach |
206
+ |---|---|
207
+ | Binding to entity | Writable `computed` — getter reads nested array, setter writes it back |
208
+ | Upload path | Callback `() => string` — resolved at upload time, so entity ID is available |
209
+ | Remove confirmation | Pass `confirmRemove` returning `Promise<boolean>` via `usePopup().showConfirmation` |
210
+ | Passing to AssetsManager blade | Wrap with `markRaw()` — prevents Vue from making the manager deeply reactive |
211
+ | Multiple asset types | Create separate `useAssetsManager` instances per array (images vs files) |
212
+ | Gallery edit | Open `AssetsDetails` blade, pass `assetEditHandler` and `assetRemoveHandler` callbacks |
213
+ | Widget visibility | Use `isVisible: computed(() => !!param.value)` to hide on "create new" mode |
@@ -0,0 +1,277 @@
1
+ # Child Blade Flow Pattern
2
+
3
+ Parent-child blade communication and nested blade workflows. Covers opening child blades, cross-blade method calls, close guards, and the picker blade pattern.
4
+
5
+ ---
6
+
7
+ ## Opening a Child Blade with `param` and `options`
8
+
9
+ ```ts
10
+ import { useBlade } from "@vc-shell/framework";
11
+
12
+ const { openBlade } = useBlade();
13
+
14
+ openBlade({
15
+ name: "ProductDetails",
16
+ param: product.id, // string — entity ID, appears in URL
17
+ options: { mode: "edit", origin: "catalog" }, // object — runtime-only, not in URL
18
+ onOpen() { selectedItemId.value = product.id; },
19
+ onClose() { selectedItemId.value = undefined; },
20
+ });
21
+ ```
22
+
23
+ - `param` is always a **string** (entity ID). When `undefined`, the child blade is in "create new" mode.
24
+ - `options` carries structured data that does not belong in the URL. Type it via the generic: `useBlade<{ mode: string }>()`.
25
+ - `onOpen` / `onClose` callbacks run when the child blade opens or closes — use them to sync selection state in the parent.
26
+
27
+ ---
28
+
29
+ ## `exposeToChildren()` — Parent Exposes Methods
30
+
31
+ The parent blade declares which methods child blades may invoke. Call once at the bottom of `<script setup>`.
32
+
33
+ ```ts
34
+ const { exposeToChildren } = useBlade();
35
+
36
+ async function reload() {
37
+ await searchProducts(searchQuery.value);
38
+ }
39
+
40
+ function markProductDirty() {
41
+ isModified.value = true;
42
+ }
43
+
44
+ // Expose at end of setup — method names must match callParent("methodName")
45
+ exposeToChildren({ reload, markProductDirty, closeChildren });
46
+ ```
47
+
48
+ - Pass a plain object of functions.
49
+ - Calling `exposeToChildren` multiple times **overwrites** the previous context — call it once.
50
+ - Method names must match exactly what children pass to `callParent()`.
51
+
52
+ ---
53
+
54
+ ## `callParent()` — Child Calls Parent Methods
55
+
56
+ ```ts
57
+ const { callParent, closeSelf } = useBlade();
58
+
59
+ async function onSave() {
60
+ await updateProduct(product.value);
61
+
62
+ // Tell parent list to refresh its data
63
+ await callParent("reload");
64
+
65
+ // Close the child blade
66
+ await closeSelf();
67
+ }
68
+ ```
69
+
70
+ - `callParent("methodName", args?)` invokes the function the parent exposed via `exposeToChildren`.
71
+ - If the parent did not expose that method, the call is a **silent no-op** — no error thrown.
72
+ - `callParent` is generic for typed return values: `const count = await callParent<number>("getCount");`
73
+
74
+ ---
75
+
76
+ ## `onBeforeClose` Guard — Confirmation Before Closing
77
+
78
+ Prevents accidental data loss when the user closes a blade with unsaved changes.
79
+
80
+ ```ts
81
+ import { useBlade, usePopup } from "@vc-shell/framework";
82
+
83
+ const { onBeforeClose } = useBlade();
84
+ const { showConfirmation } = usePopup();
85
+
86
+ onBeforeClose(async () => {
87
+ if (!disabled.value && isModified.value) {
88
+ // showConfirmation returns true if user confirms, false if cancelled
89
+ return !(await showConfirmation(t("MODULE.PAGES.ALERTS.CLOSE_CONFIRMATION")));
90
+ }
91
+ return false; // no unsaved changes — allow close
92
+ });
93
+ ```
94
+
95
+ Return value semantics:
96
+ - `false` — close is **allowed**
97
+ - `true` — close is **blocked** (user stays on the blade)
98
+
99
+ The `!(await showConfirmation(...))` idiom:
100
+ - User clicks "Confirm" (discard) -> `true` -> `!true` = `false` -> close allowed
101
+ - User clicks "Cancel" (stay) -> `false` -> `!false` = `true` -> close blocked
102
+
103
+ ---
104
+
105
+ ## Picker Blade Pattern
106
+
107
+ A child blade opened to select an item and return the result to the parent via `callParent`.
108
+
109
+ **Parent — opens picker and exposes a handler:**
110
+
111
+ ```ts
112
+ const { openBlade, exposeToChildren } = useBlade();
113
+
114
+ function onPickerResult(selectedItem: Category) {
115
+ product.value.categoryId = selectedItem.id;
116
+ product.value.categoryName = selectedItem.name;
117
+ isModified.value = true;
118
+ }
119
+
120
+ exposeToChildren({ onPickerResult });
121
+
122
+ function openCategoryPicker() {
123
+ openBlade({
124
+ name: "CategoryPicker",
125
+ options: { currentCategoryId: product.value.categoryId },
126
+ });
127
+ }
128
+ ```
129
+
130
+ **Child (picker) — selects item and returns to parent:**
131
+
132
+ ```ts
133
+ const { callParent, closeSelf, options } = useBlade<{ currentCategoryId?: string }>();
134
+
135
+ async function onSelect(category: Category) {
136
+ await callParent("onPickerResult", category);
137
+ await closeSelf();
138
+ }
139
+ ```
140
+
141
+ The picker blade calls the parent's exposed method with the selection, then closes itself.
142
+
143
+ ---
144
+
145
+ ## Data Refresh Flow: Child Saves -> Parent Reloads
146
+
147
+ The standard lifecycle when a child blade modifies data and the parent list needs to update:
148
+
149
+ ```
150
+ Parent (List blade)
151
+ exposeToChildren({ reload })
152
+ openBlade({ name: "Details", param: item.id, onClose: clearSelection })
153
+ |
154
+ v
155
+ Child (Details blade)
156
+ 1. User edits data
157
+ 2. onBeforeClose guard checks isModified.value
158
+ 3. Save button handler:
159
+ await saveEntity(entity.value)
160
+ await callParent("reload") <-- parent refetches list data
161
+ await closeSelf() <-- triggers onClose callback
162
+ |
163
+ v
164
+ Parent receives:
165
+ - reload() executes -> list data refreshed
166
+ - onClose() fires -> selectedItemId cleared
167
+ ```
168
+
169
+ ### Complete Example
170
+
171
+ **Parent list blade:**
172
+
173
+ ```vue
174
+ <script setup lang="ts">
175
+ import { useBlade } from "@vc-shell/framework";
176
+
177
+ defineOptions({ name: "ProductsList", url: "/products", isWorkspace: true });
178
+
179
+ const { openBlade, exposeToChildren } = useBlade();
180
+ const selectedItemId = ref<string>();
181
+
182
+ async function reload() {
183
+ await searchProducts(searchQuery.value);
184
+ }
185
+
186
+ function onItemClick(event: { data: { id?: string } }) {
187
+ openBlade({
188
+ name: "ProductDetails",
189
+ param: event.data.id,
190
+ onOpen() { selectedItemId.value = event.data.id; },
191
+ onClose() { selectedItemId.value = undefined; },
192
+ });
193
+ }
194
+
195
+ exposeToChildren({ reload });
196
+ </script>
197
+ ```
198
+
199
+ **Child details blade:**
200
+
201
+ ```vue
202
+ <script setup lang="ts">
203
+ import { useBlade, usePopup, defineBladeContext } from "@vc-shell/framework";
204
+
205
+ defineOptions({ name: "ProductDetails", url: "/product-details" });
206
+
207
+ const { param, callParent, closeSelf, exposeToChildren, onBeforeClose } = useBlade();
208
+ const { showConfirmation } = usePopup();
209
+
210
+ const item = ref<Product>();
211
+ const isModified = ref(false);
212
+
213
+ defineBladeContext({ item });
214
+
215
+ onMounted(async () => {
216
+ if (param.value) {
217
+ await loadProduct({ id: param.value });
218
+ }
219
+ });
220
+
221
+ async function onSave() {
222
+ await saveProduct(item.value);
223
+ await callParent("reload");
224
+ await closeSelf();
225
+ }
226
+
227
+ function reload() {
228
+ if (param.value) loadProduct({ id: param.value });
229
+ }
230
+
231
+ onBeforeClose(async () => {
232
+ if (isModified.value) {
233
+ return !(await showConfirmation("Discard unsaved changes?"));
234
+ }
235
+ return false;
236
+ });
237
+
238
+ // This blade is also a parent — expose reload to its own children
239
+ exposeToChildren({ reload });
240
+ </script>
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Multi-Level Chain: List -> Details -> Sub-Details
246
+
247
+ A "middle" blade acts as both child (calls its parent) and parent (exposes methods to its own children):
248
+
249
+ ```ts
250
+ const { openBlade, callParent, exposeToChildren, closeSelf } = useBlade();
251
+
252
+ // As a parent — expose reload for child blades
253
+ async function reload() { await loadOrder(param.value!); }
254
+ exposeToChildren({ reload });
255
+
256
+ // As a child — open sub-details blade
257
+ function openShipment(id: string) {
258
+ openBlade({ name: "ShipmentDetails", param: id });
259
+ }
260
+
261
+ // As a child — propagate delete up to grandparent
262
+ async function onDelete() {
263
+ await deleteOrder(param.value!);
264
+ await callParent("reload"); // grandparent list refreshes
265
+ await closeSelf();
266
+ }
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Key Rules
272
+
273
+ 1. **`exposeToChildren` must be called once** at the end of setup — calling it again overwrites the exposed context.
274
+ 2. **`callParent` is always safe** — no-ops silently if the parent did not expose the method.
275
+ 3. **`onBeforeClose` return value is inverted** — `true` blocks close, `false` allows it.
276
+ 4. **Always `await callParent("reload")` before `closeSelf()`** — ensures parent data is refreshed before the child blade is destroyed.
277
+ 5. **Use `defineBladeContext` for widget data sharing**, `exposeToChildren` / `callParent` for cross-blade communication — they serve different purposes.