@vc-shell/vc-app-skill 2.0.10-pr247.f47cc74 → 2.0.11
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/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/blade-navigation/table-query-state/useTableQueryState.docs.md +95 -0
- package/runtime/knowledge/docs/core/composables/useAppInsights/useAppInsights.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +34 -9
- package/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md +34 -0
- package/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md +1 -1
- 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 +1 -1
- package/runtime/knowledge/docs/modules/assets/assets-details.docs.md +123 -0
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +1 -1
- package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/dashboard-widget-skeleton.docs.md +33 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +67 -10
- package/runtime/knowledge/docs/ui/composables/useDataTablePagination.docs.md +44 -23
- package/runtime/knowledge/docs/ui/composables/useDataTableSort.docs.md +16 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
|
|
5
5
|
"bin": "./bin/install.cjs",
|
|
6
6
|
"files": [
|
package/runtime/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0.
|
|
1
|
+
2.0.11
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit f2225cc27 on 2026-06-22T15:40:33.524Z
|
package/runtime/knowledge/docs/core/blade-navigation/table-query-state/useTableQueryState.docs.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useTableQueryState
|
|
3
|
+
category: composables
|
|
4
|
+
group: data
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# useTableQueryState
|
|
8
|
+
|
|
9
|
+
Reads the table view state (`sort`, `search`, `page`) that `VcDataTable` persists to the URL query for URL-addressable blades. A list page calls `read()` in `setup` to seed its own refs from the restored state before its loader runs, so a reload makes one request.
|
|
10
|
+
|
|
11
|
+
Read-only: writing the view state to the URL stays inside `VcDataTable`. This composable only exposes the restored values to the page.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- A list blade is URL-addressable (workspace or routable) and uses `VcDataTable` with a `state-key`
|
|
16
|
+
- You want a single, coordinated load on reload (sort + search + page restored together)
|
|
17
|
+
- You are wiring the page's combined loader watcher and need the restored seed values
|
|
18
|
+
|
|
19
|
+
## When NOT to Use
|
|
20
|
+
|
|
21
|
+
- The table is standalone or its blade has no URL — there is nothing to restore (`read()` returns `{}`)
|
|
22
|
+
- You only need the live view state — that already lives in your own `sortField`/`searchValue`/`currentPage` refs
|
|
23
|
+
- You want to _write_ to the URL — that is automatic; do not persist manually
|
|
24
|
+
|
|
25
|
+
## Basic Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { ref, watch } from "vue";
|
|
29
|
+
import { useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
|
|
30
|
+
|
|
31
|
+
const PAGE_SIZE = 20;
|
|
32
|
+
const { debounce } = useFunctions();
|
|
33
|
+
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
34
|
+
initialField: "createdDate",
|
|
35
|
+
initialDirection: "DESC",
|
|
36
|
+
});
|
|
37
|
+
const searchValue = ref<string>();
|
|
38
|
+
const currentPage = ref(1);
|
|
39
|
+
|
|
40
|
+
// Seed refs from the URL before the loader runs.
|
|
41
|
+
const restored = useTableQueryState("offers_list").read();
|
|
42
|
+
if (restored.sort) {
|
|
43
|
+
const [field, dir] = restored.sort.split(":");
|
|
44
|
+
sortField.value = field;
|
|
45
|
+
sortOrder.value = dir === "DESC" ? -1 : 1;
|
|
46
|
+
}
|
|
47
|
+
if (restored.search) searchValue.value = restored.search;
|
|
48
|
+
if (restored.page) currentPage.value = restored.page;
|
|
49
|
+
|
|
50
|
+
function load() {
|
|
51
|
+
return loadItems({
|
|
52
|
+
sort: sortExpression.value,
|
|
53
|
+
keyword: searchValue.value || undefined,
|
|
54
|
+
skip: (currentPage.value - 1) * PAGE_SIZE,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
load();
|
|
59
|
+
watch(searchValue, () => (currentPage.value = 1));
|
|
60
|
+
watch([sortExpression, searchValue, currentPage], debounce(load, 300));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
### Parameters
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Default | Description |
|
|
68
|
+
| ---------- | --------------------- | ----------- | ------------------------------------------------------------------------- |
|
|
69
|
+
| `stateKey` | `string \| undefined` | `undefined` | The same `state-key` passed to `VcDataTable` for this table (namespacing) |
|
|
70
|
+
|
|
71
|
+
### Returns
|
|
72
|
+
|
|
73
|
+
| Property | Type | Description |
|
|
74
|
+
| -------- | ----------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
75
|
+
| `read` | `() => TableQueryPatch` | Reads the restored `{ sort?, search?, page? }` from the URL. Returns `{}` when no service is available |
|
|
76
|
+
|
|
77
|
+
`TableQueryPatch`: `{ sort?: string; search?: string; page?: number }` — `sort` is a `"field:ASC"` / `"field:DESC"` expression, `page` is 1-based.
|
|
78
|
+
|
|
79
|
+
## Details
|
|
80
|
+
|
|
81
|
+
- **Read in `setup`**: `VcDataTable` does not emit restore events on init; the page reads the state and seeds its refs in `setup`. This avoids a separate load per restored field.
|
|
82
|
+
- **No-op without a service**: When no blade provides the persistence service (standalone table, non-URL blade), `read()` returns `{}`, so you can call it unconditionally.
|
|
83
|
+
- **Write-back is automatic**: View state → URL (debounced `router.replace`) is handled by `VcDataTable` via `useTableQueryPersistence`. Pages do not persist manually.
|
|
84
|
+
|
|
85
|
+
## Tips
|
|
86
|
+
|
|
87
|
+
- Call `read()` once, synchronously, before the loader. Seeding after it runs causes an extra load.
|
|
88
|
+
- Use `useDataTablePagination`'s `setPage(n)` to seed the page without firing `onPageChange`.
|
|
89
|
+
- Pass the same `state-key` you give `VcDataTable`, so the URL keys match.
|
|
90
|
+
|
|
91
|
+
## Related
|
|
92
|
+
|
|
93
|
+
- [`useDataTableSort`](../../../ui/composables/useDataTableSort.docs.md) — sort state composable for `VcDataTable`
|
|
94
|
+
- [`useDataTablePagination`](../../../ui/composables/useDataTablePagination.docs.md) — pagination state composable (`setPage` for restore seed)
|
|
95
|
+
- `VcDataTable` → _URL query persistence_ — the write-back side of this contract
|
|
@@ -115,6 +115,6 @@ This results in page names like `[Operations Console] Dashboard` instead of just
|
|
|
115
115
|
## Related
|
|
116
116
|
|
|
117
117
|
- `vue3-application-insights` -- underlying plugin that initializes the Application Insights SDK
|
|
118
|
-
-
|
|
118
|
+
- `useUserManagement` -- provides current user context for telemetry enrichment
|
|
119
119
|
- [useErrorHandler](../useErrorHandler/useErrorHandler.docs.md) -- automatically tracks exceptions to Application Insights
|
|
120
120
|
- [useWebVitals](../useWebVitals/useWebVitals.docs.md) -- Core Web Vitals can be piped to Application Insights via a custom callback
|
|
@@ -103,15 +103,16 @@ Without the generic, `options.value` defaults to `Record<string, unknown> | unde
|
|
|
103
103
|
|
|
104
104
|
These are reactive `ComputedRef` values that reflect the current blade's state. Accessing them outside blade context throws a runtime error.
|
|
105
105
|
|
|
106
|
-
| Property
|
|
107
|
-
|
|
|
108
|
-
| `id`
|
|
109
|
-
| `param`
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
106
|
+
| Property | Type | Description |
|
|
107
|
+
| ------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `id` | `ComputedRef<string>` | The current blade's unique ID within the blade stack |
|
|
109
|
+
| `param` | `ComputedRef<string \| undefined>` | Route parameter passed when the blade was opened |
|
|
110
|
+
| `activeChildParam` | `ComputedRef<string \| undefined>` | The `param` of the direct child blade opened from this blade (the entity currently shown in the details pane), or `undefined`. Reactive and reload-safe. Bind it to a list table's `:active-item-id` to highlight the open entity's row — it stays correct across reloads, unlike a local ref. |
|
|
111
|
+
| `options` | `ComputedRef<TOptions \| undefined>` | Additional options object passed to the blade. Type is `Record<string, unknown>` by default; provide a generic to get a typed ref: `useBlade<MyOptions>()` |
|
|
112
|
+
| `query` | `ComputedRef<Record<string, string> \| undefined>` | URL query parameters scoped to this blade |
|
|
113
|
+
| `closable` | `ComputedRef<boolean>` | `true` when this blade has a parent (i.e., can be closed) |
|
|
114
|
+
| `expanded` | `ComputedRef<boolean>` | `true` when this blade is the active (rightmost) blade |
|
|
115
|
+
| `name` | `ComputedRef<string>` | The blade's registered component name |
|
|
115
116
|
|
|
116
117
|
#### Navigation Methods (work everywhere)
|
|
117
118
|
|
|
@@ -387,6 +388,30 @@ onDeactivated(() => {
|
|
|
387
388
|
});
|
|
388
389
|
```
|
|
389
390
|
|
|
391
|
+
### Highlight the row of the open child blade
|
|
392
|
+
|
|
393
|
+
Bind `activeChildParam` to the table's `active-item-id` so the row of the currently open details blade is highlighted — including after a page reload:
|
|
394
|
+
|
|
395
|
+
```vue
|
|
396
|
+
<script setup lang="ts">
|
|
397
|
+
const { openBlade, activeChildParam } = useBlade();
|
|
398
|
+
|
|
399
|
+
function onRowClick(e: { data: { id: string } }) {
|
|
400
|
+
openBlade({ name: "Details", param: e.data.id });
|
|
401
|
+
}
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<template>
|
|
405
|
+
<VcDataTable
|
|
406
|
+
:items="items"
|
|
407
|
+
:active-item-id="activeChildParam"
|
|
408
|
+
@row-click="onRowClick"
|
|
409
|
+
>
|
|
410
|
+
<!-- columns -->
|
|
411
|
+
</VcDataTable>
|
|
412
|
+
</template>
|
|
413
|
+
```
|
|
414
|
+
|
|
390
415
|
### Error Management
|
|
391
416
|
|
|
392
417
|
Display or clear error banners on the blade:
|
package/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: usePlatformLocaleSync
|
|
3
|
+
category: composables
|
|
4
|
+
group: user
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# usePlatformLocaleSync
|
|
8
|
+
|
|
9
|
+
One-way reactive bridge from the VirtoCommerce platform's locale storage key (`NG_TRANSLATE_LANG_KEY`, set by AngularJS + angular-translate) to the shell's language service.
|
|
10
|
+
|
|
11
|
+
Call this composable only when the shell runs embedded inside the platform — `useShellBootstrap` invokes it automatically when `options.isEmbedded === true`. In standalone mode the shell owns its own locale via `VC_LANGUAGE_SETTINGS`, and this composable should not be used.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Never call directly from feature code. This is a framework-internal sync primitive.
|
|
16
|
+
- It is invoked once per `VcApp` mount from `useShellBootstrap`.
|
|
17
|
+
|
|
18
|
+
## Behaviour
|
|
19
|
+
|
|
20
|
+
- Reads `localStorage["NG_TRANSLATE_LANG_KEY"]` via VueUse's `useLocalStorage`, which subscribes to `storage` events for cross-tab reactivity.
|
|
21
|
+
- On setup, if the value is non-empty, calls `LanguageService.setLocale(value)`. `setLocale` normalises the value (e.g. `en-US` → `en-us`), falls back to `en` for unsupported locales, updates `vue-i18n`, reconfigures `vee-validate`, and persists to `VC_LANGUAGE_SETTINGS`.
|
|
22
|
+
- On subsequent changes of the platform key, re-applies the value.
|
|
23
|
+
- Skips empty strings (platform clearing the key does not blank the shell locale).
|
|
24
|
+
- Skips values equal to `currentLocale` to avoid redundant re-configuration.
|
|
25
|
+
|
|
26
|
+
## How It Works
|
|
27
|
+
|
|
28
|
+
`useLocalStorage("NG_TRANSLATE_LANG_KEY", "")` returns a `Ref<string>` that VueUse keeps in sync with `localStorage` and the DOM `storage` event (which fires in tabs other than the writer). The composable applies the current ref value once synchronously and then registers a `watch` on it; any cross-tab mutation flows through the ref into `setLocale`.
|
|
29
|
+
|
|
30
|
+
The watcher is bound to the active effect scope (typically `VcApp`'s setup). When `VcApp` unmounts, the watcher stops; `useLocalStorage` cleans up its own `storage` listener.
|
|
31
|
+
|
|
32
|
+
## Relationship to `VC_LANGUAGE_SETTINGS`
|
|
33
|
+
|
|
34
|
+
The sync is strictly one-directional. `setLocale` writes to `VC_LANGUAGE_SETTINGS` as a side effect, but this composable never writes to `NG_TRANSLATE_LANG_KEY`. In embedded mode the in-shell `LanguageSelector` is unreachable (it lives inside `UserDropdownButton`, which is hidden when `isEmbedded` is `true`), so there is no competing writer from the shell side.
|
|
@@ -200,7 +200,7 @@ const { isMobile } = useResponsive();
|
|
|
200
200
|
|
|
201
201
|
## Migration
|
|
202
202
|
|
|
203
|
-
See [migration guide #36](
|
|
203
|
+
See [migration guide #36](https://github.com/VirtoCommerce/vc-shell/blob/main/migration/36-use-responsive.md) for automated codemod and manual migration instructions.
|
|
204
204
|
|
|
205
205
|
## Related
|
|
206
206
|
|
|
@@ -167,5 +167,5 @@ trackRequest("my-request-2");
|
|
|
167
167
|
## Related
|
|
168
168
|
|
|
169
169
|
- [`useConnectionStatus`](../useConnectionStatus/useConnectionStatus.docs.md) — offline detection (binary online/offline)
|
|
170
|
-
- [`registerInterceptors`](
|
|
170
|
+
- [`registerInterceptors`](https://github.com/VirtoCommerce/vc-shell/blob/main/framework/core/interceptors/index.ts) — the fetch wrapper that calls `trackRequest`/`untrackRequest`
|
|
171
171
|
- `notification` from `@shared/components/notifications` — the notification system used to display the warning
|
|
@@ -173,6 +173,6 @@ async function fetchCustomEndpoint(url: string) {
|
|
|
173
173
|
|
|
174
174
|
## Related
|
|
175
175
|
|
|
176
|
-
-
|
|
176
|
+
- `useUserManagement` -- extended API with sign-in, password reset, token validation (for auth pages)
|
|
177
177
|
- [usePermissions](../usePermissions/) -- permission checks based on user roles
|
|
178
178
|
- `UserDetail` from `@core/api/platform` -- the user detail type returned by the API
|
|
@@ -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)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useBroadcastFilter
|
|
3
|
+
category: composables
|
|
4
|
+
group: notifications
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# useBroadcastFilter
|
|
8
|
+
|
|
9
|
+
Controls which **broadcast** push notifications reach the store. The platform SignalR hub delivers messages through two channels: `Send` (targeted to a specific user) and `SendSystemEvents` (broadcast to everyone connected). Broadcasts run through the filter installed here; targeted messages are always accepted.
|
|
10
|
+
|
|
11
|
+
This is the standard mechanism for scoping a multi-tenant app — show a seller only the broadcasts that mention them, hide events from other tenants. Without a filter every broadcast lands in every user's history.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- Multi-tenant apps where the same broadcast topic carries events for different tenants (sellers, organizations, departments) and each user should only see their slice.
|
|
16
|
+
- Apps that want to drop noisy `IndexProgressPushNotification` or similar maintenance events for non-admin roles.
|
|
17
|
+
- When NOT to use: filtering targeted notifications. The platform already delivers `Send` messages only to the addressed user; `useBroadcastFilter` does not see them.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { useBroadcastFilter, useUser } from "@vc-shell/framework";
|
|
23
|
+
import { onMounted } from "vue";
|
|
24
|
+
|
|
25
|
+
const { user } = useUser();
|
|
26
|
+
const { setBroadcastFilter } = useBroadcastFilter();
|
|
27
|
+
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
setBroadcastFilter((msg) => msg.creator === user.value?.userName);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install the filter once at app bootstrap (or whenever the active user changes). Every incoming broadcast is run through it; messages that return `false` are dropped before they touch history, toasts, or subscribers.
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### Returns
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
interface UseBroadcastFilterReturn {
|
|
41
|
+
setBroadcastFilter(fn: (msg: PushNotification) => boolean): void;
|
|
42
|
+
clearBroadcastFilter(): void;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Method | Type | Description |
|
|
47
|
+
| ---------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
48
|
+
| `setBroadcastFilter` | `((msg: PushNotification) => boolean) => void` | Install the filter. Replaces any previous filter — there is at most one active at a time. |
|
|
49
|
+
| `clearBroadcastFilter` | `() => void` | Remove the filter. All subsequent broadcasts are accepted. |
|
|
50
|
+
|
|
51
|
+
The filter returns `true` to **accept** a message, `false` to **drop** it.
|
|
52
|
+
|
|
53
|
+
## Common patterns
|
|
54
|
+
|
|
55
|
+
### Scope by current user
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
setBroadcastFilter((msg) => msg.creator === user.value?.userName);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`creator` is the user that originated the event on the platform side. This is the canonical "show me my own broadcasts" filter in multi-tenant back-office apps.
|
|
64
|
+
|
|
65
|
+
### Scope by tenant id
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
onMounted(() => {
|
|
69
|
+
setBroadcastFilter((msg) => (msg as TenantPush).sellerId === currentSellerId.value);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
When your broadcast payloads carry a tenant id, gate on it instead of `creator`. The cast clarifies typing without expanding `PushNotification` for every caller.
|
|
74
|
+
|
|
75
|
+
### Re-install on user switch
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { watch } from "vue";
|
|
79
|
+
|
|
80
|
+
watch(
|
|
81
|
+
() => user.value?.userName,
|
|
82
|
+
(name) => {
|
|
83
|
+
if (!name) clearBroadcastFilter();
|
|
84
|
+
else setBroadcastFilter((msg) => msg.creator === name);
|
|
85
|
+
},
|
|
86
|
+
{ immediate: true },
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If the app supports user switching without a full reload (impersonation, multi-account), re-install the filter on every change. There is only one slot — installing again replaces the previous filter.
|
|
91
|
+
|
|
92
|
+
### Drop a noisy type entirely
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
setBroadcastFilter((msg) => msg.notifyType !== "IndexProgressPushNotification");
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Broadcast-only suppression. To suppress targeted messages too, set `toast: false` or `toast: { mode: "silent" }` on the type's `defineAppModule({ notifications })` config — that controls the toast surface; the history still records the event.
|
|
99
|
+
|
|
100
|
+
## Behavior
|
|
101
|
+
|
|
102
|
+
- The filter applies only to messages ingested with the `broadcast: true` flag (the SignalR `SendSystemEvents` channel).
|
|
103
|
+
- Targeted messages (`Send`) bypass the filter entirely.
|
|
104
|
+
- Installing a filter mid-session does not retroactively prune `history` or `realtime`. Past broadcasts stay; only future ones are filtered.
|
|
105
|
+
- The filter is a single function. To compose multiple predicates, `&&` them inside one callback.
|
|
106
|
+
|
|
107
|
+
## Tips
|
|
108
|
+
|
|
109
|
+
- **Install once, early.** Setting the filter in `App.vue` `onMounted` (after authentication) is the canonical placement, so messages arriving before the first blade mounts are already scoped.
|
|
110
|
+
- **Filter exceptions go straight to the console.** If your predicate throws, the message is dropped. Wrap the logic if you are reading off potentially missing fields.
|
|
111
|
+
- **Do not query the store from inside the filter.** The store is `useBroadcastFilter`'s parent — calling back into it during ingestion causes re-entrancy.
|
|
112
|
+
|
|
113
|
+
## Related
|
|
114
|
+
|
|
115
|
+
- [useNotificationStore](./useNotificationStore.md) — exposes the same set/clear methods plus the rest of the store API.
|
|
116
|
+
- [useBladeNotifications](./useBladeNotifications.md) — blade-scoped subscription that sees broadcasts after filtering.
|
|
117
|
+
- [Notifications concept page — broadcasts.](../../concepts/notifications.md#broadcast-vs-targeted)
|