@vc-shell/vc-app-skill 2.0.10 → 2.0.11-pr247.0f1e741
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/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/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/notifications.docs.md +1 -1
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +1 -1
- 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/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md +0 -34
- package/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md +0 -184
- package/runtime/knowledge/docs/core/notifications/composables/useBroadcastFilter.docs.md +0 -117
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationContext.docs.md +0 -150
- package/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md +0 -113
- package/runtime/knowledge/docs/modules/assets/assets-details.docs.md +0 -123
- package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/dashboard-widget-skeleton.docs.md +0 -33
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-pr247.0f1e741",
|
|
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
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -207,7 +207,7 @@ store.registerType("CatalogExportCompleted", {
|
|
|
207
207
|
});
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
Custom templates access the notification via `useNotificationContext()` composable — no props needed. See
|
|
210
|
+
Custom templates access the notification via `useNotificationContext()` composable — no props needed. See the `NotificationTemplate` component for template examples.
|
|
211
211
|
|
|
212
212
|
### Subscribing in a blade
|
|
213
213
|
|
|
@@ -35,7 +35,7 @@ The blade reads its configuration from `options` via `useBlade<AssetsManagerOpti
|
|
|
35
35
|
| `disabled` | `boolean?` | When true, hides upload/delete actions and disables reordering |
|
|
36
36
|
| `hiddenFields` | `string[]?` | Fields to hide in the detail view |
|
|
37
37
|
|
|
38
|
-
> **Breaking change:** The old options (`assets`, `loading`, `assetsUploadHandler`, `assetsEditHandler`, `assetsRemoveHandler`) have been removed. Pass a single `manager` instance instead. See [migration guide #32](
|
|
38
|
+
> **Breaking change:** The old options (`assets`, `loading`, `assetsUploadHandler`, `assetsEditHandler`, `assetsRemoveHandler`) have been removed. Pass a single `manager` instance instead. See [migration guide #32](https://github.com/VirtoCommerce/vc-shell/blob/main/migration/32-use-assets-manager.md).
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
@@ -376,6 +376,8 @@ For server-side "select all" that includes items not currently visible:
|
|
|
376
376
|
</VcDataTable>
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
+
> **Opt-in.** The "Select all N items" banner is shown only when you bind `selectAllActive` (via `v-model:selectAllActive` or a one-way `:selectAllActive`). Its default is `undefined`, so a table that passes `totalCount` only for pagination never surfaces the banner.
|
|
380
|
+
|
|
379
381
|
```ts
|
|
380
382
|
const allSelected = ref(false);
|
|
381
383
|
|
|
@@ -1030,7 +1032,7 @@ async function loadNextPage() {
|
|
|
1030
1032
|
!!! tip "Use unique state keys"
|
|
1031
1033
|
Every table in your application must have a distinct `state-key`. Two tables sharing the same key will silently overwrite each other's persisted column widths, order, and hidden/shown column lists.
|
|
1032
1034
|
|
|
1033
|
-
Persist column widths, column order, and column visibility across page reloads.
|
|
1035
|
+
Persist column widths, column order, and column visibility across page reloads. Column layout is stored in `localStorage`/`sessionStorage` (keyed by `state-key`). Sort, search, and the current page are persisted separately — to the **URL query** — automatically when the table is rendered inside a URL-addressable blade (see [URL query persistence](#url-query-persistence) below). Selection, column filters, and other transient state are session-scoped and are deliberately excluded from any persistence:
|
|
1034
1036
|
|
|
1035
1037
|
```vue
|
|
1036
1038
|
<VcDataTable :items="products" state-key="product-list">
|
|
@@ -1063,6 +1065,61 @@ Listen to state events:
|
|
|
1063
1065
|
|
|
1064
1066
|
> **Tip:** Each table in your application should have a unique `state-key`. If two tables share the same key, they will overwrite each other's persisted state.
|
|
1065
1067
|
|
|
1068
|
+
### URL query persistence
|
|
1069
|
+
|
|
1070
|
+
When a table is rendered inside a URL-addressable blade (a workspace or routable blade whose address appears in the address bar), its sort, search, and current page are persisted to the URL query string. Reloading the page or sharing the link restores that view.
|
|
1071
|
+
|
|
1072
|
+
Writing the view state to the URL is automatic — you do not wire it up. Tables outside a blade (or in a blade with no URL) do not persist (no-op). Reading it back on reload is done by the page (see below).
|
|
1073
|
+
|
|
1074
|
+
- **Namespacing:** query keys are prefixed with the table's `state-key` if set, otherwise with the blade's URL segment — e.g. `?offers_sort=name:DESC&offers_search=foo&offers_page=2`. Give each table a unique `state-key` when several tables can be visible at once (the same rule as column-layout persistence).
|
|
1075
|
+
- **Keys:** `<ns>_sort` (`field:ASC` / `field:DESC`), `<ns>_search`, `<ns>_page` (1-based).
|
|
1076
|
+
- **History:** changes use `router.replace`, so they do not add browser-history entries.
|
|
1077
|
+
- **Scope:** only sort, search, and page. Column layout still uses `localStorage`/`sessionStorage` (above); column filters and selection are not persisted.
|
|
1078
|
+
|
|
1079
|
+
The page reads the persisted values with `useTableQueryState(stateKey)`, seeds its sort/search/page refs in `setup`, and loads once. The table does not push the restored values back into the page, so a reload runs a single request rather than one per restored field.
|
|
1080
|
+
|
|
1081
|
+
```ts
|
|
1082
|
+
import { ref, watch } from "vue";
|
|
1083
|
+
import { useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
|
|
1084
|
+
|
|
1085
|
+
const PAGE_SIZE = 20;
|
|
1086
|
+
const { debounce } = useFunctions();
|
|
1087
|
+
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
1088
|
+
initialField: "createdDate",
|
|
1089
|
+
initialDirection: "DESC",
|
|
1090
|
+
});
|
|
1091
|
+
const searchValue = ref<string>();
|
|
1092
|
+
const currentPage = ref(1);
|
|
1093
|
+
|
|
1094
|
+
// Restore from the URL, then load once.
|
|
1095
|
+
const restored = useTableQueryState("offers_list").read();
|
|
1096
|
+
if (restored.sort) {
|
|
1097
|
+
const [field, dir] = restored.sort.split(":");
|
|
1098
|
+
sortField.value = field;
|
|
1099
|
+
sortOrder.value = dir === "DESC" ? -1 : 1;
|
|
1100
|
+
}
|
|
1101
|
+
if (restored.search) searchValue.value = restored.search;
|
|
1102
|
+
if (restored.page) currentPage.value = restored.page;
|
|
1103
|
+
|
|
1104
|
+
function load() {
|
|
1105
|
+
return loadItems({
|
|
1106
|
+
sort: sortExpression.value,
|
|
1107
|
+
keyword: searchValue.value || undefined,
|
|
1108
|
+
skip: (currentPage.value - 1) * PAGE_SIZE,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
load();
|
|
1113
|
+
watch(searchValue, () => (currentPage.value = 1));
|
|
1114
|
+
watch([sortExpression, searchValue, currentPage], debounce(load, 300));
|
|
1115
|
+
|
|
1116
|
+
function onPaginationClick(page: number) {
|
|
1117
|
+
currentPage.value = page;
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
If a blade cannot be reopened on reload (e.g. a non-routable blade, or an intermediate blade that is not on the restored URL path), its table query params have no owner — they are automatically dropped from the URL once the restored blades have mounted, so the address bar never accumulates stale query keys.
|
|
1122
|
+
|
|
1066
1123
|
---
|
|
1067
1124
|
|
|
1068
1125
|
## Row Actions
|
|
@@ -1345,15 +1402,15 @@ function onRowRemove(event: { data: Product; index: number; cancel: () => void }
|
|
|
1345
1402
|
|
|
1346
1403
|
### Selection
|
|
1347
1404
|
|
|
1348
|
-
| Prop | Type | Default
|
|
1349
|
-
| -------------------- | ------------------------ |
|
|
1350
|
-
| `selection` | `T \| T[]` | --
|
|
1351
|
-
| `selectionMode` | `"single" \| "multiple"` | --
|
|
1352
|
-
| `isRowSelectable` | `(data: T) => boolean` | --
|
|
1353
|
-
| `compareSelectionBy` | `"equals" \| "field"` | --
|
|
1354
|
-
| `selectAll` | `boolean` | `false`
|
|
1355
|
-
| `selectAllActive` | `boolean` | `
|
|
1356
|
-
| `activeItemId` | `string` | --
|
|
1405
|
+
| Prop | Type | Default | Description |
|
|
1406
|
+
| -------------------- | ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1407
|
+
| `selection` | `T \| T[]` | -- | Selected item(s). Use with `v-model:selection`. |
|
|
1408
|
+
| `selectionMode` | `"single" \| "multiple"` | -- | Row selection mode. |
|
|
1409
|
+
| `isRowSelectable` | `(data: T) => boolean` | -- | Per-row function to disable selection. |
|
|
1410
|
+
| `compareSelectionBy` | `"equals" \| "field"` | -- | Compare items by deep equality or by `dataKey` field. |
|
|
1411
|
+
| `selectAll` | `boolean` | `false` | Enable "select all" header checkbox. |
|
|
1412
|
+
| `selectAllActive` | `boolean` | `undefined` | "Select all" (including non-visible items) active state. Binding it (e.g. `v-model:selectAllActive`) opts the table into the cross-page "Select all N items" banner; left unbound, the banner never appears. |
|
|
1413
|
+
| `activeItemId` | `string` | -- | ID of the highlighted row. Use with `v-model:activeItemId`. |
|
|
1357
1414
|
|
|
1358
1415
|
### Sorting
|
|
1359
1416
|
|
|
@@ -29,7 +29,6 @@ import { useDataTablePagination } from "@vc-shell/framework";
|
|
|
29
29
|
const pagination = useDataTablePagination({
|
|
30
30
|
pageSize: 20,
|
|
31
31
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
32
|
-
onPageChange: ({ skip }) => loadItems({ ...searchQuery.value, skip }),
|
|
33
32
|
});
|
|
34
33
|
```
|
|
35
34
|
|
|
@@ -37,6 +36,8 @@ const pagination = useDataTablePagination({
|
|
|
37
36
|
<VcDataTable :items="items" :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
38
37
|
```
|
|
39
38
|
|
|
39
|
+
> **Note:** Let `@pagination-click` only update the page (`goToPage`) and load from one watcher that reads `pagination.skip` together with sort and search — see the recipe below. `onPageChange` (load on each page change) still works for simple tables, but do not combine it with that watcher or the page loads twice.
|
|
40
|
+
|
|
40
41
|
## API
|
|
41
42
|
|
|
42
43
|
### Parameters (Options)
|
|
@@ -49,15 +50,16 @@ const pagination = useDataTablePagination({
|
|
|
49
50
|
|
|
50
51
|
### Returns (`reactive()` object)
|
|
51
52
|
|
|
52
|
-
| Property | Type | Description
|
|
53
|
-
| ------------- | ------------------------ |
|
|
54
|
-
| `currentPage` | `number` | Current 1-based page number (writable)
|
|
55
|
-
| `pages` | `number` (readonly) | Total number of pages
|
|
56
|
-
| `skip` | `number` (readonly) | Current skip offset for API calls
|
|
57
|
-
| `pageSize` | `number` (readonly) | Resolved page size
|
|
58
|
-
| `totalCount` | `number` (readonly) | Resolved total item count
|
|
59
|
-
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange`
|
|
60
|
-
| `
|
|
53
|
+
| Property | Type | Description |
|
|
54
|
+
| ------------- | ------------------------ | ----------------------------------------------------------------------- |
|
|
55
|
+
| `currentPage` | `number` | Current 1-based page number (writable) |
|
|
56
|
+
| `pages` | `number` (readonly) | Total number of pages |
|
|
57
|
+
| `skip` | `number` (readonly) | Current skip offset for API calls |
|
|
58
|
+
| `pageSize` | `number` (readonly) | Resolved page size |
|
|
59
|
+
| `totalCount` | `number` (readonly) | Resolved total item count |
|
|
60
|
+
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange` |
|
|
61
|
+
| `setPage` | `(page: number) => void` | Seed the page WITHOUT firing `onPageChange` — use for pull-restore seed |
|
|
62
|
+
| `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
|
|
61
63
|
|
|
62
64
|
All properties are auto-unwrapped by `reactive()` — no `.value` access needed in script or template.
|
|
63
65
|
|
|
@@ -65,30 +67,43 @@ All properties are auto-unwrapped by `reactive()` — no `.value` access needed
|
|
|
65
67
|
|
|
66
68
|
```vue
|
|
67
69
|
<script setup lang="ts">
|
|
68
|
-
import { ref, computed } from "vue";
|
|
69
|
-
import { useDataTablePagination, useDataTableSort } from "@vc-shell/framework";
|
|
70
|
+
import { ref, computed, watch } from "vue";
|
|
71
|
+
import { useDataTablePagination, useDataTableSort, useTableQueryState } from "@vc-shell/framework";
|
|
70
72
|
|
|
71
73
|
// Data layer
|
|
72
74
|
const searchResult = ref<{ results: Item[]; totalCount: number }>();
|
|
73
|
-
const searchQuery = ref({ take: 20, skip: 0 });
|
|
74
75
|
|
|
75
|
-
async function loadItems(query?:
|
|
76
|
-
|
|
77
|
-
searchResult.value = await api.search(searchQuery.value);
|
|
76
|
+
async function loadItems(query: { sort?: string; keyword?: string; skip?: number }) {
|
|
77
|
+
searchResult.value = await api.search({ take: 20, ...query });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// State only — no self-loading callback.
|
|
81
81
|
const pagination = useDataTablePagination({
|
|
82
82
|
pageSize: 20,
|
|
83
83
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
84
|
-
onPageChange: ({ skip }) => loadItems({ skip }),
|
|
85
84
|
});
|
|
86
|
-
|
|
87
|
-
// Sort state
|
|
88
85
|
const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
89
86
|
initialField: "createdDate",
|
|
90
87
|
initialDirection: "DESC",
|
|
91
88
|
});
|
|
89
|
+
const searchValue = ref<string>();
|
|
90
|
+
|
|
91
|
+
// Restore from the URL, then load once. setPage seeds the page without loading.
|
|
92
|
+
const restored = useTableQueryState("product-list").read();
|
|
93
|
+
if (restored.sort) {
|
|
94
|
+
const [field, dir] = restored.sort.split(":");
|
|
95
|
+
sortField.value = field;
|
|
96
|
+
sortOrder.value = dir === "DESC" ? -1 : 1;
|
|
97
|
+
}
|
|
98
|
+
if (restored.search) searchValue.value = restored.search;
|
|
99
|
+
if (restored.page) pagination.setPage(restored.page);
|
|
100
|
+
|
|
101
|
+
// One loader reading sort + search + page.
|
|
102
|
+
watch(
|
|
103
|
+
() => ({ sort: sortExpression.value, keyword: searchValue.value || undefined, skip: pagination.skip }),
|
|
104
|
+
(query) => loadItems(query),
|
|
105
|
+
{ immediate: true },
|
|
106
|
+
);
|
|
92
107
|
</script>
|
|
93
108
|
|
|
94
109
|
<template>
|
|
@@ -97,6 +112,12 @@ const { sortField, sortOrder, sortExpression } = useDataTableSort({
|
|
|
97
112
|
:total-count="pagination.totalCount"
|
|
98
113
|
:pagination="pagination"
|
|
99
114
|
@pagination-click="pagination.goToPage"
|
|
115
|
+
@search="
|
|
116
|
+
(kw) => {
|
|
117
|
+
searchValue = kw;
|
|
118
|
+
pagination.setPage(1);
|
|
119
|
+
}
|
|
120
|
+
"
|
|
100
121
|
v-model:sort-field="sortField"
|
|
101
122
|
v-model:sort-order="sortOrder"
|
|
102
123
|
>
|
|
@@ -130,7 +151,6 @@ export function useOffers() {
|
|
|
130
151
|
const pagination = useDataTablePagination({
|
|
131
152
|
pageSize: 20,
|
|
132
153
|
totalCount: computed(() => searchResult.value?.totalCount ?? 0),
|
|
133
|
-
onPageChange: ({ skip }) => loadOffers({ ...searchQuery.value, skip }),
|
|
134
154
|
});
|
|
135
155
|
|
|
136
156
|
return {
|
|
@@ -141,7 +161,7 @@ export function useOffers() {
|
|
|
141
161
|
}
|
|
142
162
|
```
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
The blade owns the single loader watch (reading `pagination.skip` with sort/search) and binds:
|
|
145
165
|
|
|
146
166
|
```vue
|
|
147
167
|
<VcDataTable :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
|
|
@@ -150,7 +170,8 @@ Blade then simply binds:
|
|
|
150
170
|
## Details
|
|
151
171
|
|
|
152
172
|
- **`reactive()` return**: The composable wraps its return in `reactive()`, so all `Ref`/`ComputedRef` properties are auto-unwrapped. Access with `pagination.pages` (not `pagination.pages.value`). This allows the object to be passed directly as `:pagination` prop to VcDataTable without intermediate conversion.
|
|
153
|
-
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not
|
|
173
|
+
- **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not fetch data. With a single loader watcher you usually omit it and read `pagination.skip` from the watcher instead.
|
|
174
|
+
- **`setPage` vs `goToPage`**: `goToPage(n)` updates the page and fires `onPageChange`; `setPage(n)` updates it without the callback. Use `setPage` to seed the page from a URL restore so the seed itself does not load.
|
|
154
175
|
- **No auto-clamp**: When `totalCount` decreases (e.g. after deletion), `currentPage` is NOT automatically clamped. Call `reset()` or `goToPage(1)` explicitly after mutations.
|
|
155
176
|
- **reset() is silent**: `reset()` sets `currentPage = 1` but does NOT fire `onPageChange`. This prevents accidental double-fetches when the consumer resets pagination during a reload.
|
|
156
177
|
- **Pure without callback**: Omit `onPageChange` and the composable works as pure state — useful for unit tests or when the consumer prefers to watch properties reactively.
|
|
@@ -84,14 +84,16 @@ const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
|
|
|
84
84
|
|
|
85
85
|
const items = ref([]);
|
|
86
86
|
const loading = ref(false);
|
|
87
|
+
const currentPage = ref(1);
|
|
88
|
+
const PAGE_SIZE = 20;
|
|
87
89
|
|
|
88
|
-
async function loadItems() {
|
|
90
|
+
async function loadItems(query: { sort?: string; skip?: number }) {
|
|
89
91
|
loading.value = true;
|
|
90
92
|
try {
|
|
91
93
|
const response = await api.searchProducts({
|
|
92
|
-
sort:
|
|
93
|
-
skip: 0,
|
|
94
|
-
take:
|
|
94
|
+
sort: query.sort, // e.g. "createdDate:DESC" or undefined
|
|
95
|
+
skip: query.skip ?? 0,
|
|
96
|
+
take: PAGE_SIZE,
|
|
95
97
|
});
|
|
96
98
|
items.value = response.results;
|
|
97
99
|
} finally {
|
|
@@ -99,8 +101,13 @@ async function loadItems() {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
+
// Load from one watcher that reads sort together with search and page, so changing
|
|
105
|
+
// several at once is a single request. { immediate: true } also does the first load.
|
|
106
|
+
watch(
|
|
107
|
+
() => ({ sort: sortExpression.value, skip: (currentPage.value - 1) * PAGE_SIZE }),
|
|
108
|
+
(query) => loadItems(query),
|
|
109
|
+
{ immediate: true },
|
|
110
|
+
);
|
|
104
111
|
</script>
|
|
105
112
|
|
|
106
113
|
<template>
|
|
@@ -134,12 +141,13 @@ watch(sortExpression, () => loadItems(), { immediate: true });
|
|
|
134
141
|
- **Sort expression format**: `sortExpression` returns `"field:DIRECTION"` when both `sortField` and a non-zero `sortOrder` are set, otherwise `undefined`. This format is directly compatible with VirtoCommerce Platform search endpoints.
|
|
135
142
|
- **Reset behavior**: `resetSort()` restores `sortField` and `sortOrder` to the values passed at construction time. If no initial options were provided, both are cleared.
|
|
136
143
|
- **Numeric order convention**: `VcDataTable` uses PrimeVue's numeric sort order convention (`1`/`-1`/`0`). This composable encapsulates the mapping so call sites never need to handle the numeric values directly.
|
|
137
|
-
- **Stateless regarding data**: The composable only manages the sort field and direction.
|
|
144
|
+
- **Stateless regarding data**: The composable only manages the sort field and direction. Read `sortExpression` from the same loader watcher that reads search and page, instead of giving sort its own `watch(sortExpression, load)`. See VcDataTable → URL query persistence.
|
|
138
145
|
|
|
139
146
|
## Tips
|
|
140
147
|
|
|
141
148
|
- Always mark columns as `sortable` in your `<VcColumn>` definitions for the sort indicator icon to appear.
|
|
142
|
-
- Use `{ immediate: true }` on the
|
|
149
|
+
- Use `{ immediate: true }` on the combined loader watcher to trigger the initial (or URL-restored) data load with the correct sort on mount.
|
|
150
|
+
- On reload, seed `sortField`/`sortOrder` from `useTableQueryState(stateKey).read().sort` (`"field:DIR"`) before the loader watcher, so restore costs a single request.
|
|
143
151
|
- Call `resetSort()` alongside any "clear all filters" action to keep sort state consistent with the rest of the filter UI.
|
|
144
152
|
|
|
145
153
|
## Related
|
package/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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.
|
|
@@ -1,184 +0,0 @@
|
|
|
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)
|
|
@@ -1,117 +0,0 @@
|
|
|
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)
|
package/runtime/knowledge/docs/core/notifications/composables/useNotificationContext.docs.md
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: useNotificationContext
|
|
3
|
-
category: composables
|
|
4
|
-
group: notifications
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# useNotificationContext
|
|
8
|
-
|
|
9
|
-
Reads the current `PushNotification` payload inside a custom notification template. The framework provides the message via Vue's `inject()` from the dropdown or toast surface that hosts the template — the composable returns a reactive `ComputedRef` over it.
|
|
10
|
-
|
|
11
|
-
This is the one piece you write when a notification type registered with `defineAppModule({ notifications: { Type: { template } } })` needs a richer rendering than the default `NotificationTemplate` chrome — for example, formatting a domain-specific status, deriving a colored accent, or wiring a click handler that opens the relevant blade.
|
|
12
|
-
|
|
13
|
-
## When to use
|
|
14
|
-
|
|
15
|
-
- Implementing the `template` component for a notification type registered through `defineAppModule({ notifications })`.
|
|
16
|
-
- Reading typed payload fields (status, entity name, job id) to drive the template's layout, color, or actions.
|
|
17
|
-
- When NOT to use: anywhere outside a notification template. The composable throws if the inject context is not present.
|
|
18
|
-
|
|
19
|
-
## Quick Start
|
|
20
|
-
|
|
21
|
-
```vue
|
|
22
|
-
<script lang="ts" setup>
|
|
23
|
-
import { PushNotification, NotificationTemplate, useNotificationContext } from "@vc-shell/framework";
|
|
24
|
-
import { computed } from "vue";
|
|
25
|
-
|
|
26
|
-
interface IOrderPushNotification extends PushNotification {
|
|
27
|
-
orderId: string;
|
|
28
|
-
total: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const notificationRef = useNotificationContext<IOrderPushNotification>();
|
|
32
|
-
const notification = computed(() => notificationRef.value);
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<template>
|
|
36
|
-
<NotificationTemplate
|
|
37
|
-
:title="notification.title ?? ''"
|
|
38
|
-
:notification="notification"
|
|
39
|
-
>
|
|
40
|
-
<p>Order {{ notification.orderId }} — ${{ notification.total }}</p>
|
|
41
|
-
</NotificationTemplate>
|
|
42
|
-
</template>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## API
|
|
46
|
-
|
|
47
|
-
### Parameters
|
|
48
|
-
|
|
49
|
-
None. The composable is always called without arguments. Generic type parameter narrows the payload shape:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
function useNotificationContext<T extends PushNotification = PushNotification>(): ComputedRef<T>;
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Returns
|
|
56
|
-
|
|
57
|
-
`ComputedRef<T>` — reactive reference to the current `PushNotification` (or your extended subtype via the generic). Update reactively if the same template instance is reused for a refreshed payload (for example, when a progress message is updated through `notification.update`).
|
|
58
|
-
|
|
59
|
-
## Common patterns
|
|
60
|
-
|
|
61
|
-
### Compute display strings from payload fields
|
|
62
|
-
|
|
63
|
-
```vue
|
|
64
|
-
<script lang="ts" setup>
|
|
65
|
-
import { PushNotification, useNotificationContext, NotificationTemplate } from "@vc-shell/framework";
|
|
66
|
-
import { computed } from "vue";
|
|
67
|
-
import { useI18n } from "vue-i18n";
|
|
68
|
-
|
|
69
|
-
interface IProductPush extends PushNotification {
|
|
70
|
-
productName?: string;
|
|
71
|
-
newStatus?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const ctx = useNotificationContext<IProductPush>();
|
|
75
|
-
const notification = computed(() => ctx.value);
|
|
76
|
-
const { t } = useI18n({ useScope: "global" });
|
|
77
|
-
|
|
78
|
-
const title = computed(() => (notification.value.productName ? `${t("PRODUCTS.PUSH.PRODUCT")} "${notification.value.productName}" ${t("PRODUCTS.PUSH.UPDATE")}` : (notification.value.title ?? "")));
|
|
79
|
-
</script>
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Open a blade on click
|
|
83
|
-
|
|
84
|
-
```vue
|
|
85
|
-
<script lang="ts" setup>
|
|
86
|
-
import { useBlade, useNotificationContext, NotificationTemplate } from "@vc-shell/framework";
|
|
87
|
-
import { computed } from "vue";
|
|
88
|
-
|
|
89
|
-
const { openBlade } = useBlade();
|
|
90
|
-
const ctx = useNotificationContext<IOrderPush>();
|
|
91
|
-
const notification = computed(() => ctx.value);
|
|
92
|
-
|
|
93
|
-
function onClick() {
|
|
94
|
-
if (!notification.value.orderId) return;
|
|
95
|
-
openBlade({ name: "OrderDetails", param: notification.value.orderId });
|
|
96
|
-
}
|
|
97
|
-
</script>
|
|
98
|
-
|
|
99
|
-
<template>
|
|
100
|
-
<NotificationTemplate
|
|
101
|
-
:title="notification.title ?? ''"
|
|
102
|
-
:notification="notification"
|
|
103
|
-
@click="onClick"
|
|
104
|
-
>
|
|
105
|
-
<p>{{ notification.description }}</p>
|
|
106
|
-
</NotificationTemplate>
|
|
107
|
-
</template>
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
`NotificationTemplate` re-emits the click event so the host (dropdown row, toast) can close itself before your handler runs.
|
|
111
|
-
|
|
112
|
-
### Color the template by status
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
const notificationStyle = computed(() => {
|
|
116
|
-
switch (notification.value.newStatus) {
|
|
117
|
-
case "Approved":
|
|
118
|
-
return { color: "var(--success-400)", icon: "lucide-check-circle" };
|
|
119
|
-
case "RequestChanges":
|
|
120
|
-
return { color: "var(--danger-400)", icon: "lucide-alert-circle" };
|
|
121
|
-
case "WaitForApproval":
|
|
122
|
-
return { color: "var(--warning-600)", icon: "lucide-clock" };
|
|
123
|
-
default:
|
|
124
|
-
return { color: "var(--primary-400)", icon: "lucide-bell" };
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
`NotificationTemplate` accepts `:color` and `:icon` props that line up with these computeds — the dropdown row and the toast use the same template, so the styling stays consistent across surfaces.
|
|
130
|
-
|
|
131
|
-
## Where the template runs
|
|
132
|
-
|
|
133
|
-
Notification templates render in two places:
|
|
134
|
-
|
|
135
|
-
- **In the bell dropdown**, as one row in the history list.
|
|
136
|
-
- **As a toast**, when the type's `toast.mode` is `"auto"` or `"progress"` (set the mode to `"silent"` to render only in the dropdown).
|
|
137
|
-
|
|
138
|
-
The template component must be **the same** in both — register it once on `defineAppModule({ notifications })` and the framework reuses it everywhere.
|
|
139
|
-
|
|
140
|
-
## Tips
|
|
141
|
-
|
|
142
|
-
- **Always type the generic.** `useNotificationContext<MyPushType>()` enables autocompletion on payload fields. Without it everything degrades to `PushNotification`.
|
|
143
|
-
- **`computed(() => ctx.value)` is idiomatic** in the example apps because consumers want a regular `Ref` shape to pass into child components and template bindings. Direct access via `ctx.value` is fine too.
|
|
144
|
-
- **Do not subscribe inside a template.** The template renders one message; if you need to react to other notifications, do that in a blade with `useBladeNotifications`.
|
|
145
|
-
|
|
146
|
-
## Related
|
|
147
|
-
|
|
148
|
-
- [useBladeNotifications](./useBladeNotifications.md) — subscribe to types inside a blade.
|
|
149
|
-
- [useNotificationStore](./useNotificationStore.md) — access the underlying store.
|
|
150
|
-
- [Notifications concept page.](../../concepts/notifications.md)
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: useNotificationStore
|
|
3
|
-
category: composables
|
|
4
|
-
group: notifications
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# useNotificationStore
|
|
8
|
-
|
|
9
|
-
!!! warning "Advanced — most apps do not need this"
|
|
10
|
-
Reach for [useBladeNotifications](./useBladeNotifications.md), [useBroadcastFilter](./useBroadcastFilter.md), and the `notifications` option on `defineAppModule` first. The bell dropdown, unread badge, and toast pipeline are already wired by the shell. This composable is an **escape hatch** for the small set of cases where those facades do not fit.
|
|
11
|
-
|
|
12
|
-
Returns the singleton store that backs the framework's notification system. Direct access exposes the full reactive state plus low-level actions: subscribing, ingesting synthetic messages, controlling the broadcast filter, paging history.
|
|
13
|
-
|
|
14
|
-
## When to use
|
|
15
|
-
|
|
16
|
-
There are only two cases where this composable belongs in app code:
|
|
17
|
-
|
|
18
|
-
- **A test or scripted harness** that needs to push synthetic messages through the same pipeline SignalR uses, via `ingest`. Real-time production code never calls `ingest` directly.
|
|
19
|
-
- **A custom shell** that replaces the framework's bell dropdown entirely — typically a module-federation host with its own chrome — and therefore needs to bind to `history`, `unreadCount`, and `markAllAsRead` outside the default surface. Apps that use the standard `VcApp` shell do not need this; the dropdown is already there.
|
|
20
|
-
|
|
21
|
-
For everything else — reacting in a blade, gating broadcasts, registering types, displaying a toast — use the facades. Direct store mutations can bypass invariants the facades enforce (broadcast filter, scope-aware cleanup, type validation).
|
|
22
|
-
|
|
23
|
-
## Quick Start
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { useNotificationStore } from "@vc-shell/framework";
|
|
27
|
-
|
|
28
|
-
const store = useNotificationStore();
|
|
29
|
-
|
|
30
|
-
await store.loadHistory(50);
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## API
|
|
34
|
-
|
|
35
|
-
The store exposes the full reactive state plus actions. The shape is summarized below; the underlying types live in `core/notifications/store.ts` and `core/notifications/types.ts`.
|
|
36
|
-
|
|
37
|
-
| Member | Type | Description |
|
|
38
|
-
| ------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
-
| `registry` | `Map<string, NotificationTypeConfig>` | Notification types registered through `defineAppModule({ notifications })`. |
|
|
40
|
-
| `history` | `Ref<PushNotification[]>` | Full history (server-loaded plus ingested), newest first. |
|
|
41
|
-
| `realtime` | `Ref<PushNotification[]>` | Session-only realtime queue from the SignalR hub. Drives `messages` in `useBladeNotifications`. |
|
|
42
|
-
| `unreadCount` | `ComputedRef<number>` | Count of unread items in `history`. Drives the bell badge. |
|
|
43
|
-
| `hasUnread` | `ComputedRef<boolean>` | Convenience boolean over `unreadCount`. |
|
|
44
|
-
| `registerType(t, cfg)` | `(string, NotificationTypeConfig) => void` | Register a type. Called by the framework when `defineAppModule({ notifications })` runs — rarely needed manually. |
|
|
45
|
-
| `ingest(msg, opts?)` | `(PushNotification, { broadcast?: boolean }?) => void` | Push a message through the same pipeline SignalR uses. Broadcasts pass through the active broadcast filter. |
|
|
46
|
-
| `setBroadcastFilter(fn)` | `((PushNotification) => boolean) => void` | Install a filter for broadcast messages. Prefer [useBroadcastFilter](./useBroadcastFilter.md) — same method. |
|
|
47
|
-
| `clearBroadcastFilter()` | `() => void` | Remove the broadcast filter. |
|
|
48
|
-
| `markAsRead(msg)` | `(PushNotification) => void` | Mark one message as read. Mirrors to the server. |
|
|
49
|
-
| `markAllAsRead()` | `() => Promise<void>` | Optimistic mark-all with rollback on failure. |
|
|
50
|
-
| `loadHistory(take?)` | `(number?) => Promise<void>` | Fetch history from the platform. Default page size is 10. The shell already calls this at bootstrap. |
|
|
51
|
-
| `subscribe(opts)` | `({ types, filter?, handler? }) => () => void` | Low-level pub/sub. Returns an `unsub` function. Inside blades use [useBladeNotifications](./useBladeNotifications.md) — it wraps this and registers cleanup automatically. |
|
|
52
|
-
| `getByType(type)` | `(string) => PushNotification[]` | Filter `history` by `notifyType`. |
|
|
53
|
-
|
|
54
|
-
## Escape-hatch patterns
|
|
55
|
-
|
|
56
|
-
### Manually ingest a message (tests, scripted replay)
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
const store = useNotificationStore();
|
|
60
|
-
|
|
61
|
-
store.ingest({
|
|
62
|
-
id: "test-1",
|
|
63
|
-
notifyType: "OrderCreatedDomainEvent",
|
|
64
|
-
title: "Test order",
|
|
65
|
-
isNew: true,
|
|
66
|
-
created: new Date().toISOString(),
|
|
67
|
-
} as PushNotification);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The ingest pipeline runs the configured toast logic and notifies subscribers exactly like a real SignalR message would, so you can verify the end-to-end behavior of `defineAppModule({ notifications })` + `useBladeNotifications` without a live hub.
|
|
71
|
-
|
|
72
|
-
`ingest` with `{ broadcast: true }` simulates `SendSystemEvents` and runs the broadcast filter; without it the message is treated as targeted and bypasses the filter.
|
|
73
|
-
|
|
74
|
-
### Custom shell surface (replace the bell dropdown)
|
|
75
|
-
|
|
76
|
-
When you are building a shell variant that omits the framework's bell dropdown — for example, a module-federation host that renders its own header — bind to the store directly:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { useNotificationStore } from "@vc-shell/framework";
|
|
80
|
-
import { onMounted } from "vue";
|
|
81
|
-
|
|
82
|
-
const store = useNotificationStore();
|
|
83
|
-
|
|
84
|
-
onMounted(() => store.loadHistory(100));
|
|
85
|
-
// Use store.history, store.unreadCount, store.markAllAsRead in your own components.
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
If you are using `VcApp` (the default shell), do **not** do this — the dropdown is already mounted and binding to the same store, so a second surface duplicates what is already on screen.
|
|
89
|
-
|
|
90
|
-
## Resolution
|
|
91
|
-
|
|
92
|
-
`useNotificationStore()` resolves in this order:
|
|
93
|
-
|
|
94
|
-
1. If called inside a Vue component's `setup` (or `app.runWithContext()`), it uses `inject(NotificationStoreKey)`.
|
|
95
|
-
2. Otherwise it returns a module-level singleton created on first call.
|
|
96
|
-
|
|
97
|
-
The fallback exists so module-federation remotes and standalone scripts see the same store the host app uses.
|
|
98
|
-
|
|
99
|
-
## Tips
|
|
100
|
-
|
|
101
|
-
- **Prefer facades.** Every time you find yourself writing `store.subscribe(...)` inside a blade, you want `useBladeNotifications`. Every time you write `store.setBroadcastFilter(...)`, you want `useBroadcastFilter`. The facades exist to keep cleanup, typing, and invariants in one place.
|
|
102
|
-
- **Do not iterate `realtime` for unread counts inside a blade.** That is what `useBladeNotifications` does correctly. Touching `realtime` directly couples your component to shell internals.
|
|
103
|
-
- **`loadHistory` replaces, then merges.** Calling it again pages in more entries and merges them with the existing history.
|
|
104
|
-
- **`markAllAsRead` is optimistic.** The local state flips immediately; if the server call fails the change is rolled back and an error toast surfaces.
|
|
105
|
-
- **Direct store mutations can bypass invariants.** The broadcast filter only runs through `ingest({ broadcast: true })`. Pushing arrays around `history.value` directly is not supported.
|
|
106
|
-
|
|
107
|
-
## Related
|
|
108
|
-
|
|
109
|
-
- [useBladeNotifications](./useBladeNotifications.md) — recommended scope-aware subscription. **Start here.**
|
|
110
|
-
- [useBroadcastFilter](./useBroadcastFilter.md) — broadcast acceptance gate.
|
|
111
|
-
- [useNotificationContext](./useNotificationContext.md) — payload access inside templates.
|
|
112
|
-
- [Notifications concept page.](../../concepts/notifications.md)
|
|
113
|
-
- [Notifications plugin reference.](../../plugins/notifications.md)
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Assets Module
|
|
3
|
-
category: reference
|
|
4
|
-
group: modules
|
|
5
|
-
slug: assets
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Assets Details Module
|
|
9
|
-
|
|
10
|
-
A built-in child blade for editing a single asset's metadata: display name, alt text (images only), and description. Opened by `AssetsManager` when the user clicks a table row, but also usable standalone from any parent blade that needs single-asset editing.
|
|
11
|
-
|
|
12
|
-
## Overview
|
|
13
|
-
|
|
14
|
-
The blade reads an `AssetLike` instance from `options.asset`, exposes a form pre-populated from it, and delegates persistence back to the caller through two callbacks (`assetEditHandler`, `assetRemoveHandler`). The blade itself never mutates the original asset and never talks to the network — the caller decides what saving and removing means.
|
|
15
|
-
|
|
16
|
-
The module is registered as `AssetsDetailsModule` and exposes the `AssetsDetails` blade (registered globally under the name `"AssetsDetails"`).
|
|
17
|
-
|
|
18
|
-
## Module Registration
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { AssetsDetailsModule } from "@vc-shell/framework";
|
|
22
|
-
|
|
23
|
-
// Registered automatically when the framework loads
|
|
24
|
-
// Exposes: AssetsDetails blade component
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Options (via `useBlade`)
|
|
28
|
-
|
|
29
|
-
The blade reads its configuration from `options` via `useBlade<AssetsDetailsOptions>()` (not props):
|
|
30
|
-
|
|
31
|
-
| Option | Type | Description |
|
|
32
|
-
| -------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
-
| `asset` | `AssetLike` | The asset to edit. The blade clones it into local state — the source object is never mutated until `assetEditHandler` is called. |
|
|
34
|
-
| `disabled` | `boolean?` | When true, every input is read-only and the toolbar Save/Delete buttons are disabled. |
|
|
35
|
-
| `hiddenFields` | `string[]?` | Field names to hide. Supported values: `"name"`, `"altText"`, `"description"`. The header (preview, size, created date, URL) is always shown. |
|
|
36
|
-
| `assetEditHandler` | `(asset: AssetLike) => void \| Promise<void>` | Called by the toolbar Save button with the edited copy. The blade closes itself after the handler resolves. |
|
|
37
|
-
| `assetRemoveHandler` | `(asset: AssetLike) => Promise<void>` | Called by the toolbar Delete button. The blade closes itself after the handler resolves. |
|
|
38
|
-
|
|
39
|
-
## Usage
|
|
40
|
-
|
|
41
|
-
### Direct invocation
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { useBlade } from "@vc-shell/framework";
|
|
45
|
-
|
|
46
|
-
const { openBlade } = useBlade();
|
|
47
|
-
|
|
48
|
-
openBlade({
|
|
49
|
-
name: "AssetsDetails",
|
|
50
|
-
options: {
|
|
51
|
-
asset: selectedAsset,
|
|
52
|
-
disabled: !canEdit.value,
|
|
53
|
-
hiddenFields: ["altText"],
|
|
54
|
-
assetEditHandler: async (edited) => {
|
|
55
|
-
await api.updateAsset(edited);
|
|
56
|
-
},
|
|
57
|
-
assetRemoveHandler: async (asset) => {
|
|
58
|
-
await api.deleteAsset(asset.id);
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Used by AssetsManager
|
|
65
|
-
|
|
66
|
-
When `AssetsManager` opens this blade on row click, it wires the manager's mutation methods into the handlers automatically:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// inside AssetsManager
|
|
70
|
-
openBlade({
|
|
71
|
-
name: "AssetsDetails",
|
|
72
|
-
options: {
|
|
73
|
-
asset,
|
|
74
|
-
disabled: readonly.value,
|
|
75
|
-
hiddenFields: options.value?.hiddenFields,
|
|
76
|
-
assetEditHandler: (asset) => manager.updateItem(asset),
|
|
77
|
-
assetRemoveHandler: (asset) => manager.remove(asset),
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Header
|
|
83
|
-
|
|
84
|
-
The header is always rendered above the editable form and is not configurable through `hiddenFields`:
|
|
85
|
-
|
|
86
|
-
| Element | Source |
|
|
87
|
-
| ------------ | -------------------------------------------------------------------------------------------------- |
|
|
88
|
-
| Preview | `VcImage` thumbnail for image extensions (png/jpg/jpeg/svg/gif), colored extension badge otherwise |
|
|
89
|
-
| Size | `readableSize(asset.size)` — formatted as `KB`/`MB`/`GB` |
|
|
90
|
-
| Created date | `asset.createdDate` rendered with `type="date-ago"` |
|
|
91
|
-
| URL | `asset.url` displayed as a copyable link with `asset.name` as the visible label |
|
|
92
|
-
|
|
93
|
-
## Form fields
|
|
94
|
-
|
|
95
|
-
All fields are bound to a local clone of `asset`. They can be hidden individually via `hiddenFields`.
|
|
96
|
-
|
|
97
|
-
| Field | Visibility | Notes |
|
|
98
|
-
| ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
99
|
-
| `name` | Hidden if `hiddenFields` includes `"name"` | Required. Edits the **base name only** — the original extension is preserved on save (e.g. editing `report.pdf` keeps `.pdf`). |
|
|
100
|
-
| `altText` | Image assets only; hide via `"altText"` | Plain string. Shown only when `asset.typeId === "Image"`. |
|
|
101
|
-
| `description` | Hidden if `hiddenFields` includes `"description"` | Multiline textarea. |
|
|
102
|
-
|
|
103
|
-
## Toolbar
|
|
104
|
-
|
|
105
|
-
| Button | Disabled condition | Action |
|
|
106
|
-
| ------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
107
|
-
| Save | `disabled === true`, the form is invalid, or the form is not dirty (vee-validate `useIsFormValid`/`useIsFormDirty`) | `await assetEditHandler(editedAsset)`, then `closeSelf()` |
|
|
108
|
-
| Delete | `disabled === true` | `await assetRemoveHandler(editedAsset)`, then `closeSelf()` |
|
|
109
|
-
|
|
110
|
-
## Tips
|
|
111
|
-
|
|
112
|
-
- **Extension is locked.** The `name` input only edits the base name; the original extension is reattached on save. Renaming `photo.png` to `cover` produces `cover.png`.
|
|
113
|
-
- **Required name validation** uses vee-validate's `required` rule. The Save button stays disabled until validation passes.
|
|
114
|
-
- **`assetEditHandler`** is invoked with the edited clone — the original `options.asset` reference is never mutated. Callers should treat the argument as the new state.
|
|
115
|
-
- **`disabled` makes the blade fully read-only:** inputs are disabled and toolbar Save/Delete are disabled regardless of dirtiness.
|
|
116
|
-
- **`hiddenFields` does not enforce required-ness.** Hiding `"name"` skips the validated `<Field>`, so the Save button only depends on form dirtiness.
|
|
117
|
-
|
|
118
|
-
## Related
|
|
119
|
-
|
|
120
|
-
- `framework/modules/assets-manager/` -- parent `AssetsManager` blade that opens `AssetsDetails` on row click
|
|
121
|
-
- `framework/core/composables/useAssetsManager/` -- `AssetLike` type definition
|
|
122
|
-
- `framework/core/utilities/assets.ts` -- `isImage`, `readableSize`, `getExtensionColor`, `getExtensionLabel`
|
|
123
|
-
- `framework/ui/components/molecules/vc-field/` -- `VcField` used by the header for size/date/url
|
package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/dashboard-widget-skeleton.docs.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: DashboardWidgetSkeleton
|
|
3
|
-
category: composables
|
|
4
|
-
group: utilities
|
|
5
|
-
internal: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# DashboardWidgetSkeleton
|
|
9
|
-
|
|
10
|
-
Internal placeholder card used by `GridstackDashboard` while remote modules are still loading. Mimics the shape of `DashboardWidgetCard` (header with icon + title, stats row, content lines) with a shimmer animation. Not exported from `@vc-shell/framework` — consumed only inside the dashboard organism.
|
|
11
|
-
|
|
12
|
-
## When to Use
|
|
13
|
-
|
|
14
|
-
- You won't use this directly. `GridstackDashboard` renders one per `SkeletonItem` while `ModulesReadyKey` resolves to `false`.
|
|
15
|
-
- If you build a custom dashboard layout and want the same loading aesthetic, copy this file rather than importing it — the component is intentionally internal so the dashboard team can change its markup at any time.
|
|
16
|
-
|
|
17
|
-
## Layout Contract
|
|
18
|
-
|
|
19
|
-
The skeleton has no props. Its parent positions it via inline `style` (`grid-column` / `grid-row`) inside a 12-column CSS grid. Card height/width is fully determined by the grid cell it occupies, so the skeleton stretches to fill its slot.
|
|
20
|
-
|
|
21
|
-
## Accessibility
|
|
22
|
-
|
|
23
|
-
- The wrapper has `aria-hidden="true"` so screen readers ignore the visual placeholders. The parent grid carries `role="status"` + `aria-busy="true"` to announce loading state once.
|
|
24
|
-
- Shimmer animation is disabled when the user prefers reduced motion.
|
|
25
|
-
|
|
26
|
-
## Design Tokens
|
|
27
|
-
|
|
28
|
-
Skeleton inherits dashboard card tokens where possible (`--dashboard-widget-card-background`, `--dashboard-widget-card-border-color`, `--dashboard-widget-card-border-radius`, `--dashboard-widget-card-shadow`) so its silhouette matches the real card. Shimmer colors are read from `--neutrals-100` / `--neutrals-200`.
|
|
29
|
-
|
|
30
|
-
## Related
|
|
31
|
-
|
|
32
|
-
- [DraggableDashboard](./draggable-dashboard.docs.md) — owns the skeleton grid and decides when to render placeholders.
|
|
33
|
-
- [DashboardWidgetCard](../dashboard-widget-card/dashboard-widget-card.docs.md) — the real card whose shape skeletons imitate.
|