@vc-shell/vc-app-skill 2.0.5 → 2.0.6
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 +6 -0
- package/README.md +7 -7
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/api/platform.docs.md +6 -6
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/bladeContext/index.docs.md +2 -2
- package/runtime/knowledge/docs/core/composables/useApiClient/useApiClient.docs.md +67 -42
- package/runtime/knowledge/docs/core/composables/useAppBarMobileButtons/useAppBarMobileButtons.docs.md +2 -2
- package/runtime/knowledge/docs/core/composables/useAppBarWidget/useAppBarWidget.docs.md +15 -13
- package/runtime/knowledge/docs/core/composables/useAppInsights/useAppInsights.docs.md +11 -11
- package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +17 -14
- package/runtime/knowledge/docs/core/composables/useBeforeUnload/useBeforeUnload.docs.md +4 -2
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +31 -23
- package/runtime/knowledge/docs/core/composables/useBladeWidgets/index.docs.md +46 -26
- package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +1 -0
- package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +1 -0
- package/runtime/knowledge/docs/core/composables/useNotifications/useNotifications.docs.md +20 -6
- package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +2 -1
- package/runtime/knowledge/docs/core/composables/useSettings/useSettings.docs.md +2 -2
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +109 -133
- package/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md +1 -2
- package/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md +184 -0
- package/runtime/knowledge/docs/core/notifications/composables/useBroadcastFilter.docs.md +117 -0
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationContext.docs.md +150 -0
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md +113 -0
- package/runtime/knowledge/docs/core/notifications/notifications.docs.md +12 -15
- package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +1 -1
- package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +54 -116
- package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +12 -3
- package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +20 -20
- package/runtime/knowledge/docs/injection-keys.docs.md +1 -1
- package/runtime/knowledge/docs/shell/auth/LoginPage/login-page.docs.md +1 -1
- package/runtime/knowledge/docs/shell/components/language-selector/language-selector.docs.md +1 -1
- package/runtime/knowledge/docs/shell/components/notification-dropdown/notification-dropdown.docs.md +1 -1
- package/runtime/knowledge/docs/shell/components/notification-template/notification-template.docs.md +1 -1
- package/runtime/knowledge/docs/shell/components/settings-menu/settings-menu.docs.md +7 -7
- package/runtime/knowledge/docs/shell/components/settings-menu-item/settings-menu-item.docs.md +2 -2
- package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/draggable-dashboard.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-badge/vc-badge.docs.md +4 -4
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-container/vc-container.docs.md +21 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-link/vc-link.docs.md +22 -6
- package/runtime/knowledge/docs/ui/components/atoms/vc-skeleton/vc-skeleton.docs.md +2 -2
- package/runtime/knowledge/docs/ui/components/atoms/vc-status-icon/vc-status-icon.docs.md +32 -26
- package/runtime/knowledge/docs/ui/components/atoms/vc-tooltip/vc-tooltip.docs.md +3 -3
- package/runtime/knowledge/docs/ui/components/molecules/multilanguage-selector/multilanguage-selector.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/molecules/vc-accordion/vc-accordion.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/molecules/vc-breadcrumbs/vc-breadcrumbs.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/molecules/vc-dropdown-panel/vc-dropdown-panel.docs.md +13 -13
- package/runtime/knowledge/docs/ui/components/molecules/vc-form/vc-form.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/molecules/vc-pagination/vc-pagination.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-app/vc-app.docs.md +4 -4
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +39 -37
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +17 -17
- package/runtime/knowledge/docs/ui/components/organisms/vc-dynamic-property/vc-dynamic-property.docs.md +16 -7
- package/runtime/knowledge/docs/ui/components/organisms/vc-sidebar/vc-sidebar.docs.md +1 -1
|
@@ -5,84 +5,88 @@ group: services
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
!!! tip "Long page"
|
|
8
|
-
Use the section headings to jump directly to what you need: [
|
|
8
|
+
Use the section headings to jump directly to what you need: [Primary pattern: array + toolbar-items](#primary-pattern-array--toolbar-items), [When to reach for useToolbar](#when-to-reach-for-usetoolbar), [Quick Start](#quick-start), or [API Reference](#api-reference).
|
|
9
9
|
|
|
10
10
|
# useToolbar
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`useToolbar` is the framework's **advanced** API for blade toolbar registration. The everyday way to give a blade a toolbar is the `:toolbar-items` prop on `VcBlade` with a plain `IBladeToolbar[]` array — there is no need to call `useToolbar` in the typical case. Reach for `useToolbar` when toolbar items must appear after mount, when a blade needs to mutate another blade's toolbar, or when toolbar composition is driven from a non-blade owner.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Primary pattern: array + toolbar-items
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
- Dynamically update button state (disabled, visible, icon) in response to loading or form changes
|
|
18
|
-
- When NOT to use: for global app-bar actions -- use `useAppBarWidget`; for navigation links -- use `useMenuService`
|
|
16
|
+
In a regular blade, declare the toolbar as `ref<IBladeToolbar[]>([...])` and bind it. Reactive fields (`isVisible`, `disabled`, computed `title`) drive visibility and state without any `watch` plumbing. The framework filters items by `permissions` and `isVisible` before render.
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
18
|
+
```vue title="orders-details.vue"
|
|
23
19
|
<script setup lang="ts">
|
|
24
|
-
import {
|
|
20
|
+
import { computed, ref } from "vue";
|
|
21
|
+
import { useAsync, usePermissions, VcBlade, type IBladeToolbar } from "@vc-shell/framework";
|
|
25
22
|
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
id: "save",
|
|
30
|
-
title: "Save",
|
|
31
|
-
icon: "fas fa-save",
|
|
32
|
-
clickHandler: () => saveData(),
|
|
23
|
+
const { hasAccess } = usePermissions();
|
|
24
|
+
const { loading: saving, action: save } = useAsync(async () => {
|
|
25
|
+
await saveOrder(order.value);
|
|
33
26
|
});
|
|
27
|
+
|
|
28
|
+
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
29
|
+
{
|
|
30
|
+
id: "save",
|
|
31
|
+
title: "Save",
|
|
32
|
+
icon: "lucide-save",
|
|
33
|
+
disabled: computed(() => saving.value),
|
|
34
|
+
isVisible: computed(() => isDirty.value && hasAccess("order:update")),
|
|
35
|
+
clickHandler: () => save(),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "delete",
|
|
39
|
+
title: "Delete",
|
|
40
|
+
icon: "lucide-trash",
|
|
41
|
+
isVisible: computed(() => hasAccess(["order:delete", "order:manage"])),
|
|
42
|
+
clickHandler: () => confirmDelete(),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
34
45
|
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<VcBlade :toolbar-items="bladeToolbar" />
|
|
49
|
+
</template>
|
|
35
50
|
```
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
Use this pattern for every blade unless you have a concrete reason to register imperatively. `IBladeToolbar` is exported from `@vc-shell/framework`; see [Core types](../../types/) for the full shape. Permission gating goes through `isVisible: computed(() => hasAccess(...))` so it stays reactive when the user's permission set changes.
|
|
38
53
|
|
|
39
|
-
##
|
|
54
|
+
## When to reach for useToolbar
|
|
40
55
|
|
|
41
|
-
|
|
56
|
+
`useToolbar` becomes the right tool only in these cases:
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
- **Dynamic registration after mount.** A toolbar item is decided after the blade is already on screen — for example, a button that appears after a long-running operation completes and never has a representation in the initial array.
|
|
59
|
+
- **Cross-blade toolbar mutation.** Code in blade A needs to add or change a button on blade B. Pass `targetBladeId` to the imperative methods.
|
|
60
|
+
- **Non-blade owner.** A composable or service that doesn't live inside a blade `<script setup>` but still needs to contribute toolbar items.
|
|
61
|
+
- **Wildcard / global items.** Toolbar items that appear on every blade through the `*` wildcard bladeId.
|
|
62
|
+
|
|
63
|
+
In all other cases, declare the array and bind `:toolbar-items`. The imperative API exists to cover edge cases, not to replace the array.
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
Imperative registration from inside a blade, scoped to the current blade context:
|
|
68
|
+
|
|
69
|
+
```typescript title="<script setup>"
|
|
45
70
|
import { useToolbar } from "@vc-shell/framework";
|
|
46
71
|
|
|
47
72
|
const { registerToolbarItem } = useToolbar();
|
|
48
73
|
|
|
49
|
-
// Primary action — highest priority, appears first
|
|
50
74
|
registerToolbarItem({
|
|
51
75
|
id: "save",
|
|
52
76
|
title: "Save",
|
|
53
77
|
icon: "fas fa-save",
|
|
54
|
-
clickHandler: () =>
|
|
55
|
-
priority: 100,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Secondary action — lower priority, appears after Save
|
|
59
|
-
registerToolbarItem({
|
|
60
|
-
id: "refresh",
|
|
61
|
-
title: "Refresh",
|
|
62
|
-
icon: "fas fa-sync",
|
|
63
|
-
clickHandler: () => refresh(),
|
|
64
|
-
priority: 50,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Destructive action with permission gate
|
|
68
|
-
registerToolbarItem({
|
|
69
|
-
id: "delete",
|
|
70
|
-
title: "Delete",
|
|
71
|
-
icon: "fas fa-trash",
|
|
72
|
-
clickHandler: () => confirmDelete(),
|
|
73
|
-
priority: 10,
|
|
74
|
-
permissions: "order:delete",
|
|
78
|
+
clickHandler: () => saveData(),
|
|
75
79
|
});
|
|
76
|
-
</script>
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
The button appears in the current blade's toolbar immediately. When the component unmounts, the button is automatically removed (controlled by `autoCleanup`).
|
|
80
83
|
|
|
81
|
-
## Updating
|
|
84
|
+
## Updating button state dynamically
|
|
82
85
|
|
|
83
|
-
Use `updateToolbarItem` to change any property of a registered button without re-registering it. This is the
|
|
86
|
+
Use `updateToolbarItem` to change any property of a registered button without re-registering it. This is the imperative analogue of binding a reactive `disabled` value in the array pattern.
|
|
84
87
|
|
|
85
88
|
```typescript
|
|
89
|
+
// pseudo-code: replace OrderClient with your generated API client
|
|
86
90
|
<script setup lang="ts">
|
|
87
91
|
import { watch } from "vue";
|
|
88
92
|
import { useToolbar, useAsync } from "@vc-shell/framework";
|
|
@@ -109,6 +113,8 @@ watch(loading, (isLoading) => {
|
|
|
109
113
|
</script>
|
|
110
114
|
```
|
|
111
115
|
|
|
116
|
+
In the array pattern, the same outcome reads as `disabled: computed(() => loading.value)` on the entry — no `watch` needed. Use `updateToolbarItem` only when the item has been registered imperatively and there is no array reference to mutate.
|
|
117
|
+
|
|
112
118
|
You can update any subset of `IToolbarItem` properties:
|
|
113
119
|
|
|
114
120
|
```typescript
|
|
@@ -119,14 +125,12 @@ updateToolbarItem("toggle-publish", {
|
|
|
119
125
|
});
|
|
120
126
|
```
|
|
121
127
|
|
|
122
|
-
## Visibility and
|
|
128
|
+
## Visibility and permissions
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
Both reactive visibility and permission gating work the same way whether the item lives in an array or is registered imperatively — the framework filters by `isVisible` and `permissions` before render in either path.
|
|
125
131
|
|
|
126
132
|
### Reactive visibility with `isVisible`
|
|
127
133
|
|
|
128
|
-
The `isVisible` property accepts a boolean, a `Ref<boolean>`, a `ComputedRef<boolean>`, or a function:
|
|
129
|
-
|
|
130
134
|
```typescript
|
|
131
135
|
import { computed } from "vue";
|
|
132
136
|
|
|
@@ -143,25 +147,7 @@ registerToolbarItem({
|
|
|
143
147
|
|
|
144
148
|
### Permission-based registration
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
import { useToolbar, usePermissions } from "@vc-shell/framework";
|
|
150
|
-
|
|
151
|
-
const { registerToolbarItem } = useToolbar();
|
|
152
|
-
const { hasAccess } = usePermissions();
|
|
153
|
-
|
|
154
|
-
if (hasAccess("order:delete")) {
|
|
155
|
-
registerToolbarItem({
|
|
156
|
-
id: "delete",
|
|
157
|
-
title: "Delete",
|
|
158
|
-
icon: "fas fa-trash",
|
|
159
|
-
clickHandler: () => deleteOrder(),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Alternatively, set the `permissions` property on the item itself. The toolbar renderer checks this before displaying the button:
|
|
150
|
+
Set `permissions` on the item; the renderer hides it when the user lacks the listed permission(s) (OR logic on arrays):
|
|
165
151
|
|
|
166
152
|
```typescript
|
|
167
153
|
registerToolbarItem({
|
|
@@ -169,13 +155,15 @@ registerToolbarItem({
|
|
|
169
155
|
title: "Delete",
|
|
170
156
|
icon: "fas fa-trash",
|
|
171
157
|
clickHandler: () => deleteOrder(),
|
|
172
|
-
permissions: ["order:delete"],
|
|
158
|
+
permissions: ["order:delete", "order:manage"],
|
|
173
159
|
});
|
|
174
160
|
```
|
|
175
161
|
|
|
176
|
-
|
|
162
|
+
Avoid wrapping `registerToolbarItem` in an `if (hasAccess(...))` check unless you specifically want to skip side effects of the registration; the `permissions` field already gates rendering.
|
|
163
|
+
|
|
164
|
+
## Cross-blade toolbar management
|
|
177
165
|
|
|
178
|
-
|
|
166
|
+
All methods accept an optional `targetBladeId` second argument. Pass it to register, update, or clear toolbar items on a blade other than the current one:
|
|
179
167
|
|
|
180
168
|
```typescript
|
|
181
169
|
// Register a button on a specific child blade
|
|
@@ -185,14 +173,15 @@ registerToolbarItem({ id: "child-action", title: "Action", clickHandler: () => {
|
|
|
185
173
|
clearBladeToolbarItems("ChildBlade");
|
|
186
174
|
```
|
|
187
175
|
|
|
188
|
-
|
|
176
|
+
Pass `"*"` as the bladeId to register a global item that appears on every blade.
|
|
177
|
+
|
|
178
|
+
## Auto-cleanup control
|
|
189
179
|
|
|
190
|
-
By default,
|
|
180
|
+
By default, items registered through `useToolbar()` inside a component setup are cleared when that component unmounts (`autoCleanup: true`). Disable this for items that should persist beyond the registering component:
|
|
191
181
|
|
|
192
182
|
```typescript
|
|
193
183
|
const { registerToolbarItem } = useToolbar({ autoCleanup: false });
|
|
194
184
|
|
|
195
|
-
// These items survive the component's unmount cycle
|
|
196
185
|
registerToolbarItem({
|
|
197
186
|
id: "global-help",
|
|
198
187
|
title: "Help",
|
|
@@ -201,68 +190,38 @@ registerToolbarItem({
|
|
|
201
190
|
});
|
|
202
191
|
```
|
|
203
192
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### Complete Blade with Save / Delete / Refresh
|
|
193
|
+
Use `autoCleanup: false` for shared toolbar items registered outside a blade lifecycle (for example, from a module's bootstrap).
|
|
207
194
|
|
|
208
|
-
|
|
209
|
-
<script setup lang="ts">
|
|
210
|
-
import { watch } from "vue";
|
|
211
|
-
import { useToolbar, useAsync, usePermissions, useApiClient } from "@vc-shell/framework";
|
|
212
|
-
import { OrderClient } from "@api/orders";
|
|
195
|
+
## Recipes
|
|
213
196
|
|
|
214
|
-
|
|
215
|
-
const { hasAccess } = usePermissions();
|
|
216
|
-
const { getApiClient } = useApiClient(OrderClient);
|
|
197
|
+
### Dynamic registration after a non-blade action
|
|
217
198
|
|
|
218
|
-
|
|
219
|
-
const client = await getApiClient();
|
|
220
|
-
await client.updateOrder(order.value);
|
|
221
|
-
});
|
|
199
|
+
`useToolbar` shines when a button is decided by a non-blade flow — for example, after a long-running export completes, expose a "Download result" button on the originating blade:
|
|
222
200
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
201
|
+
```ts
|
|
202
|
+
// pseudo-code: replace ExportClient with your generated API client
|
|
203
|
+
import { useToolbar, useBlade } from "@vc-shell/framework";
|
|
227
204
|
|
|
228
|
-
registerToolbarItem(
|
|
229
|
-
|
|
230
|
-
title: "Save",
|
|
231
|
-
icon: "fas fa-save",
|
|
232
|
-
clickHandler: () => save(),
|
|
233
|
-
priority: 100,
|
|
234
|
-
});
|
|
205
|
+
const { registerToolbarItem, unregisterToolbarItem } = useToolbar();
|
|
206
|
+
const { id: bladeId } = useBlade();
|
|
235
207
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
icon: "fas fa-sync",
|
|
240
|
-
clickHandler: () => refresh(),
|
|
241
|
-
priority: 50,
|
|
242
|
-
});
|
|
208
|
+
async function startExport() {
|
|
209
|
+
const job = await startServerExport();
|
|
210
|
+
await waitForCompletion(job.id);
|
|
243
211
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
212
|
+
registerToolbarItem(
|
|
213
|
+
{
|
|
214
|
+
id: `download-${job.id}`,
|
|
215
|
+
title: "Download CSV",
|
|
216
|
+
icon: "lucide-download",
|
|
217
|
+
clickHandler: () => downloadResult(job.id),
|
|
218
|
+
},
|
|
219
|
+
bladeId.value,
|
|
220
|
+
);
|
|
252
221
|
}
|
|
253
|
-
|
|
254
|
-
// Disable all buttons while any operation is running
|
|
255
|
-
watch([saving, refreshing], ([s, r]) => {
|
|
256
|
-
const busy = s || r;
|
|
257
|
-
updateToolbarItem("save", { disabled: busy });
|
|
258
|
-
updateToolbarItem("refresh", { disabled: busy });
|
|
259
|
-
});
|
|
260
|
-
</script>
|
|
261
222
|
```
|
|
262
223
|
|
|
263
|
-
### Visual
|
|
264
|
-
|
|
265
|
-
Use the `separator` property to add a vertical divider:
|
|
224
|
+
### Visual separator between button groups
|
|
266
225
|
|
|
267
226
|
```typescript
|
|
268
227
|
registerToolbarItem({
|
|
@@ -275,7 +234,21 @@ registerToolbarItem({
|
|
|
275
234
|
});
|
|
276
235
|
```
|
|
277
236
|
|
|
278
|
-
## Common
|
|
237
|
+
## Common mistakes
|
|
238
|
+
|
|
239
|
+
### Reaching for `useToolbar` before considering the array pattern
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Avoid in everyday blades — preferred pattern is the array + :toolbar-items
|
|
243
|
+
const { registerToolbarItem } = useToolbar();
|
|
244
|
+
registerToolbarItem({ id: "save", ... });
|
|
245
|
+
|
|
246
|
+
// Preferred: declarative array bound to VcBlade
|
|
247
|
+
const bladeToolbar = ref<IBladeToolbar[]>([{ id: "save", ... }]);
|
|
248
|
+
// <VcBlade :toolbar-items="bladeToolbar" />
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The array form is shorter, reactive without `watch`/`updateToolbarItem` plumbing, and easier to test.
|
|
279
252
|
|
|
280
253
|
### Forgetting the `id` property
|
|
281
254
|
|
|
@@ -299,7 +272,7 @@ registerToolbarItem({
|
|
|
299
272
|
### Re-registering instead of updating
|
|
300
273
|
|
|
301
274
|
```typescript
|
|
302
|
-
// Wrong --
|
|
275
|
+
// Wrong -- duplicates the entry on each loading state change
|
|
303
276
|
watch(loading, (isLoading) => {
|
|
304
277
|
registerToolbarItem({
|
|
305
278
|
id: "save",
|
|
@@ -315,7 +288,7 @@ watch(loading, (isLoading) => {
|
|
|
315
288
|
});
|
|
316
289
|
```
|
|
317
290
|
|
|
318
|
-
### Using useToolbar outside a component setup
|
|
291
|
+
### Using `useToolbar` outside a component setup
|
|
319
292
|
|
|
320
293
|
```typescript
|
|
321
294
|
// Wrong -- no component instance, autoCleanup will not work
|
|
@@ -324,7 +297,7 @@ function helperFunction() {
|
|
|
324
297
|
registerToolbarItem({ id: "test", title: "Test" });
|
|
325
298
|
}
|
|
326
299
|
|
|
327
|
-
// Correct -- call in <script setup> or within setup()
|
|
300
|
+
// Correct -- call in <script setup> or within setup(), or pass autoCleanup: false
|
|
328
301
|
```
|
|
329
302
|
|
|
330
303
|
## API Reference
|
|
@@ -368,8 +341,11 @@ function helperFunction() {
|
|
|
368
341
|
| `permissions` | `string \| string[]` | No | Required permission(s) to display the button |
|
|
369
342
|
| `bladeId` | `string` | No | Target blade ID (auto-resolved from context) |
|
|
370
343
|
|
|
344
|
+
`IToolbarItem` is the shape consumed by `ToolbarService`. The blade-level array binding uses `IBladeToolbar` (see [Core types](../../types/)), a near-identical shape that the framework normalizes into `IToolbarItem` before render.
|
|
345
|
+
|
|
371
346
|
## Related
|
|
372
347
|
|
|
373
348
|
- [useBlade](../useBlade/) -- blade context that toolbar items are scoped to
|
|
374
349
|
- [usePermissions](../usePermissions/) -- conditionally register toolbar items based on permissions
|
|
375
350
|
- [useAsync](../useAsync/) -- wraps async operations with loading state for disabling buttons
|
|
351
|
+
- `IBladeToolbar` in [Core types](../../types/) — the shape used by the `:toolbar-items` array binding
|
|
@@ -153,6 +153,5 @@ const allWidgets = computed(() => [...bladeWidgets.value, ...externalWidgets.val
|
|
|
153
153
|
|
|
154
154
|
## Related
|
|
155
155
|
|
|
156
|
-
- [
|
|
157
|
-
- [useBladeWidgets](../useBladeWidgets/) -- lifecycle-managed widget registration for blades (preferred API)
|
|
156
|
+
- [useBladeWidgets](../useBladeWidgets/index.docs.md) -- lifecycle-managed widget registration for blades (preferred API)
|
|
158
157
|
- `framework/core/services/widget-service.ts` -- underlying service implementation
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useBladeNotifications
|
|
3
|
+
category: composables
|
|
4
|
+
group: notifications
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# useBladeNotifications
|
|
8
|
+
|
|
9
|
+
Subscribes a blade to one or more push-notification types from the platform's SignalR stream. The composable returns a reactive list of matching unread messages, an unread count, and a `markAsRead` action. The subscription is bound to the current effect scope, so it disappears the moment the blade closes — no manual unsubscribe.
|
|
10
|
+
|
|
11
|
+
This is the **Level 2** entry point in the notification system. Level 1 — `defineAppModule({ notifications })` — registers types globally with their toast configuration and is the always-on path. Level 2 layers blade-specific behavior on top of that: refresh a list, update a progress UI, mark a job complete.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- A list blade needs to refresh when an entity is created, updated, or deleted elsewhere.
|
|
16
|
+
- A long-running operation has a dedicated blade and the blade should update as `processedCount` / `errorCount` flow in.
|
|
17
|
+
- A blade wants to surface an inline "N new" badge for messages of a specific type.
|
|
18
|
+
- When NOT to use: app-wide toasts already come from the Level 1 module config — the blade does not need to subscribe just to show a toast. Reach for the blade subscription only when you also need to _react_ to the event in code.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { useBladeNotifications } from "@vc-shell/framework";
|
|
24
|
+
|
|
25
|
+
useBladeNotifications({
|
|
26
|
+
types: ["OfferDeletedDomainEvent"],
|
|
27
|
+
onMessage: () => reload(),
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That is the full recipe for "refresh this list when an offer is deleted somewhere in the app." The handler runs once per matching message; the framework cleans up the subscription when the blade unmounts.
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### Parameters
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface BladeNotificationOptions<T extends PushNotification = PushNotification> {
|
|
39
|
+
types: string[];
|
|
40
|
+
filter?: (msg: T) => boolean;
|
|
41
|
+
onMessage?: (msg: T) => void;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Field | Type | Required | Description |
|
|
46
|
+
| ----------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `types` | `string[]` | Yes | Notification types to subscribe to. Must match the `notifyType` field on incoming messages. |
|
|
48
|
+
| `filter` | `(msg: T) => boolean` | No | Narrow the subscription further (for example, only events for the entity this blade is editing). |
|
|
49
|
+
| `onMessage` | `(msg: T) => void` | No | Callback fired once per matching message. Use it to refresh data, mark progress, or update local state. |
|
|
50
|
+
|
|
51
|
+
### Returns
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface BladeNotificationReturn<T extends PushNotification = PushNotification> {
|
|
55
|
+
messages: ComputedRef<T[]>;
|
|
56
|
+
unreadCount: ComputedRef<number>;
|
|
57
|
+
markAsRead: (msg: T) => void;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Property | Type | Description |
|
|
62
|
+
| ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `messages` | `ComputedRef<T[]>` | Realtime messages matching `types` and `filter` that are still unread. Updates reactively as new messages arrive. |
|
|
64
|
+
| `unreadCount` | `ComputedRef<number>` | `messages.value.length`. Bind to a badge. |
|
|
65
|
+
| `markAsRead` | `(msg: T) => void` | Mark a specific message as read. Removes it from `messages` (and reduces the global unread badge in the bell dropdown). |
|
|
66
|
+
|
|
67
|
+
## Typed payloads
|
|
68
|
+
|
|
69
|
+
Notification payloads often extend `PushNotification` with domain fields. Pass the type parameter so `onMessage` and `messages` are typed:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import type { PushNotification } from "@vc-shell/framework";
|
|
73
|
+
|
|
74
|
+
interface ImportPushNotification extends PushNotification {
|
|
75
|
+
jobId: string;
|
|
76
|
+
profileId: string;
|
|
77
|
+
profileName?: string;
|
|
78
|
+
processedCount: number;
|
|
79
|
+
errorCount: number;
|
|
80
|
+
finished: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { messages, markAsRead } = useBladeNotifications<ImportPushNotification>({
|
|
84
|
+
types: ["ImportPushNotification"],
|
|
85
|
+
onMessage: (message) => {
|
|
86
|
+
if (message.finished) {
|
|
87
|
+
reload();
|
|
88
|
+
markAsRead(message);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Common patterns
|
|
95
|
+
|
|
96
|
+
### Refresh a list on any matching event
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
useBladeNotifications({
|
|
100
|
+
types: ["OfferDeletedDomainEvent"],
|
|
101
|
+
onMessage: () => reload(),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Drop-in for a list blade that needs to stay in sync with deletions happening anywhere in the app.
|
|
106
|
+
|
|
107
|
+
### Filter to the entity this blade owns
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
useBladeNotifications<ImportPushNotification>({
|
|
111
|
+
types: ["ImportPushNotification"],
|
|
112
|
+
onMessage: (message) => {
|
|
113
|
+
if (message.profileId !== param.value) return; // not our job
|
|
114
|
+
if (!message.finished) updateProgress(message);
|
|
115
|
+
else finalizeImport(message);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Two open import blades will both receive the stream; each one filters by its own `profileId` so they do not step on each other.
|
|
121
|
+
|
|
122
|
+
### Drive a manual progress toast
|
|
123
|
+
|
|
124
|
+
When the platform sends progress updates for a long-running job, you may want to render one persistent toast that you update as messages arrive — instead of letting Level 1 spawn a new toast per event.
|
|
125
|
+
|
|
126
|
+
Set the Level 1 type to `silent` and drive the toast yourself:
|
|
127
|
+
|
|
128
|
+
```ts title="src/modules/import/index.ts"
|
|
129
|
+
defineAppModule({
|
|
130
|
+
notifications: {
|
|
131
|
+
ImportPushNotification: { toast: { mode: "silent" } },
|
|
132
|
+
},
|
|
133
|
+
// ...
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```ts title="pages/import-process.vue"
|
|
138
|
+
import { useBladeNotifications, notification } from "@vc-shell/framework";
|
|
139
|
+
|
|
140
|
+
let toastId: string | undefined;
|
|
141
|
+
|
|
142
|
+
useBladeNotifications<ImportPushNotification>({
|
|
143
|
+
types: ["ImportPushNotification"],
|
|
144
|
+
onMessage: (message) => {
|
|
145
|
+
const content = message.profileName ? `${message.profileName}: ${message.title}` : message.title;
|
|
146
|
+
|
|
147
|
+
if (!toastId) {
|
|
148
|
+
toastId = notification(content, { timeout: false });
|
|
149
|
+
} else if (!message.finished) {
|
|
150
|
+
notification.update(toastId, { content });
|
|
151
|
+
} else {
|
|
152
|
+
notification.update(toastId, {
|
|
153
|
+
content,
|
|
154
|
+
timeout: 5000,
|
|
155
|
+
type: message.errorCount ? "error" : "success",
|
|
156
|
+
onClose: () => (toastId = undefined),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The `notification()` helper returns the toast id; `notification.update` mutates it in place. The bell-dropdown history still grows — `silent` only suppresses the auto-toast.
|
|
164
|
+
|
|
165
|
+
## Lifecycle
|
|
166
|
+
|
|
167
|
+
`useBladeNotifications` calls `useNotificationStore().subscribe(...)` and registers `onScopeDispose(unsub)` against the current effect scope. Inside a Vue `setup()` (component or `<script setup>`) the scope is the component's; the subscription dies with the component.
|
|
168
|
+
|
|
169
|
+
If you call the composable from a manually managed `effectScope()`, the cleanup runs when that scope is stopped. Calling it outside any scope is a bug — the subscription would never be released.
|
|
170
|
+
|
|
171
|
+
## Tips
|
|
172
|
+
|
|
173
|
+
- **Listen, do not declare.** `useBladeNotifications` does not register the notification type with the framework. Types must already be declared by some module via `defineAppModule({ notifications })`, otherwise nothing reaches `onMessage`.
|
|
174
|
+
- **`messages` shows only unread.** `markAsRead(msg)` removes a message from `messages` (and from the global unread count). The notification stays in history.
|
|
175
|
+
- **One subscription per call.** Calling `useBladeNotifications` multiple times in the same blade creates independent subscriptions. Combine handlers if you only need one.
|
|
176
|
+
- **Type strings are case-sensitive.** The string in `types` must exactly equal the `notifyType` field on incoming messages.
|
|
177
|
+
|
|
178
|
+
## Related
|
|
179
|
+
|
|
180
|
+
- [useNotificationStore](./useNotificationStore.md) — direct store access for app-shell features (dropdown, badge).
|
|
181
|
+
- [useNotificationContext](./useNotificationContext.md) — read the current notification inside a custom template.
|
|
182
|
+
- [useBroadcastFilter](./useBroadcastFilter.md) — control which `SendSystemEvents` broadcasts reach the store.
|
|
183
|
+
- [Notifications concept page.](../../concepts/notifications.md)
|
|
184
|
+
- [Notifications plugin reference.](../../plugins/notifications.md)
|