@vc-shell/vc-app-skill 2.0.0-alpha.23 → 2.0.0-alpha.25
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 +12 -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/useBladeWidgets.docs.md +164 -9
- 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/composables/useWidgets/useWidgets.docs.md +2 -2
- 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/injection-keys.docs.md +1 -1
- 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/core/composables/useWidget/useWidget.docs.md +0 -159
- package/runtime/knowledge/docs/shell/components/app-switcher/app-switcher.docs.md +0 -104
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
A bordered container with an optional header, icon, action buttons, and collapsible body. VcCard groups related content into visually distinct sections on blades and detail views. It supports three color variants for semantic meaning and smooth animated collapse/expand.
|
|
4
4
|
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
| Scenario | Component |
|
|
8
|
+
|----------|-----------|
|
|
9
|
+
| Grouping form fields or content with a header | **VcCard** |
|
|
10
|
+
| Scrollable content wrapper without a header | [VcContainer](../vc-container/) |
|
|
11
|
+
| Collapsible section with rich toggle behavior | [VcAccordion](../../molecules/vc-accordion/) (for multiple exclusive panels) |
|
|
12
|
+
| Alert or notification banner | [VcBanner](../vc-banner/) |
|
|
13
|
+
|
|
14
|
+
Use VcCard to visually separate content sections on a blade -- especially when you need a titled header, action buttons, or collapsible body. **Do not use** VcCard for dismissible alerts or status messages (use `VcBanner`), and avoid nesting VcCard when a simple `VcContainer` with padding would suffice.
|
|
15
|
+
|
|
5
16
|
## Quick Start
|
|
6
17
|
|
|
7
18
|
```vue
|
|
@@ -25,6 +25,17 @@ Traditional admin panels use page-based routing: click a link, the entire viewpo
|
|
|
25
25
|
| Layout | Full viewport | Side-by-side panels |
|
|
26
26
|
| Depth | Flat (1 level) | Unlimited nesting |
|
|
27
27
|
|
|
28
|
+
## When to Use
|
|
29
|
+
|
|
30
|
+
| Scenario | Component |
|
|
31
|
+
|----------|-----------|
|
|
32
|
+
| Stacked panel with toolbar, header, and lifecycle | **VcBlade** |
|
|
33
|
+
| One-off confirmation or input dialog | [VcPopup](../../molecules/vc-popup/) |
|
|
34
|
+
| Full-page route without blade stack | Vue Router view |
|
|
35
|
+
| Scrollable content section inside a blade | [VcContainer](../../molecules/vc-container/) |
|
|
36
|
+
|
|
37
|
+
Use VcBlade for every screen in a vc-shell application -- it is the standard container that integrates with the navigation system, toolbar, breadcrumbs, and unsaved-changes guards. **Do not use** VcBlade for transient dialogs (use `VcPopup` / `usePopup()`) or for content areas that do not need their own header and close button.
|
|
38
|
+
|
|
28
39
|
## Quick Start
|
|
29
40
|
|
|
30
41
|
```vue
|
|
@@ -18,6 +18,17 @@ Columns are defined as `<VcColumn>` child components -- no configuration objects
|
|
|
18
18
|
- State persistence (column widths, order, sort, filters) to localStorage/sessionStorage
|
|
19
19
|
- Full TypeScript generics -- `VcDataTable<Product>` propagates types to events and slots
|
|
20
20
|
|
|
21
|
+
## When to Use
|
|
22
|
+
|
|
23
|
+
| Scenario | Component |
|
|
24
|
+
|----------|-----------|
|
|
25
|
+
| Tabular data with sorting, selection, pagination | **VcDataTable** |
|
|
26
|
+
| Simple short list without table features | `v-for` with custom markup |
|
|
27
|
+
| Image/card grid layout | [VcGallery](../vc-gallery/) |
|
|
28
|
+
| Key-value detail display | [VcField](../../molecules/vc-field/) or [VcCard](../../atoms/vc-card/) |
|
|
29
|
+
|
|
30
|
+
Use VcDataTable whenever you need structured rows and columns with any combination of sorting, filtering, inline editing, or column management. **Do not use** VcDataTable for simple lists of 5-10 items that need no table features -- a plain `v-for` loop is lighter. For thumbnail/card grids, prefer VcGallery.
|
|
31
|
+
|
|
21
32
|
---
|
|
22
33
|
|
|
23
34
|
## Table of Contents
|
|
@@ -1329,6 +1340,7 @@ function onRowRemove(event: { data: Product; index: number; cancel: () => void }
|
|
|
1329
1340
|
|
|
1330
1341
|
| Event | Payload | Description |
|
|
1331
1342
|
|-------|---------|-------------|
|
|
1343
|
+
| `update:editingRows` | `T[]` | v-model update for currently editing rows. |
|
|
1332
1344
|
| `cell-edit-init` | `{ data: T, field: string, index: number }` | Cell entered edit mode. |
|
|
1333
1345
|
| `cell-edit-complete` | `{ data: T, field: string, newValue: unknown, index: number }` | Cell edit committed. |
|
|
1334
1346
|
| `cell-edit-cancel` | `{ data: T, field: string, index: number }` | Cell edit cancelled. |
|
|
@@ -29,9 +29,23 @@ Notable location: `useDataTableSort.docs.md` is under `ui/composables/`, not `co
|
|
|
29
29
|
- `knowledge/patterns/blade-navigation.md`
|
|
30
30
|
|
|
31
31
|
### Framework docs to read (basename glob):
|
|
32
|
+
|
|
33
|
+
**Core table components (always read):**
|
|
32
34
|
- `**/vc-blade.docs.md`
|
|
33
35
|
- `**/vc-data-table.docs.md`
|
|
34
36
|
- `**/vc-pagination.docs.md`
|
|
37
|
+
|
|
38
|
+
**Cell renderer components (read for custom #body slots in VcColumn):**
|
|
39
|
+
- `**/vc-status.docs.md` — for status/state columns (variant-colored badge)
|
|
40
|
+
- `**/vc-status-icon.docs.md` — for boolean columns (check/cross icon)
|
|
41
|
+
- `**/vc-badge.docs.md` — for count/tag columns (numeric badge, category tag)
|
|
42
|
+
- `**/vc-image.docs.md` — for image/avatar/thumbnail columns
|
|
43
|
+
- `**/vc-link.docs.md` — for clickable URL/email columns
|
|
44
|
+
- `**/vc-progress.docs.md` — for percentage/progress columns
|
|
45
|
+
- `**/vc-rating.docs.md` — for rating columns (read-only stars)
|
|
46
|
+
- `**/vc-tooltip.docs.md` — for columns with truncated text that need hover detail
|
|
47
|
+
|
|
48
|
+
**Composable docs (always read):**
|
|
35
49
|
- `**/useBlade.docs.md`
|
|
36
50
|
- `**/useAsync.docs.md`
|
|
37
51
|
- `**/useLoading.docs.md`
|
|
@@ -50,17 +64,63 @@ Notable location: `useDataTableSort.docs.md` is under `ui/composables/`, not `co
|
|
|
50
64
|
- `knowledge/patterns/blade-navigation.md`
|
|
51
65
|
|
|
52
66
|
### Framework docs to read (basename glob):
|
|
67
|
+
|
|
68
|
+
**Core form components (always read):**
|
|
53
69
|
- `**/vc-blade.docs.md`
|
|
54
70
|
- `**/vc-form.docs.md`
|
|
55
71
|
- `**/vc-input.docs.md`
|
|
56
72
|
- `**/vc-select.docs.md`
|
|
57
73
|
- `**/vc-switch.docs.md`
|
|
74
|
+
|
|
75
|
+
**Extended form components (read based on field types — see Field Type → Component Mapping in details-blade-pattern.md):**
|
|
76
|
+
- `**/vc-textarea.docs.md` — for `text` / `rich-text` fields
|
|
77
|
+
- `**/vc-editor.docs.md` — for `rich-text` fields (WYSIWYG HTML editor)
|
|
78
|
+
- `**/vc-date-picker.docs.md` — for `date-time` fields (calendar widget, preferred over VcInput type="datetime-local")
|
|
79
|
+
- `**/vc-checkbox.docs.md` — for `boolean` fields (alternative to VcSwitch for "agree/accept" semantics)
|
|
80
|
+
- `**/vc-checkbox-group.docs.md` — for `multi-select` fields with few static options
|
|
81
|
+
- `**/vc-radio-group.docs.md` — for `enum` fields with 2-5 options
|
|
82
|
+
- `**/vc-input-currency.docs.md` — for `currency` fields (formatted monetary input)
|
|
83
|
+
- `**/vc-multivalue.docs.md` — for `multi-select` / `tags` fields (tag-style multi-picker)
|
|
84
|
+
- `**/vc-rating.docs.md` — for `rating` fields (star rating)
|
|
85
|
+
- `**/vc-slider.docs.md` — for `range` fields (numeric slider)
|
|
86
|
+
- `**/vc-color-input.docs.md` — for `color` fields
|
|
87
|
+
- `**/vc-file-upload.docs.md` — for `file` fields (file attachment upload)
|
|
88
|
+
- `**/vc-input-group.docs.md` — for grouped inputs (prefix/suffix patterns like URL, phone)
|
|
89
|
+
|
|
90
|
+
**Read-only display components (read when blade has view-only fields):**
|
|
91
|
+
- `**/vc-field.docs.md` — for read-only key-value display (horizontal label:value, copyable, tooltip). Use instead of VcInput for non-editable data.
|
|
92
|
+
|
|
93
|
+
**Layout & display components (read when the form has sections or read-only data):**
|
|
94
|
+
- `**/vc-card.docs.md` — for grouping form sections visually (ALWAYS use for 3+ field groups)
|
|
95
|
+
- `**/vc-accordion.docs.md` — for collapsible form sections
|
|
96
|
+
- `**/vc-badge.docs.md` — for status indicators in form headers
|
|
97
|
+
- `**/vc-banner.docs.md` — for contextual alerts at top of form (error, info, warning states)
|
|
98
|
+
- `**/vc-hint.docs.md` — for field-level help text or secondary descriptions
|
|
99
|
+
- `**/vc-status.docs.md` — for status display in read-only areas
|
|
100
|
+
- `**/vc-button.docs.md` — for inline action buttons (e.g., inside banners)
|
|
101
|
+
|
|
102
|
+
**Media components (read when entity has images/gallery/video):**
|
|
103
|
+
- `**/vc-image-upload.docs.md` — for `image` fields (single image upload with preview)
|
|
104
|
+
- `**/vc-gallery.docs.md` — for `gallery` / `images` fields (multi-image management)
|
|
105
|
+
- `**/vc-image.docs.md` — for read-only image display
|
|
106
|
+
|
|
107
|
+
**Composable docs (always read):**
|
|
58
108
|
- `**/useBlade.docs.md`
|
|
59
109
|
- `**/useAsync.docs.md`
|
|
60
110
|
- `**/useModificationTracker.docs.md`
|
|
61
111
|
- `**/useLoading.docs.md`
|
|
62
112
|
- `**/validation.docs.md`
|
|
63
113
|
|
|
114
|
+
**Composable docs (read when entity has related sub-entities):**
|
|
115
|
+
- `**/useBladeWidgets.docs.md` — for blade sidebar widgets (icon buttons with badge counts that open child blades)
|
|
116
|
+
|
|
117
|
+
### Advanced patterns to read (based on design intent):
|
|
118
|
+
|
|
119
|
+
Read `knowledge/patterns/` files when the design requires these patterns:
|
|
120
|
+
- `knowledge/patterns/blade-widget.md` — when entity has related sub-entities that need sidebar widget navigation
|
|
121
|
+
- `knowledge/patterns/dashboard-widget.md` — when module should contribute a dashboard card
|
|
122
|
+
- `knowledge/patterns/notification-template.md` — when module needs push notification rendering
|
|
123
|
+
|
|
64
124
|
---
|
|
65
125
|
|
|
66
126
|
## Module Assembler
|
|
@@ -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.
|