@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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# SignalR Notifications Pattern
|
|
2
|
+
|
|
3
|
+
Real-time push notifications from the VirtoCommerce platform to vc-shell modules via SignalR WebSocket transport.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Notification Flow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Platform backend
|
|
11
|
+
| (domain event fires)
|
|
12
|
+
v
|
|
13
|
+
ASP.NET SignalR hub (/pushNotificationHub)
|
|
14
|
+
|
|
|
15
|
+
+-- "Send" channel (broadcast to all clients)
|
|
16
|
+
+-- "SendSystemEvents" (filtered by creator ID)
|
|
17
|
+
|
|
|
18
|
+
v
|
|
19
|
+
SignalR plugin (framework auto-installed)
|
|
20
|
+
|
|
|
21
|
+
v
|
|
22
|
+
NotificationStore.ingest(message)
|
|
23
|
+
|
|
|
24
|
+
+---> history[] (persistent, loaded from API)
|
|
25
|
+
+---> realtime[] (session-only, from SignalR)
|
|
26
|
+
+---> ToastController (Level 1: module-level toasts)
|
|
27
|
+
+---> subscriber callbacks (Level 2: blade-scoped handlers)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The SignalR plugin connects on login, disconnects on logout, and auto-reconnects on connection loss. Module developers never interact with SignalR directly.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 1: Create a Notification Template
|
|
35
|
+
|
|
36
|
+
Each notification type can have a custom Vue SFC that controls how it renders in the dropdown and toasts. The component receives its data via `useNotificationContext()`, not props.
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<!-- notifications/OrderShippedEvent.vue -->
|
|
40
|
+
<template>
|
|
41
|
+
<NotificationTemplate
|
|
42
|
+
:color="color"
|
|
43
|
+
:title="notification.title ?? ''"
|
|
44
|
+
:icon="'lucide-truck'"
|
|
45
|
+
:notification="notification"
|
|
46
|
+
@click="onClick"
|
|
47
|
+
>
|
|
48
|
+
<VcHint v-if="notification.description" class="tw-mb-1">
|
|
49
|
+
{{ notification.description }}
|
|
50
|
+
</VcHint>
|
|
51
|
+
</NotificationTemplate>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script lang="ts" setup>
|
|
55
|
+
import {
|
|
56
|
+
NotificationTemplate,
|
|
57
|
+
useNotificationContext,
|
|
58
|
+
useBlade,
|
|
59
|
+
} from "@vc-shell/framework";
|
|
60
|
+
import type { PushNotification } from "@vc-shell/framework";
|
|
61
|
+
import { computed } from "vue";
|
|
62
|
+
|
|
63
|
+
interface IOrderShippedNotification extends PushNotification {
|
|
64
|
+
orderId?: string;
|
|
65
|
+
trackingNumber?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const notificationRef = useNotificationContext<IOrderShippedNotification>();
|
|
69
|
+
const notification = computed(() => notificationRef.value);
|
|
70
|
+
|
|
71
|
+
const color = computed(() =>
|
|
72
|
+
notification.value.trackingNumber ? "var(--success-400)" : "var(--primary-500)"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { openBlade } = useBlade();
|
|
76
|
+
|
|
77
|
+
async function onClick() {
|
|
78
|
+
await openBlade({ name: "OrderDetails", param: notification.value.orderId });
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `NotificationTemplate` props
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Description |
|
|
86
|
+
|----------------|--------------------|------------------------------------------------|
|
|
87
|
+
| `title` | `string` | Notification title text |
|
|
88
|
+
| `notification` | `PushNotification` | Full notification object (used for timestamp) |
|
|
89
|
+
| `icon` | `string` | Lucide icon name, e.g. `"lucide-truck"` |
|
|
90
|
+
| `color` | `string` | CSS color/variable for the icon circle |
|
|
91
|
+
|
|
92
|
+
The default slot renders below the title/timestamp. Use it for description text, progress bars, or action links.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Step 2: Register in the Module
|
|
97
|
+
|
|
98
|
+
Pass a `notifications` record to `defineAppModule`. Each key must exactly match the `notifyType` string sent by the platform backend.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// modules/orders/index.ts
|
|
102
|
+
import * as pages from "./pages";
|
|
103
|
+
import * as locales from "./locales";
|
|
104
|
+
import { defineAppModule } from "@vc-shell/framework";
|
|
105
|
+
import OrderShippedEvent from "./notifications/OrderShippedEvent.vue";
|
|
106
|
+
|
|
107
|
+
export default defineAppModule({
|
|
108
|
+
blades: pages,
|
|
109
|
+
locales,
|
|
110
|
+
notifications: {
|
|
111
|
+
OrderShippedEvent: {
|
|
112
|
+
template: OrderShippedEvent,
|
|
113
|
+
toast: { mode: "auto" },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Toast configuration
|
|
120
|
+
|
|
121
|
+
The `toast` object controls how the notification appears as a popup.
|
|
122
|
+
|
|
123
|
+
| Property | Type | Description |
|
|
124
|
+
|-----------------|---------------------------------------------|------------------------------------------------------|
|
|
125
|
+
| `mode` | `"auto" \| "progress" \| "silent"` | `auto` = fire-and-forget; `progress` = persistent until complete; `silent` = no toast |
|
|
126
|
+
| `severity` | `Severity \| (msg) => Severity` | Static or dynamic: `"info"`, `"warning"`, `"error"`, `"critical"` |
|
|
127
|
+
| `timeout` | `number` | Override default timeout (ms). Defaults: info=5s, warning=8s, error/critical=persistent |
|
|
128
|
+
| `isComplete` | `(msg) => boolean` | For `"progress"` mode: returns true when the operation finishes |
|
|
129
|
+
| `completedType` | `(msg) => string` | For `"progress"` mode: final toast variant (`"success"` or `"error"`) |
|
|
130
|
+
|
|
131
|
+
#### Toast mode examples
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// Fire-and-forget toast with default severity (info, 5s timeout)
|
|
135
|
+
toast: { mode: "auto" }
|
|
136
|
+
|
|
137
|
+
// Warning toast with custom timeout
|
|
138
|
+
toast: { mode: "auto", severity: "warning", timeout: 8000 }
|
|
139
|
+
|
|
140
|
+
// Long-running operation with progress tracking
|
|
141
|
+
toast: {
|
|
142
|
+
mode: "progress",
|
|
143
|
+
severity: (msg) => msg.finished ? "info" : "warning",
|
|
144
|
+
isComplete: (msg) => !!msg.finished,
|
|
145
|
+
completedType: (msg) => msg.errorCount ? "error" : "success",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// No toast -- notification only appears in the dropdown
|
|
149
|
+
toast: { mode: "silent" }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `groupBy` option
|
|
153
|
+
|
|
154
|
+
For notification types that emit multiple messages for the same logical operation (e.g. export progress updates), set `groupBy` to the field name that identifies the operation. The store upserts instead of appending:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
notifications: {
|
|
158
|
+
CatalogExportProgress: {
|
|
159
|
+
template: ExportProgressTemplate,
|
|
160
|
+
toast: { mode: "progress", isComplete: (msg) => !!msg.finished },
|
|
161
|
+
groupBy: "jobId",
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Step 3: React to Notifications in a Blade
|
|
169
|
+
|
|
170
|
+
Use `useBladeNotifications()` inside `<script setup>` to subscribe to specific notification types. Subscriptions auto-cleanup when the blade unmounts.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { useBladeNotifications } from "@vc-shell/framework";
|
|
174
|
+
|
|
175
|
+
const { messages, unreadCount, markAsRead } = useBladeNotifications({
|
|
176
|
+
types: ["OrderShippedEvent"],
|
|
177
|
+
filter: (msg) => msg.orderId === currentOrderId.value,
|
|
178
|
+
onMessage: (msg) => {
|
|
179
|
+
// Refresh blade data when a relevant notification arrives
|
|
180
|
+
reloadOrder();
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `useBladeNotifications` API
|
|
186
|
+
|
|
187
|
+
**Parameters:**
|
|
188
|
+
|
|
189
|
+
| Field | Type | Description |
|
|
190
|
+
|------------|-----------------------------|--------------------------------------------------|
|
|
191
|
+
| `types` | `string[]` | Notification type(s) to subscribe to |
|
|
192
|
+
| `filter` | `(msg) => boolean` | Optional: further filter by message fields |
|
|
193
|
+
| `onMessage`| `(msg) => void` | Callback fired for each matching notification |
|
|
194
|
+
|
|
195
|
+
**Returns:**
|
|
196
|
+
|
|
197
|
+
| Field | Type | Description |
|
|
198
|
+
|--------------|---------------------------|------------------------------------------------|
|
|
199
|
+
| `messages` | `ComputedRef<T[]>` | Matching unread messages from the realtime queue |
|
|
200
|
+
| `unreadCount`| `ComputedRef<number>` | Count of matching unread messages |
|
|
201
|
+
| `markAsRead` | `(msg) => void` | Mark a specific message as read |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Directory Structure
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
src/modules/orders/
|
|
209
|
+
├── index.ts # defineAppModule with notifications
|
|
210
|
+
├── notifications/
|
|
211
|
+
│ ├── index.ts # barrel export
|
|
212
|
+
│ └── OrderShippedEvent.vue # template component
|
|
213
|
+
└── pages/
|
|
214
|
+
└── ...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`notifications/index.ts` barrel:
|
|
218
|
+
```ts
|
|
219
|
+
export { default as OrderShippedEvent } from "./OrderShippedEvent.vue";
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Key Rules
|
|
225
|
+
|
|
226
|
+
1. **Notification keys are case-sensitive** -- the key in `notifications: { ... }` must exactly match the `notifyType` string from the platform backend.
|
|
227
|
+
2. **Use `useNotificationContext()`** inside template components, not props. The framework injects the notification via provide/inject.
|
|
228
|
+
3. **Use `useBladeNotifications()`** for blade-level subscriptions (preferred). The deprecated `useNotifications()` still works but logs a warning.
|
|
229
|
+
4. **Do not interact with SignalR directly.** The plugin is auto-installed by the framework. Use the notification store and composables instead.
|
|
230
|
+
5. **`Send` vs `SendSystemEvents`** -- broadcast notifications use the `Send` channel; user-scoped notifications use `SendSystemEvents` filtered by the `creator` field on the server side.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Related Patterns
|
|
235
|
+
|
|
236
|
+
- [notification-template](./notification-template.md) -- detailed template component authoring guide
|
|
237
|
+
- [module-structure](./module-structure.md) -- overall module layout including notifications directory
|
package/runtime/vc-app.md
CHANGED
|
@@ -951,7 +951,29 @@ Parse `DESIGN_PROMPT` into a structured application plan. Apply these parsing ru
|
|
|
951
951
|
|
|
952
952
|
**Field extraction:**
|
|
953
953
|
- Concrete field mentions ("subscription token key", "trial period", "email") → columns or formFields with inferred types
|
|
954
|
-
- Type inference
|
|
954
|
+
- Type inference — use the most specific type possible:
|
|
955
|
+
|
|
956
|
+
| Signal in prompt | Field type | Component (details) | Column type (list) |
|
|
957
|
+
|---|---|---|---|
|
|
958
|
+
| date, deadline, birthday, created, expires | `date-time` | `VcDatePicker` | `date-ago` |
|
|
959
|
+
| is*, has*, can*, enabled, active, published | `boolean` | `VcSwitch` | `status-icon` |
|
|
960
|
+
| price, cost, amount, total, salary, budget, fee | `currency` | `VcInputCurrency` | `money` |
|
|
961
|
+
| count, quantity, age, priority (numeric) | `number` | `VcInput type="number"` | `number` |
|
|
962
|
+
| description, notes, comment, bio, summary | `text` | `VcTextarea` | — (not in list) |
|
|
963
|
+
| body, content, html, article, template | `rich-text` | `VcEditor` | — (not in list) |
|
|
964
|
+
| status, state, type, category (from fixed set) | `enum` | `VcSelect` or `VcRadioGroup` | `status` |
|
|
965
|
+
| tags, labels, categories, roles, permissions | `multi-select` | `VcMultivalue` | — (not in list) |
|
|
966
|
+
| avatar, logo, photo, thumbnail, banner | `image` | `VcImageUpload` | `image` |
|
|
967
|
+
| photos, images, screenshots, gallery | `gallery` | `VcGallery` | — (not in list) |
|
|
968
|
+
| file, attachment, document, contract | `file` | `VcFileUpload` | — (not in list) |
|
|
969
|
+
| rating, score, stars | `rating` | `VcRating` | — (custom slot) |
|
|
970
|
+
| color, colour, brandColor | `color` | `VcColorInput` | — (custom slot) |
|
|
971
|
+
| discount, opacity, percentage, progress | `range` | `VcSlider` | — (custom slot) |
|
|
972
|
+
| email | `string` | `VcInput` (rules="email") | plain text |
|
|
973
|
+
| phone, tel | `string` | `VcInput` (type="tel") | plain text |
|
|
974
|
+
| url, website, link | `string` | `VcInput` (type="url") | plain text |
|
|
975
|
+
| everything else | `string` | `VcInput` | plain text |
|
|
976
|
+
|
|
955
977
|
- If a field clearly belongs to a list view (searchable, sortable characteristic) → column
|
|
956
978
|
- If a field clearly belongs to a form (editable, configurable) → formField
|
|
957
979
|
- If unclear → put in both columns and formFields
|
|
@@ -976,9 +998,13 @@ DESIGN_PLAN = {
|
|
|
976
998
|
"name": "string — kebab-case module name (english)",
|
|
977
999
|
"description": "string — what this module does",
|
|
978
1000
|
"bladeTypes": "list+details | list-only | details-only",
|
|
1001
|
+
"readOnly": "boolean? — true if details blade is view-only (use VcField instead of VcInput). Default: false",
|
|
979
1002
|
"columns": [{ "name": "string", "type": "string", "sortable": true/false }],
|
|
980
|
-
"formFields": [{ "name": "string", "type": "string", "required": true/false }],
|
|
1003
|
+
"formFields": [{ "name": "string", "type": "string (use specific types from type inference table above)", "required": true/false, "component": "string? — VcInput|VcTextarea|VcEditor|VcDatePicker|VcSelect|VcSwitch|VcCheckbox|VcRadioGroup|VcInputCurrency|VcMultivalue|VcRating|VcSlider|VcColorInput|VcImageUpload|VcGallery|VcFileUpload|VcField", "readOnly": "boolean? — true for display-only fields within editable form" }],
|
|
981
1004
|
"toolbarActions": [{ "label": "string", "action": "string (camelCase)" }],
|
|
1005
|
+
"widgets": "string[]? — names of related sub-entities for blade sidebar widgets (e.g., ['offers', 'videos'])",
|
|
1006
|
+
"dashboard": "boolean? — true to generate a DashboardWidgetCard for this module",
|
|
1007
|
+
"notifications": "string[]? — domain event names to generate notification templates for (e.g., ['OrderCreated'])",
|
|
982
1008
|
"todos": ["string — exact quote from prompt"]
|
|
983
1009
|
}
|
|
984
1010
|
],
|
|
@@ -998,8 +1024,18 @@ DESIGN_PLAN = {
|
|
|
998
1024
|
- Entity with both list-worthy columns AND editable fields → `"list+details"`
|
|
999
1025
|
- Entity that is mainly a collection/catalog with no edit form → `"list-only"`
|
|
1000
1026
|
- Entity that is a singleton/settings with no list → `"details-only"`
|
|
1027
|
+
- Entity that is view-only (order details, transaction log, audit) → `"list+details"` but mark the details blade `readOnly: true`
|
|
1001
1028
|
- Default to `"list+details"` when unclear
|
|
1002
1029
|
|
|
1030
|
+
**Details blade mode inference:**
|
|
1031
|
+
- If the entity is view-only (order, transaction, audit log, payment) → set `readOnly: true` on the module — the details generator will use `VcField` for display instead of `VcInput`
|
|
1032
|
+
- If the entity has mixed editable + read-only fields → default `readOnly: false`, individual fields marked as `readOnly` in `formFields`
|
|
1033
|
+
|
|
1034
|
+
**Feature inference (enrich modules based on prompt signals):**
|
|
1035
|
+
- If entity mentions "related" sub-entities (e.g., "product has offers and videos") → add `widgets: ["sub-entity-name"]` — generates blade sidebar widgets with `useBladeWidgets()`
|
|
1036
|
+
- If entity is a "primary" concept that would benefit from a dashboard summary → add `dashboard: true` — generates a `DashboardWidgetCard` with stats
|
|
1037
|
+
- If entity mentions "notifications" or "events" or "alerts" → add `notifications: ["EventName"]` — generates notification template components
|
|
1038
|
+
|
|
1003
1039
|
### Phase 3: Plan Presentation
|
|
1004
1040
|
|
|
1005
1041
|
Present the parsed plan to the user in this format:
|
|
@@ -1009,10 +1045,13 @@ Application Plan: {DESIGN_PLAN.appName}
|
|
|
1009
1045
|
|
|
1010
1046
|
Modules ({count}):
|
|
1011
1047
|
─────────────────────────────────────────
|
|
1012
|
-
1. {name} ({bladeTypes})
|
|
1013
|
-
Columns: {comma-separated column names}
|
|
1014
|
-
Fields: {comma-separated
|
|
1048
|
+
1. {name} ({bladeTypes}{, read-only if readOnly})
|
|
1049
|
+
Columns: {comma-separated column names with types, e.g. "name(string), status(enum), price(currency)"}
|
|
1050
|
+
Fields: {comma-separated "fieldName → Component", e.g. "name → VcInput, description → VcTextarea, price → VcInputCurrency"}
|
|
1015
1051
|
Actions: {comma-separated action labels}
|
|
1052
|
+
{Widgets: sub-entity-1, sub-entity-2 — if widgets present}
|
|
1053
|
+
{Dashboard: yes — if dashboard present}
|
|
1054
|
+
{Notifications: EventName — if notifications present}
|
|
1016
1055
|
TODO: "{todo text}"
|
|
1017
1056
|
|
|
1018
1057
|
2. {name} ({bladeTypes})
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# useWidget
|
|
2
|
-
|
|
3
|
-
Widget-side composable that auto-discovers widget identity from `WidgetProvider` and provides a `setTrigger()` method to register refresh/badge contracts with the widget service. This composable is the counterpart to the blade-side `useWidgets()` / `useBladeWidgets()` -- while those manage widget registration from the blade's perspective, `useWidget()` is called from inside the widget component itself to communicate its state (badge count, icon, refresh callback) back to the widget system.
|
|
4
|
-
|
|
5
|
-
**Deprecated**: Prefer headless widgets via `useBladeWidgets()` instead. This composable is only needed for legacy SFC widgets that register trigger contracts manually.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- Inside a widget component wrapped by `WidgetProvider` to register badge counts, refresh callbacks, or click handlers
|
|
10
|
-
- When building a custom blade widget that needs to communicate its state back to the parent blade's widget panel
|
|
11
|
-
- When NOT to use: outside a `WidgetProvider` context (will throw). For managing widgets from the blade side, use `useWidgets()` or `useBladeWidgets()` instead. For new widgets, prefer the headless widget pattern.
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```vue
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
import { useWidget } from '@vc-shell/framework';
|
|
18
|
-
import { ref, computed, onMounted } from 'vue';
|
|
19
|
-
|
|
20
|
-
const { widgetId, setTrigger } = useWidget();
|
|
21
|
-
|
|
22
|
-
const items = ref<unknown[]>([]);
|
|
23
|
-
const unreadCount = computed(() => items.value.filter((i) => !i.isRead).length);
|
|
24
|
-
|
|
25
|
-
async function fetchData() {
|
|
26
|
-
items.value = await api.getWidgetItems();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Register the trigger contract: badge + refresh callback
|
|
30
|
-
setTrigger({
|
|
31
|
-
badge: unreadCount,
|
|
32
|
-
icon: 'fas fa-inbox',
|
|
33
|
-
title: 'Messages',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
onMounted(fetchData);
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<template>
|
|
40
|
-
<div class="tw-p-4">
|
|
41
|
-
<p class="tw-text-sm tw-text-gray-500">Widget ID: {{ widgetId }}</p>
|
|
42
|
-
<ul>
|
|
43
|
-
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
|
|
44
|
-
</ul>
|
|
45
|
-
</div>
|
|
46
|
-
</template>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## API
|
|
50
|
-
|
|
51
|
-
### Parameters
|
|
52
|
-
|
|
53
|
-
None. Identity is auto-discovered from the `WidgetProvider` injection context. The composable injects `WidgetIdKey` for the widget ID, `WidgetServiceKey` for the service, and `BladeDescriptorKey` for the parent blade context.
|
|
54
|
-
|
|
55
|
-
### Returns
|
|
56
|
-
|
|
57
|
-
| Property | Type | Description |
|
|
58
|
-
|----------|------|-------------|
|
|
59
|
-
| `widgetId` | `string` | The widget's unique ID, injected by `WidgetProvider`. |
|
|
60
|
-
| `setTrigger` | `(trigger: IWidgetTrigger) => void` | Registers a trigger contract (badge, icon, title, callbacks) for this widget. Can be called multiple times to update the trigger. |
|
|
61
|
-
|
|
62
|
-
### IWidgetTrigger
|
|
63
|
-
|
|
64
|
-
| Property | Type | Description |
|
|
65
|
-
|----------|------|-------------|
|
|
66
|
-
| `icon` | `string?` | Lucide or FontAwesome icon name for the widget's display in the dropdown/panel. |
|
|
67
|
-
| `title` | `string?` | Display title for the widget. Falls back to `IWidget.title` if not set. |
|
|
68
|
-
| `badge` | `Ref<number \| string> \| ComputedRef<number \| string>?` | Reactive badge value displayed on the widget button/tab. Pass a computed ref for automatic updates. |
|
|
69
|
-
|
|
70
|
-
## How It Works
|
|
71
|
-
|
|
72
|
-
1. **Identity discovery**: On creation, the composable injects `WidgetIdKey` (provided by `WidgetProvider`) and `WidgetServiceKey` (provided by `provideWidgetService()`). It also injects `BladeDescriptorKey` to know which blade this widget belongs to.
|
|
73
|
-
|
|
74
|
-
2. **Trigger registration**: When you call `setTrigger(trigger)`, the composable calls `widgetService.updateWidget({ id: widgetId, bladeId, widget: { trigger } })`. This stores the trigger contract in the widget service, where the blade's widget panel can read it.
|
|
75
|
-
|
|
76
|
-
3. **Reactive badges**: Because `badge` accepts a `Ref` or `ComputedRef`, the widget panel automatically updates whenever the badge value changes. There is no need to call `setTrigger` again after the initial registration.
|
|
77
|
-
|
|
78
|
-
## Recipe: Widget with Live Badge Count
|
|
79
|
-
|
|
80
|
-
```vue
|
|
81
|
-
<script setup lang="ts">
|
|
82
|
-
import { useWidget } from '@vc-shell/framework';
|
|
83
|
-
import { ref, computed, onMounted } from 'vue';
|
|
84
|
-
|
|
85
|
-
const { setTrigger } = useWidget();
|
|
86
|
-
const pendingOrders = ref<Order[]>([]);
|
|
87
|
-
|
|
88
|
-
async function loadPendingOrders() {
|
|
89
|
-
pendingOrders.value = await orderApi.getPending();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Badge shows the count of pending orders
|
|
93
|
-
setTrigger({
|
|
94
|
-
badge: computed(() => pendingOrders.value.length),
|
|
95
|
-
icon: 'fas fa-clock',
|
|
96
|
-
title: 'Pending Orders',
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
onMounted(loadPendingOrders);
|
|
100
|
-
|
|
101
|
-
// Optionally refresh periodically
|
|
102
|
-
const interval = setInterval(loadPendingOrders, 30_000);
|
|
103
|
-
onUnmounted(() => clearInterval(interval));
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<template>
|
|
107
|
-
<div class="tw-space-y-2">
|
|
108
|
-
<div v-for="order in pendingOrders" :key="order.id" class="tw-p-2 tw-border tw-rounded">
|
|
109
|
-
<p class="tw-font-medium">{{ order.number }}</p>
|
|
110
|
-
<p class="tw-text-sm tw-text-gray-500">{{ order.status }}</p>
|
|
111
|
-
</div>
|
|
112
|
-
<p v-if="!pendingOrders.length" class="tw-text-gray-400">No pending orders</p>
|
|
113
|
-
</div>
|
|
114
|
-
</template>
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Recipe: Widget That Updates on Blade Data Change
|
|
118
|
-
|
|
119
|
-
```vue
|
|
120
|
-
<script setup lang="ts">
|
|
121
|
-
import { useWidget } from '@vc-shell/framework';
|
|
122
|
-
import { inject, watch, ref, computed } from 'vue';
|
|
123
|
-
|
|
124
|
-
const { setTrigger } = useWidget();
|
|
125
|
-
|
|
126
|
-
// Inject blade data from parent
|
|
127
|
-
const bladeData = inject('bladeData') as Ref<{ orderId: string }>;
|
|
128
|
-
const comments = ref<Comment[]>([]);
|
|
129
|
-
|
|
130
|
-
async function loadComments(orderId: string) {
|
|
131
|
-
comments.value = await commentApi.getByOrder(orderId);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Reload when blade data changes
|
|
135
|
-
watch(() => bladeData.value.orderId, (id) => {
|
|
136
|
-
if (id) loadComments(id);
|
|
137
|
-
}, { immediate: true });
|
|
138
|
-
|
|
139
|
-
setTrigger({
|
|
140
|
-
badge: computed(() => comments.value.length),
|
|
141
|
-
icon: 'fas fa-comments',
|
|
142
|
-
title: 'Comments',
|
|
143
|
-
});
|
|
144
|
-
</script>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## Tips
|
|
148
|
-
|
|
149
|
-
- **Must be inside `WidgetProvider`.** This composable relies on `WidgetIdKey` being injected by `WidgetProvider`. If called outside that context, it throws an `InjectionError`. The `WidgetProvider` component is automatically rendered by the blade widget system.
|
|
150
|
-
- **`setTrigger` can be called multiple times.** Each call replaces the previous trigger contract. However, since `badge` is reactive, you typically only need to call `setTrigger` once during setup.
|
|
151
|
-
- **Badge accepts both numbers and strings.** You can use `computed(() => 3)` for a numeric badge or `computed(() => '!')` for a text indicator.
|
|
152
|
-
- **Prefer headless widgets for new code.** The `useBladeWidgets()` composable supports a headless pattern where widgets are defined declaratively without a separate SFC. This is simpler and avoids the WidgetProvider ceremony.
|
|
153
|
-
|
|
154
|
-
## Related
|
|
155
|
-
|
|
156
|
-
- [useWidgets](../useWidgets/useWidgets.docs.md) -- blade-level widget service access
|
|
157
|
-
- [useBladeWidgets](../useBladeWidgets/) -- lifecycle-managed widget registration for blades (preferred API for new code)
|
|
158
|
-
- `WidgetProvider` -- the component that provides the `WidgetIdKey` injection
|
|
159
|
-
- `framework/injection-keys.ts` -- defines `WidgetIdKey` and `WidgetServiceKey`
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# App Switcher
|
|
2
|
-
|
|
3
|
-
A component and composable for switching between multiple VirtoCommerce applications (e.g., Vendor Portal, Admin Portal) within the same platform instance.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The App Switcher fetches a list of available applications from the platform API and renders them in a dropdown. Clicking an app navigates the browser to that app's URL. Permission checks ensure users only see apps they have access to.
|
|
8
|
-
|
|
9
|
-
## Directory Structure
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
app-switcher/
|
|
13
|
-
components/
|
|
14
|
-
vc-app-switcher/
|
|
15
|
-
vc-app-switcher.vue # Dropdown UI component
|
|
16
|
-
composables/
|
|
17
|
-
useAppSwitcher/
|
|
18
|
-
index.ts # Data fetching and navigation logic
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Composable: useAppSwitcher
|
|
22
|
-
|
|
23
|
-
Provides the data layer for app switching.
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { useAppSwitcher } from "@vc-shell/framework";
|
|
27
|
-
|
|
28
|
-
const { appsList, getApps, switchApp } = useAppSwitcher();
|
|
29
|
-
|
|
30
|
-
// Fetch available apps on mount
|
|
31
|
-
await getApps();
|
|
32
|
-
|
|
33
|
-
// Switch to an app (checks permissions, navigates via window.location)
|
|
34
|
-
switchApp(selectedApp);
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Return Value
|
|
38
|
-
|
|
39
|
-
| Property | Type | Description |
|
|
40
|
-
|---|---|---|
|
|
41
|
-
| `appsList` | `Ref<AppDescriptor[]>` | Reactive list of available applications (readonly computed) |
|
|
42
|
-
| `getApps` | `() => Promise<void>` | Fetches the app list from `AppsClient` |
|
|
43
|
-
| `switchApp` | `(app: AppDescriptor) => void` | Navigates to the app's URL if the user has permission |
|
|
44
|
-
|
|
45
|
-
### AppDescriptor
|
|
46
|
-
|
|
47
|
-
Each app in the list has:
|
|
48
|
-
- `id` -- unique identifier
|
|
49
|
-
- `title` -- display name
|
|
50
|
-
- `iconUrl` -- URL of the app icon
|
|
51
|
-
- `relativeUrl` -- the URL path to navigate to
|
|
52
|
-
- `permission` -- required permission string
|
|
53
|
-
|
|
54
|
-
## Component: VcAppSwitcher
|
|
55
|
-
|
|
56
|
-
Renders the app list inside a `VcDropdown`. Highlights the currently active app based on URL matching.
|
|
57
|
-
|
|
58
|
-
### Props
|
|
59
|
-
|
|
60
|
-
| Prop | Type | Description |
|
|
61
|
-
|---|---|---|
|
|
62
|
-
| `appsList` | `AppDescriptor[]` | List of apps to display |
|
|
63
|
-
|
|
64
|
-
### Events
|
|
65
|
-
|
|
66
|
-
| Event | Payload | Description |
|
|
67
|
-
|---|---|---|
|
|
68
|
-
| `onClick` | `AppDescriptor` | Emitted when an app is clicked |
|
|
69
|
-
|
|
70
|
-
## Usage
|
|
71
|
-
|
|
72
|
-
```vue
|
|
73
|
-
<script setup>
|
|
74
|
-
import { useAppSwitcher } from "@vc-shell/framework";
|
|
75
|
-
import { VcAppSwitcher } from "@vc-shell/framework";
|
|
76
|
-
|
|
77
|
-
const { appsList, getApps, switchApp } = useAppSwitcher();
|
|
78
|
-
onMounted(() => getApps());
|
|
79
|
-
</script>
|
|
80
|
-
|
|
81
|
-
<template>
|
|
82
|
-
<VcAppSwitcher
|
|
83
|
-
:apps-list="appsList"
|
|
84
|
-
@on-click="switchApp"
|
|
85
|
-
/>
|
|
86
|
-
</template>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Behavior
|
|
90
|
-
|
|
91
|
-
- `switchApp` checks `hasAccess(app.permission)` before navigating. If access is denied, a notification error is shown.
|
|
92
|
-
- Navigation uses `window.location.href` (full page reload) since apps are separate SPA deployments.
|
|
93
|
-
- The active app is determined by matching `window.location.pathname` against each app's `relativeUrl`.
|
|
94
|
-
|
|
95
|
-
## Tips
|
|
96
|
-
|
|
97
|
-
- The composable uses `usePermissions()` internally -- no need to check permissions manually.
|
|
98
|
-
- `getApps()` calls the platform API and may throw on network errors. Wrap in try/catch if needed.
|
|
99
|
-
- The component is typically rendered in the app bar header area.
|
|
100
|
-
|
|
101
|
-
## Related
|
|
102
|
-
|
|
103
|
-
- `framework/core/api/platform/` -- `AppsClient` API client
|
|
104
|
-
- `framework/core/composables/usePermissions/` -- permission checking
|