@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +12 -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/useBladeWidgets.docs.md +164 -9
  15. package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +6 -0
  16. package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +6 -0
  17. package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +6 -0
  18. package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +6 -0
  19. package/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md +2 -2
  20. package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +6 -0
  21. package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +6 -0
  22. package/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +6 -0
  23. package/runtime/knowledge/docs/core/plugins/i18n/i18n.docs.md +74 -0
  24. package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +6 -0
  25. package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +6 -0
  26. package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +6 -0
  27. package/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +6 -0
  28. package/runtime/knowledge/docs/injection-keys.docs.md +1 -1
  29. package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +29 -33
  30. package/runtime/knowledge/docs/shell/components/logout-button/logout-button.docs.md +3 -3
  31. package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +11 -0
  32. package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +11 -0
  33. package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +11 -0
  34. package/runtime/knowledge/docs/ui/components/organisms/vc-table/vc-data-table.docs.md +12 -0
  35. package/runtime/knowledge/index.md +60 -0
  36. package/runtime/knowledge/patterns/assets-management.md +213 -0
  37. package/runtime/knowledge/patterns/child-blade-flow.md +277 -0
  38. package/runtime/knowledge/patterns/details-blade-pattern.md +350 -3
  39. package/runtime/knowledge/patterns/extension-points-usage.md +308 -0
  40. package/runtime/knowledge/patterns/form-validation.md +377 -0
  41. package/runtime/knowledge/patterns/multilanguage-fields.md +239 -0
  42. package/runtime/knowledge/patterns/signalr-notifications.md +237 -0
  43. package/runtime/vc-app.md +44 -5
  44. package/runtime/knowledge/docs/core/composables/useWidget/useWidget.docs.md +0 -159
  45. 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: dates `"date-time"`, booleans/flags `"boolean"`, numbers/counts/amounts → `"number"`, everything else → `"string"`
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 field names}
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