@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.
- package/CHANGELOG.md +8 -0
- package/bin/knowledge-stats.cjs +135 -0
- package/bin/sync-docs.cjs +62 -0
- package/package.json +5 -1
- package/runtime/VERSION +1 -1
- package/runtime/agents/details-blade-generator.md +75 -14
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/api/platform.docs.md +14 -7
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useApiClient/useApiClient.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useAssetsManager/useAssetsManager.docs.md +149 -0
- package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/i18n/i18n.docs.md +74 -0
- package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +6 -0
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +29 -33
- package/runtime/knowledge/docs/shell/components/logout-button/logout-button.docs.md +3 -3
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-table/vc-data-table.docs.md +12 -0
- package/runtime/knowledge/index.md +60 -0
- package/runtime/knowledge/patterns/assets-management.md +213 -0
- package/runtime/knowledge/patterns/child-blade-flow.md +277 -0
- package/runtime/knowledge/patterns/details-blade-pattern.md +350 -3
- package/runtime/knowledge/patterns/extension-points-usage.md +308 -0
- package/runtime/knowledge/patterns/form-validation.md +377 -0
- package/runtime/knowledge/patterns/multilanguage-fields.md +239 -0
- package/runtime/knowledge/patterns/signalr-notifications.md +237 -0
- package/runtime/vc-app.md +44 -5
- 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.
|