diff --git a/.storybook/STORY_STANDARD.md b/.storybook/STORY_STANDARD.md index 52fef401f..98909e64b 100644 --- a/.storybook/STORY_STANDARD.md +++ b/.storybook/STORY_STANDARD.md @@ -35,16 +35,16 @@ VcCard, VcWidget **Minimum 3 stories.** Required: -| Requirement | Description | -|-------------|-------------| -| `tags: ["autodocs"]` | Enables automatic docs page | -| JSDoc on meta | 2-3 sentence component description | -| `argTypes` for all props | With `description`, `table.type`, `table.defaultValue` | -| `argTypes` for slots | With `table.category: "Slots"` | -| `parameters.docs.description.component` | With inline code usage example | -| Story: Default | Most common usage | -| Story: Variants/Sizes | If component has visual variants | -| Story: Real-World Context | Component used in a realistic scenario | +| Requirement | Description | +| --------------------------------------- | ------------------------------------------------------ | +| `tags: ["autodocs"]` | Enables automatic docs page | +| JSDoc on meta | 2-3 sentence component description | +| `argTypes` for all props | With `description`, `table.type`, `table.defaultValue` | +| `argTypes` for slots | With `table.category: "Slots"` | +| `parameters.docs.description.component` | With inline code usage example | +| Story: Default | Most common usage | +| Story: Variants/Sizes | If component has visual variants | +| Story: Real-World Context | Component used in a realistic scenario | ### Tier 2 — Standard (atoms/molecules with state and interaction) @@ -52,18 +52,18 @@ VcCard, VcWidget VcSwitch, VcCheckbox, VcRadioButton, VcSelect, VcDatePicker, VcPagination, VcToast, VcBreadcrumbs, VcDropdown, VcMenu, VcField, VcForm, VcSlider, VcRating, VcAccordion, VcInputCurrency, VcInputGroup, VcMultivalue, VcColorInput, VcFileUpload, VcEditor, -VcInputDropdown, VcDropdownPanel, VcCheckboxGroup, VcRadioGroup, VcImageTile +VcInputDropdown, VcPopover, VcCheckboxGroup, VcRadioGroup, VcImageTile **Minimum 6 stories.** Everything from Tier 1, plus: -| Requirement | Description | -|-------------|-------------| -| Event argTypes | With `action()` and `table.category: "Events"` | -| Category grouping | `"Model"`, `"Data"`, `"Appearance"`, `"State"`, `"Layout"`, `"Events"`, `"Slots"` | -| Story: States | disabled, loading, error, required | -| Story: Interactive | Uses `ref`, responds to user actions | -| Story: Accessibility | Demonstrates aria-label, keyboard navigation | -| Story: Form Context | Component used within a form (if applicable) | +| Requirement | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| Event argTypes | With `action()` and `table.category: "Events"` | +| Category grouping | `"Model"`, `"Data"`, `"Appearance"`, `"State"`, `"Layout"`, `"Events"`, `"Slots"` | +| Story: States | disabled, loading, error, required | +| Story: Interactive | Uses `ref`, responds to user actions | +| Story: Accessibility | Demonstrates aria-label, keyboard navigation | +| Story: Form Context | Component used within a form (if applicable) | ### Tier 3 — Complex (organisms) @@ -72,12 +72,12 @@ VcApp, VcDynamicProperty, VcAuthLayout **Minimum 10 stories.** Everything from Tier 2, plus: -| Requirement | Description | -|-------------|-------------| -| Feature stories | Each major feature as a separate named story | -| Edge cases | Empty state, error state, many items, long content | -| Mobile stories | If component has mobile variant | -| Composition stories | Used with other vc-shell components | +| Requirement | Description | +| ------------------- | -------------------------------------------------- | +| Feature stories | Each major feature as a separate named story | +| Edge cases | Empty state, error state, many items, long content | +| Mobile stories | If component has mobile variant | +| Composition stories | Used with other vc-shell components | ## Template @@ -157,7 +157,9 @@ export const Default: Story = { }; /** All visual variants side by side. */ -export const AllVariants: Story = { /* ... */ }; +export const AllVariants: Story = { + /* ... */ +}; // ... more stories per tier requirements ``` @@ -182,22 +184,28 @@ Structure: Brief description (1-2 sentences). ## When to Use + - Use case 1 - When NOT to use ## Basic Usage + (code example) ## Key Props + (table with most important props only — full list in Storybook autodocs) ## Common Patterns + (2-3 real-world code examples) ## Accessibility + (keyboard, screen reader, ARIA notes) ## Related Components + (links to related components) ``` @@ -225,15 +233,15 @@ Before modifying any existing story file: Stories use functional categories instead of Atomic Design levels. Use the category that matches the component's primary purpose. -| Category | Components | -|---|---| -| **Form** | VcInput, VcTextarea, VcSelect, VcCheckbox, VcCheckboxGroup, VcRadioButton, VcRadioGroup, VcSwitch, VcDatePicker, VcInputCurrency, VcInputDropdown, VcInputGroup, VcColorInput, VcSlider, VcRating, VcFileUpload, VcEditor, VcMultivalue, VcField, VcForm | -| **Data Display** | VcDataTable, VcBadge, VcStatus, VcStatusIcon, VcLabel, VcHint, VcImage, VcVideo, VcCard, VcWidget, VcDynamicProperty, VcGallery, VcImageUpload | -| **Navigation** | VcBreadcrumbs, VcMenu, VcPagination, VcBlade, VcBladeNavigation | -| **Overlay** | VcPopup, VcTooltip, VcDropdown, VcDropdownPanel, VcToast, VcSidebar | -| **Layout** | VcContainer, VcScrollableContainer, VcRow, VcCol, VcSkeleton, VcProgress, VcIcon, VcLoading, VcApp, VcAuthLayout | -| **Action** | VcButton, VcButtonGroup, VcLink, VcBanner, VcAccordion, VcImageTile | -| **Shared** | App-level components (settings, notifications, auth pages) | +| Category | Components | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Form** | VcInput, VcTextarea, VcSelect, VcCheckbox, VcCheckboxGroup, VcRadioButton, VcRadioGroup, VcSwitch, VcDatePicker, VcInputCurrency, VcInputDropdown, VcInputGroup, VcColorInput, VcSlider, VcRating, VcFileUpload, VcEditor, VcMultivalue, VcField, VcForm | +| **Data Display** | VcDataTable, VcBadge, VcStatus, VcStatusIcon, VcLabel, VcHint, VcImage, VcVideo, VcCard, VcWidget, VcDynamicProperty, VcGallery, VcImageUpload | +| **Navigation** | VcBreadcrumbs, VcMenu, VcPagination, VcBlade, VcBladeNavigation | +| **Overlay** | VcPopup, VcTooltip, VcDropdown, VcPopover, VcToast, VcSidebar | +| **Layout** | VcContainer, VcScrollableContainer, VcRow, VcCol, VcSkeleton, VcProgress, VcIcon, VcLoading, VcApp, VcAuthLayout | +| **Action** | VcButton, VcButtonGroup, VcLink, VcBanner, VcAccordion, VcImageTile | +| **Shared** | App-level components (settings, notifications, auth pages) | ## Automated Testing @@ -250,8 +258,8 @@ To disable snapshot testing for a story or entire component: ```typescript const meta = { parameters: { - snapshot: { disable: true }, // Skip all snapshot tests for this component - visual: { disable: true }, // Skip all visual tests for this component + snapshot: { disable: true }, // Skip all snapshot tests for this component + visual: { disable: true }, // Skip all visual tests for this component }, }; ``` diff --git a/cli/migrate/src/transforms/remove-global-components.ts b/cli/migrate/src/transforms/remove-global-components.ts index af919c3cb..0fb27559a 100644 --- a/cli/migrate/src/transforms/remove-global-components.ts +++ b/cli/migrate/src/transforms/remove-global-components.ts @@ -41,7 +41,7 @@ const UI_COMPONENTS = new Set([ "VcDatePicker", "VcDropdown", "VcDropdownItem", - "VcDropdownPanel", + "VcPopover", "VcEditor", "VcField", "VcFileUpload", diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/api/platform.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/api/platform.docs.md index 82d0912a4..6a0a62c47 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/api/platform.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/api/platform.docs.md @@ -31,9 +31,11 @@ All clients extend `AuthApiBase` and accept an optional `baseUrl` and `http` fet | Client | Description | | --------------------------- | -------------------------------------------------------- | | `ExternalSignInClient` | External authentication provider sign-in | +| `AppManifestClient` | Application manifest descriptor operations | | `AppsClient` | Application descriptor operations | | `AuthorizationClient` | Permission and role checking | | `ChangeLogClient` | Platform change log / audit trail | +| `DeveloperToolsClient` | Developer-tools endpoint | | `DiagnosticsClient` | System diagnostics and health | | `DynamicPropertiesClient` | Dynamic property CRUD and dictionary management | | `JobsClient` | Background job management | @@ -46,20 +48,20 @@ All clients extend `AuthApiBase` and accept an optional `baseUrl` and `http` fet ## Key DTOs (Interfaces) -| Interface | Description | -| -------------------------------- | --------------------------------------------------------------------------------- | -| `PushNotification` | Push notification payload: `id`, `title`, `notifyType`, `isNew`, `finished`, etc. | -| `PushNotificationSearchCriteria` | Search criteria for notification queries | -| `ApplicationUser` | Platform user with roles, permissions, logins | -| `Role` | Security role with permissions | -| `Permission` | Individual permission entry | -| `DynamicProperty` | Dynamic property definition | -| `DynamicObjectProperty` | Property value bound to an object | -| `ModuleDescriptor` | Module metadata (id, version, dependencies) | -| `ChangeLogSearchCriteria` | Audit log search parameters | -| `ChangeLogSearchResult` | Paginated audit log results | -| `ObjectSettingEntry` | Setting entry with value and metadata | -| `License` | Platform license information | +| Interface | Description | +| -------------------------------- | -------------------------------------------------------------------------------- | +| `PushNotification` | Push notification payload: `id`, `title`, `notifyType`, `isNew`, `created`, etc. | +| `PushNotificationSearchCriteria` | Search criteria for notification queries | +| `ApplicationUser` | Platform user with roles, permissions, logins | +| `Role` | Security role with permissions | +| `Permission` | Individual permission entry | +| `DynamicProperty` | Dynamic property definition | +| `DynamicObjectProperty` | Property value bound to an object | +| `ModuleDescriptor` | Module metadata (id, version, dependencies) | +| `ChangeLogSearchCriteria` | Audit log search parameters | +| `ChangeLogSearchResult` | Paginated audit log results | +| `ObjectSettingEntry` | Setting entry with value and metadata | +| `License` | Platform license information | ## Base Class @@ -67,7 +69,7 @@ All clients extend `AuthApiBase` and accept an optional `baseUrl` and `http` fet export class AuthApiBase { authToken: string; setAuthToken(token: string): void; - protected getBaseUrl(defaultUrl: string, baseUrl: string): string; // always returns "" + getBaseUrl(defaultUrl: string, baseUrl: string): string; // always returns "" protected transformOptions(options: RequestInit): Promise; // sets the Bearer header only if `authToken` is non-empty } ``` @@ -86,7 +88,7 @@ const criteria: PushNotificationSearchCriteria = { take: 20, skip: 0 }; const result = await client.searchPushNotification(criteria); console.log(`Found ${result.totalCount} notifications`); -for (const notification of result.results ?? []) { +for (const notification of result.notifyEvents ?? []) { console.log(`${notification.title} (${notification.notifyType})`); } ``` @@ -100,7 +102,7 @@ const securityClient = new SecurityClient(); const user = await securityClient.getCurrentUser(); console.log(`Logged in as: ${user.userName}`); -console.log(`Roles: ${user.roles?.map((r) => r.name).join(", ")}`); +console.log(`Permissions: ${user.permissions?.join(", ")}`); console.log(`Is admin: ${user.isAdministrator}`); ``` @@ -142,7 +144,7 @@ import { SettingClient, ObjectSettingEntry } from "@vc-shell/framework"; const client = new SettingClient(); // Read a specific setting -const settings = await client.getValues(["VirtoCommerce.Notifications.SendGrid.ApiKey"]); +const setting = await client.getGlobalSetting("VirtoCommerce.Notifications.SendGrid.ApiKey"); // Update a setting const entry: ObjectSettingEntry = { diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md index ce2471823..3a0e6c063 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md @@ -2,6 +2,7 @@ title: Blade Navigation Composables category: composables group: blade-navigation +internal: true --- # Blade Navigation Composables @@ -27,11 +28,16 @@ Blade navigation manages an ordered stack of blade descriptors (plain data objec ## Exports ```typescript -export { useBladeNavigation } from "./useBladeNavigationAdapter"; -export { createBladeStack, useBladeStack } from "./useBladeStack"; -export { createBladeMessaging, useBladeMessaging } from "./useBladeMessaging"; +export { useBladeStack } from "@core/blade-navigation/useBladeStack"; +export { useBladeMessaging } from "@core/blade-navigation/useBladeMessaging"; +export * from "@core/blade-navigation/types"; +export { __registerBladeConfig, getBladeConfig, getAllBladeConfigs } from "@core/blade-navigation/bladeConfigRegistry"; +export { useTableQueryState } from "@core/blade-navigation/table-query-state"; +export type { UseTableQueryStateReturn, TableQueryPatch } from "@core/blade-navigation/table-query-state"; ``` +The `createBladeStack` / `createBladeMessaging` factories are internal (not re-exported); they are consumed directly by the framework rendering layer. `useBladeNavigation` is the adapter API documented below, which lives under `core/composables/useBladeNavigationAdapter/`, not in this package. + ## useBladeStack @@ -44,27 +50,28 @@ Creates a new stack instance. Called once by the navigation plugin. ### API -| Method | Description | -| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `openWorkspace(event)` | Sets the root blade (index 0). Closes all existing blades unconditionally. | -| `openBlade(event)` | Opens a child blade after a parent. Closes any blades deeper than the parent (with guard checks). | -| `closeBlade(bladeId)` | Closes a blade and all its children. Returns `true` if a guard prevented closing. | -| `closeChildren(parentId)` | Closes all blades after the given parent. | -| `replaceCurrentBlade(event)` | Destroys the current active blade and creates a new one at the same stack index with the same `parentId`. | -| `coverCurrentBlade(event)` | Hides the current active blade (keeps it in the stack) and opens a new blade on top. Closing the covering blade restores the hidden blade. | -| `registerBeforeClose(bladeId, guard)` | Registers a guard function. Return `true` from the guard to PREVENT closing. | -| `unregisterBeforeClose(bladeId)` | Removes a close guard. | -| `setBladeError(bladeId, error)` | Sets an error on a blade descriptor (displayed as error banner). | -| `clearBladeError(bladeId)` | Clears a blade's error. | -| `setBladeTitle(bladeId, title)` | Updates the blade's title in the descriptor. | +| Method | Description | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `openWorkspace(event)` | Sets the root blade (index 0). Closes all existing blades unconditionally. | +| `openBlade(event)` | Opens a child blade after a parent. Closes any blades deeper than the parent (with guard checks). | +| `closeBlade(bladeId)` | Closes a blade and all its children. Returns `true` if a guard prevented closing. | +| `closeChildren(parentId)` | Closes all blades after the given parent. | +| `replaceCurrentBlade(event)` | Destroys the current active blade and creates a new one at the same stack index with the same `parentId`. | +| `coverCurrentBlade(event)` | Hides the current active blade (keeps it in the stack) and opens a new blade on top. Closing the covering blade restores the hidden blade. | +| `registerBeforeClose(bladeId, guard)` | Registers a guard function. Return `true` from the guard to PREVENT closing. | +| `unregisterBeforeClose(bladeId)` | Removes a close guard. | +| `setBladeError(bladeId, error)` | Sets an error on a blade descriptor (displayed as error banner). | +| `clearBladeError(bladeId)` | Clears a blade's error. | +| `setBladeTitle(bladeId, title)` | Updates the blade's title in the descriptor. | +| `updateBladeQuery(bladeId, patch)` | Merges `patch` into the blade descriptor's `query`; keys with empty-string or nullish values are removed. Backs table URL-query-state persistence. | ### Computed -| Property | Type | Description | -| ------------- | ---------------------------- | ------------------------ | -| `workspace` | `BladeDescriptor` | The root blade (index 0) | -| `blades` | `readonly BladeDescriptor[]` | Full ordered stack | -| `activeBlade` | `BladeDescriptor` | The last visible blade | +| Property | Type | Description | +| ------------- | ------------------------------------------- | ------------------------ | +| `workspace` | `ComputedRef` | The root blade (index 0) | +| `blades` | `ComputedRef` | Full ordered stack | +| `activeBlade` | `ComputedRef` | The last visible blade | ### Composable: `useBladeStack()` @@ -134,18 +141,20 @@ openBlade({ The plain data object stored in the stack for each blade: -| Field | Type | Description | -| ---------- | ---------- | -------------------------------------------------------------------------- | -| `id` | `string` | Unique instance ID (auto-generated) | -| `name` | `string` | Blade registration name | -| `url` | `string?` | URL segment for address bar sync | -| `param` | `unknown` | Parameter passed when opening (e.g., entity ID) | -| `query` | `Record?` | Query parameters | -| `options` | `unknown` | Arbitrary options passed to the blade | -| `parentId` | `string?` | ID of the parent blade | -| `visible` | `boolean` | Whether the blade is rendered (false when covered via `coverCurrentBlade`) | -| `error` | `unknown?` | Error state for the error banner | -| `title` | `string?` | Dynamic title override | +| Field | Type | Description | +| ------------- | -------------------------- | -------------------------------------------------------------------------- | +| `id` | `string` | Unique instance ID (auto-generated) | +| `name` | `string` | Blade registration name | +| `url` | `string?` | URL segment for address bar sync | +| `param` | `string?` | Parameter passed when opening (e.g., entity ID) | +| `query` | `Record?` | Query parameters | +| `options` | `Record?` | Arbitrary options passed to the blade | +| `parentId` | `string?` | ID of the parent blade | +| `visible` | `boolean` | Whether the blade is rendered (false when covered via `coverCurrentBlade`) | +| `error` | `unknown?` | Error state for the error banner | +| `title` | `string?` | Dynamic title override | +| `maximized` | `boolean?` | Whether this blade is maximized (fullscreen) — set by VcBladeSlot | +| `breadcrumbs` | `Breadcrumbs[]?` | Navigation breadcrumbs — set by VcBladeSlot | ## Tips @@ -163,6 +172,6 @@ The plain data object stored in the stack for each blade: - `framework/core/composables/useBlade/` -- `useBlade()`, `useBladeContext()` (new API) -- `framework/shell/_internal/blade-nav/plugin-v2.ts` -- plugin that creates and provides the stack/messaging +- `framework/shell/_internal/blade-navigation/plugin-v2.ts` -- plugin that creates and provides the stack/messaging - `framework/core/blade-navigation/types/index.ts` -- `BladeDescriptor`, `IBladeStack`, `IBladeMessaging` diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/table-query-state/useTableQueryState.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/table-query-state/useTableQueryState.docs.md new file mode 100644 index 000000000..e6c87dda5 --- /dev/null +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/blade-navigation/table-query-state/useTableQueryState.docs.md @@ -0,0 +1,95 @@ +--- +title: useTableQueryState +category: composables +group: data +--- + +# useTableQueryState + +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. + +Read-only: writing the view state to the URL stays inside `VcDataTable`. This composable only exposes the restored values to the page. + +## When to Use + +- A list blade is URL-addressable (workspace or routable) and uses `VcDataTable` with a `state-key` +- You want a single, coordinated load on reload (sort + search + page restored together) +- You are wiring the page's combined loader watcher and need the restored seed values + +## When NOT to Use + +- The table is standalone or its blade has no URL — there is nothing to restore (`read()` returns `{}`) +- You only need the live view state — that already lives in your own `sortField`/`searchValue`/`currentPage` refs +- You want to _write_ to the URL — that is automatic; do not persist manually + +## Basic Usage + +```ts +import { ref, watch } from "vue"; +import { useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework"; + +const PAGE_SIZE = 20; +const { debounce } = useFunctions(); +const { sortField, sortOrder, sortExpression } = useDataTableSort({ + initialField: "createdDate", + initialDirection: "DESC", +}); +const searchValue = ref(); +const currentPage = ref(1); + +// Seed refs from the URL before the loader runs. +const restored = useTableQueryState("offers_list").read(); +if (restored.sort) { + const [field, dir] = restored.sort.split(":"); + sortField.value = field; + sortOrder.value = dir === "DESC" ? -1 : 1; +} +if (restored.search) searchValue.value = restored.search; +if (restored.page) currentPage.value = restored.page; + +function load() { + return loadItems({ + sort: sortExpression.value, + keyword: searchValue.value || undefined, + skip: (currentPage.value - 1) * PAGE_SIZE, + }); +} + +load(); +watch(searchValue, () => (currentPage.value = 1)); +watch([sortExpression, searchValue, currentPage], debounce(load, 300)); +``` + +## API + +### Parameters + +| Parameter | Type | Default | Description | +| ---------- | --------------------- | ----------- | ------------------------------------------------------------------------- | +| `stateKey` | `string \| undefined` | `undefined` | The same `state-key` passed to `VcDataTable` for this table (namespacing) | + +### Returns + +| Property | Type | Description | +| -------- | ----------------------- | ------------------------------------------------------------------------------------------------------ | +| `read` | `() => TableQueryPatch` | Reads the restored `{ sort?, search?, page? }` from the URL. Returns `{}` when no service is available | + +`TableQueryPatch`: `{ sort?: string; search?: string; page?: number }` — `sort` is a `"field:ASC"` / `"field:DESC"` expression, `page` is 1-based. + +## Details + +- **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. +- **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. +- **Write-back is automatic**: View state → URL (debounced `router.replace`) is handled by `VcDataTable` via `useTableQueryPersistence`. Pages do not persist manually. + +## Tips + +- Call `read()` once, synchronously, before the loader. Seeding after it runs causes an extra load. +- Use `useDataTablePagination`'s `setPage(n)` to seed the page without firing `onPageChange`. +- Pass the same `state-key` you give `VcDataTable`, so the URL keys match. + +## Related + +- [`useDataTableSort`](../../../ui/composables/useDataTableSort.docs.md) — sort state composable for `VcDataTable` +- [`useDataTablePagination`](../../../ui/composables/useDataTablePagination.docs.md) — pagination state composable (`setPage` for restore seed) +- `VcDataTable` → _URL query persistence_ — the write-back side of this contract diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useAppBarMobileButtons/useAppBarMobileButtons.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useAppBarMobileButtons/useAppBarMobileButtons.docs.md index 507c7ef09..d4e5f2a79 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useAppBarMobileButtons/useAppBarMobileButtons.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useAppBarMobileButtons/useAppBarMobileButtons.docs.md @@ -2,6 +2,7 @@ title: useAppBarMobileButtons category: composables group: services +internal: true --- # useAppBarMobileButtons @@ -19,7 +20,7 @@ Manages custom action buttons in the mobile app bar. Uses provide/inject to shar ```vue @@ -69,7 +70,7 @@ When connectivity is restored, the notification is removed, the CSS class is cle ```vue diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md new file mode 100644 index 000000000..b2ce5eee0 --- /dev/null +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePlatformLocaleSync/usePlatformLocaleSync.docs.md @@ -0,0 +1,35 @@ +--- +title: usePlatformLocaleSync +category: composables +group: user +internal: true +--- + +# usePlatformLocaleSync + +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. + +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. + +## When to Use + +- Never call directly from feature code. This is a framework-internal sync primitive. +- It is invoked once per `VcApp` mount from `useShellBootstrap`. + +## Behaviour + +- Reads `localStorage["NG_TRANSLATE_LANG_KEY"]` via VueUse's `useLocalStorage`, which subscribes to `storage` events for cross-tab reactivity. +- 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`. +- On subsequent changes of the platform key, re-applies the value. +- Skips empty strings (platform clearing the key does not blank the shell locale). +- Skips values equal to `currentLocale` to avoid redundant re-configuration. + +## How It Works + +`useLocalStorage("NG_TRANSLATE_LANG_KEY", "")` returns a `Ref` 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`. + +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. + +## Relationship to `VC_LANGUAGE_SETTINGS` + +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. diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md index 89d2c4d25..6fccaa758 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md @@ -62,7 +62,7 @@ async function deleteProduct(id: string) { | Field | Type | Required | Description | | ----------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------- | -| `component` | `ComponentPublicInstanceConstructor` | Yes | The popup component to render | +| `component` | `T` (`extends Component`) | Yes | The popup component to render | | `props` | `RawProps` | No | Props to pass to the component (typed from the component's `defineProps`) | | `emits` | `RawEmits` | No | Event handlers (typed from the component's `defineEmits`) | | `slots` | `Record` | No | Named slots — strings render as text, components render as VNodes | diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md index 696bccdd8..af01ff023 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useResponsive/useResponsive.docs.md @@ -50,12 +50,12 @@ None. The composable reads breakpoint state from the framework's provide/inject | Property | Type | Default | Description | | ----------- | -------------- | ------- | --------------------------------------------------------------------- | | `isMobile` | `Ref` | `false` | `true` when viewport width < 1024px | -| `isDesktop` | `Ref` | `true` | `true` when viewport width >= 1024px | +| `isDesktop` | `Ref` | `true` | `true` when viewport width > 1024px | | `isPhone` | `Ref` | `false` | `true` when viewport width < 480px | | `isTablet` | `Ref` | `false` | `true` when 480px <= viewport width < 1024px | | `isTouch` | `boolean` | `false` | `true` on touch-capable devices (not reactive — set once at app init) | -Breakpoint thresholds: phone < 480px, tablet 480–1023px, desktop >= 1024px. These match the framework's `setupBreakpoints()` configuration. +Breakpoint thresholds: phone < 480px, tablet 480–1023px, desktop > 1024px (bound to `bp.greater("desktop")`, strictly greater than 1024px). These match the framework's `setupBreakpoints()` configuration. Note: `isMobile` is the union of `isPhone` and `isTablet` — it covers all viewports below the desktop threshold. diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useSettings/useSettings.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useSettings/useSettings.docs.md index 7e251828f..f4dad1b97 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useSettings/useSettings.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useSettings/useSettings.docs.md @@ -104,10 +104,11 @@ applySettings({ ```vue @@ -54,7 +55,11 @@ const { isSlowNetwork } = useSlowNetworkDetection(); | `trackRequest` | `(id: string) => void` | Start tracking a request. If it isn't untracked within 10 s, it counts as slow. | | `untrackRequest` | `(id: string) => void` | Stop tracking a request. Cancels the timer or decrements the slow count. | -### Constants + + +### Internal Constants + +These are module-private `const` declarations, not exported symbols — do not `import` them. They document the thresholds baked into the composable. | Name | Value | Purpose | | --------------------------- | ------------------- | --------------------------------------------------- | @@ -62,8 +67,6 @@ const { isSlowNetwork } = useSlowNetworkDetection(); | `DISMISS_DELAY_MS` | `3000` | Delay before hiding the notification after recovery | | `SLOW_EFFECTIVE_TYPES` | `["slow-2g", "2g"]` | Connection types flagged as slow | - - ## How It Works ### Channel 1: effectiveType (proactive) @@ -88,7 +91,7 @@ The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequ ```vue @@ -47,9 +47,9 @@ next(); | Property | Type | Description | | ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `themes` | `ComputedRef` | All registered themes with their `key` and localized `name`. Reactive -- updates when themes are registered/unregistered. | +| `themes` | `Ref` | All registered themes with their `key` and localized `name`. Reactive -- updates when themes are registered/unregistered. | | `currentThemeKey` | `Ref` | Active theme key (e.g., `"light"`, `"dark"`). Two-way reactive -- setting it switches the theme. | -| `currentLocalizedName` | `ComputedRef` | Localized display name of the active theme (e.g., "Light", "Dark"). Falls back to capitalized key. | +| `currentLocalizedName` | `Ref` | Localized display name of the active theme (e.g., "Light", "Dark"). Falls back to capitalized key. | | `next` | `() => void` | Cycle to the next registered theme in order. Wraps around at the end. | | `setTheme` | `(themeKey: string) => void` | Switch to a specific registered theme. Logs a warning if the key is not registered. | | `register` | `(themes: ThemeDefinition \| ThemeDefinition[]) => void` | Add one or more themes to the global registry. Duplicates (by key) are silently ignored. | diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md index 9d191e414..46137fd3b 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md @@ -326,20 +326,26 @@ function helperFunction() { | `isToolbarItemRegistered` | `(id: string) => boolean` | Check if a toolbar item with the given ID exists | | `registeredToolbarItems` | `IToolbarRegistration[]` | All registered toolbar items across all blades | +### Additional Exports + +| Export | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `provideToolbarService()` | Creates and provides the toolbar service via Vue injection. Idempotent -- returns existing service if already provided. Cleans up via `onScopeDispose`. | + ### IToolbarItem -| Property | Type | Required | Description | -| -------------- | -------------------------------------------------------------------------- | -------- | -------------------------------------------------------------- | -| `id` | `string` | Yes | Unique identifier for the button | -| `title` | `string \| Ref \| ComputedRef` | No | Button label (supports reactive values) | -| `icon` | `string \| (() => string)` | No | Icon class (e.g., `"fas fa-save"`) or a function returning one | -| `clickHandler` | `(app?) => void` | No | Click callback | -| `disabled` | `boolean \| ComputedRef` | No | Whether the button is disabled | -| `isVisible` | `boolean \| Ref \| ComputedRef \| ((blade?) => boolean)` | No | Controls button visibility | -| `priority` | `number` | No | Sort order (higher = displayed first, default `0`) | -| `separator` | `"left" \| "right" \| "both"` | No | Adds a visual divider next to the button | -| `permissions` | `string \| string[]` | No | Required permission(s) to display the button | -| `bladeId` | `string` | No | Target blade ID (auto-resolved from context) | +| Property | Type | Required | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------- | +| `id` | `string` | Yes | Unique identifier for the button | +| `title` | `string \| Ref \| ComputedRef` | No | Button label (supports reactive values) | +| `icon` | `string \| (() => string)` | No | Icon class (e.g., `"fas fa-save"`) or a function returning one | +| `clickHandler` | `(app?) => void` | No | Click callback | +| `disabled` | `boolean \| ComputedRef` | No | Whether the button is disabled | +| `isVisible` | `boolean \| Ref \| ComputedRef \| ((blade?) => boolean \| undefined)` | No | Controls button visibility | +| `priority` | `number` | No | Sort order (higher = displayed first, default `0`) | +| `separator` | `"left" \| "right" \| "both"` | No | Adds a visual divider next to the button | +| `permissions` | `string \| string[]` | No | Required permission(s) to display the button | +| `bladeId` | `string` | No | Target blade ID (auto-resolved from context) | `IToolbarItem` is the shape consumed by `ToolbarService`. The blade-level array binding uses `IBladeToolbar` (see [Core types](../../types/)), a near-identical shape that the framework normalizes into `IToolbarItem` before render. diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md index c302172a6..950c28696 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useUser/useUser.docs.md @@ -60,7 +60,7 @@ None. | Property | Type | Description | | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `user` | `ComputedRef` | Current user details (userName, email, id, etc.), or `undefined` if not authenticated. | +| `user` | `ComputedRef` | Current user details (userName, id, permissions, isAdministrator, etc.), or `undefined` if not authenticated. | | `loading` | `ComputedRef` | Whether a user operation (loadUser, signOut) is in progress. | | `isAuthenticated` | `ComputedRef` | Whether a user session is active. Derived from `user.userName != null`. | | `isAdministrator` | `ComputedRef` | Whether the current user has admin privileges. `undefined` if user is not loaded. | @@ -118,7 +118,7 @@ router.beforeEach(async (to) => { />

{{ user?.userName }}

-

{{ user?.email }}

+

{{ user?.id }}

` | Current user details. | -| `loading` | `ComputedRef` | Whether any operation is in progress. | -| `isAdministrator` | `ComputedRef` | Admin status of the current user. | -| `isAuthenticated` | `ComputedRef` | Whether user session is active. | -| `signIn` | `(username: string, password: string) => Promise` | Authenticates with username/password. Performs cookie-based login, obtains an OAuth token with offline_access scope, and loads user details. Returns `{ succeeded: true }` on success or `{ succeeded: false, error: string }` on failure. | -| `signOut` | `() => Promise` | Clears session, auth data, and localStorage. Handles both standard and external SSO sign-out. | -| `loadUser` | `() => Promise` | Loads/reloads user info from the server. Deduplicates concurrent calls. | -| `validateToken` | `(userId: string, token: string) => Promise` | Validates a password-reset token. Returns `true` if valid. | -| `validatePassword` | `(password: string) => Promise` | Validates a password against the platform's password policy (length, complexity, etc.). | -| `resetPasswordByToken` | `(userId: string, password: string, token: string) => Promise` | Resets a user's password using a reset token. Returns `{ succeeded: true }` on success. | -| `requestPasswordReset` | `(loginOrEmail: string) => Promise` | Sends a password-reset email to the user. Returns `{ succeeded: true }` on success. | -| `changeUserPassword` | `(oldPassword: string, newPassword: string) => Promise` | Changes the current user's password. Requires the old password for verification. | -| `getLoginType` | `() => Promise` | Returns available login types (password-based, external SSO providers, etc.). Used to render the appropriate sign-in UI. | +| Property | Type | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `user` | `ComputedRef` | Current user details. | +| `loading` | `ComputedRef` | Whether any operation is in progress. | +| `isAdministrator` | `ComputedRef` | Admin status of the current user. | +| `isAuthenticated` | `ComputedRef` | Whether user session is active. | +| `signIn` | `(username: string, password: string) => Promise` | Authenticates with username/password. Performs cookie-based login, obtains an OAuth token with offline_access scope, and loads user details. Returns `{ succeeded: true }` on success or `{ succeeded: false, error: string }` on failure. | +| `signOut` | `() => Promise` | Clears session, auth data, and localStorage. Handles both standard and external SSO sign-out. | +| `loadUser` | `() => Promise` | Loads/reloads user info from the server. Deduplicates concurrent calls. | +| `validateToken` | `(userId: string, token: string) => Promise` | Validates a password-reset token. Returns `true` if valid. | +| `validatePassword` | `(password: string) => Promise` | Validates a password against the platform's password policy (length, complexity, etc.). | +| `resetPasswordByToken` | `(userId: string, password: string, token: string) => Promise` | Resets a user's password using a reset token. Returns `{ succeeded: true }` on success. | +| `requestPasswordReset` | `(loginOrEmail: string) => Promise` | Sends a password-reset email to the user. Returns `{ succeeded: true }` on success. | +| `changeUserPassword` | `(oldPassword: string, newPassword: string) => Promise` | Changes the current user's password. Requires the old password for verification. | +| `getLoginType` | `() => Promise` | Returns available login types (password-based, external SSO providers, etc.). Used to render the appropriate sign-in UI. | ## How It Works @@ -139,7 +139,7 @@ async function handleReset() { // Validate against password policy const validation = await validatePassword(newPassword.value); if (!validation.succeeded) { - error.value = validation.errors?.join(", ") ?? "Password does not meet requirements."; + error.value = validation.errors?.map((e) => e.description).join(", ") ?? "Password does not meet requirements."; return; } @@ -167,7 +167,7 @@ const showPasswordForm = ref(false); onMounted(async () => { loginTypes.value = await getLoginType(); // Show password form only if password auth is available - showPasswordForm.value = loginTypes.value.some((t) => t.loginProvider === "password"); + showPasswordForm.value = loginTypes.value.some((t) => t.hasLoginForm); }); @@ -181,10 +181,10 @@ onMounted(async () => {
- Sign in with {{ provider.loginProvider }} + Sign in with {{ provider.authenticationType }}
diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md index 0db7bf28d..ffb3454c4 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md @@ -6,7 +6,7 @@ group: services # useWidgets -Provides access to the `IWidgetService` singleton for managing blade widget registrations, activation state, and external widgets. This is the low-level composable for direct widget service interaction. It exposes the full widget lifecycle API: registering widgets to specific blades, querying registered widgets, tracking active widget state, and managing external (cross-module) widget registrations. +Provides access to the `IWidgetService` singleton for managing blade widget registrations and external widgets. This is the low-level composable for direct widget service interaction. It exposes the widget lifecycle API: registering widgets to specific blades, querying and updating registered widgets, and managing external (cross-module) widget registrations. Also exports `provideWidgetService()` for framework-level initialization and `registerWidget()` / `registerExternalWidget()` for global pre-registration before the service is created. @@ -27,10 +27,8 @@ const widgetService = useWidgets(); // Query all widgets registered for a specific blade const widgets = widgetService.getWidgets("order-details"); -// Check if a widget is currently active (visible and expanded) -if (widgetService.isActiveWidget("order-status-widget")) { - // widget is currently in focus -} +// Query external widgets contributed by other modules +const externalWidgets = widgetService.getExternalWidgetsForBlade("order-details"); ``` ## API @@ -41,21 +39,15 @@ None. ### Returns (`IWidgetService`) -| Property / Method | Type | Description | -| ---------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| `registerWidget` | `(widget: IWidget, bladeId: string) => void` | Registers a widget for a specific blade. The widget will appear in that blade's widget area. | -| `unregisterWidget` | `(widgetId: string, bladeId: string) => void` | Removes a widget from a blade by ID. | -| `getWidgets` | `(bladeId: string) => IWidget[]` | Returns all widgets registered for a specific blade. | -| `clearBladeWidgets` | `(bladeId: string) => void` | Removes all widgets for a blade (used during blade teardown). | -| `registeredWidgets` | `IWidgetRegistration[]` | All widget registrations across all blades. | -| `isActiveWidget` | `(id: string) => boolean` | Checks if a widget is currently active (selected/expanded). | -| `setActiveWidget` | `(args) => void` | Sets a widget as active with its exposed instance. | -| `updateActiveWidget` | `() => void` | Triggers the active widget's update function. Deprecated -- use headless widgets instead. | -| `isWidgetRegistered` | `(id: string) => boolean` | Checks if a widget ID exists in any blade's registry. | -| `updateWidget` | `(args) => void` | Updates properties of a registered widget (trigger, badge, etc.). | -| `resolveWidgetProps` | `(widget, bladeData) => Record` | Resolves widget props from blade data. Deprecated. | -| `getExternalWidgetsForBlade` | `(bladeId: string) => IExternalWidgetRegistration[]` | Gets external widgets that target a specific blade (registered by other modules). | -| `getAllExternalWidgets` | `() => IExternalWidgetRegistration[]` | Gets all registered external widgets across all modules. | +| Property / Method | Type | Description | +| ---------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `registerWidget` | `(widget: IWidget, bladeId: string) => void` | Registers a widget for a specific blade. The widget will appear in that blade's widget area. | +| `unregisterWidget` | `(widgetId: string, bladeId: string) => void` | Removes a widget from a blade by ID. | +| `getWidgets` | `(bladeId: string) => IWidget[]` | Returns all widgets registered for a specific blade. | +| `updateWidget` | `({ id, bladeId, widget }: { id: string; bladeId: string; widget: Partial }) => void` | Updates properties of a registered widget (trigger, badge, etc.). | +| `getExternalWidgetsForBlade` | `(bladeId: string) => IExternalWidgetRegistration[]` | Gets external widgets that target a specific blade (registered by other modules). | +| `getAllExternalWidgets` | `() => IExternalWidgetRegistration[]` | Gets all registered external widgets across all modules. | +| `cloneWidget` | `(widget: T) => T` | Returns a deep-ish clone of a widget or external widget registration. | ### Additional Exports @@ -77,7 +69,7 @@ The widget system has three layers: 3. **Runtime access** (`useWidgets()`): Components inject the service and use it to query or modify widget state. -The service tracks active widgets (which widget is currently expanded/focused) separately from registrations, enabling the widget panel to highlight the active widget and call its update function. +The service stores widget registrations per blade and returns cloned widget definitions on query. It does not track active/expanded/focused state. @@ -117,11 +109,10 @@ import ShippingTracker from "./widgets/ShippingTracker.vue"; export default { install() { registerExternalWidget({ - widgetId: "shipping-tracker", - bladeId: "order-details", // target blade from another module + id: "shipping-tracker", + targetBlades: ["order-details"], // target blades from other modules component: markRaw(ShippingTracker), title: "Shipping", - icon: "fas fa-truck", }); }, }; diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/directives/loading/loading.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/directives/loading/loading.docs.md index fbe4991e1..f00bdc138 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/directives/loading/loading.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/directives/loading/loading.docs.md @@ -25,7 +25,7 @@ Unlike a standalone loading component, `v-loading` scopes the overlay to the ele | ----------------------- | ---------------- | ------------------------------------ | | `v-loading="true"` | `boolean` | Show/hide the loading overlay | | `v-loading="options"` | `LoadingOptions` | Show with configuration (see below) | -| `v-loading:1000="true"` | `arg: string` | Set custom z-index (default: `9999`) | +| `v-loading:1000="true"` | `arg: string` | Set custom z-index (default: `9000`) | ### LoadingOptions @@ -108,7 +108,7 @@ async function loadOrder() {
Content
- +
Content
``` diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md new file mode 100644 index 000000000..0f27a62a0 --- /dev/null +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useBladeNotifications.docs.md @@ -0,0 +1,183 @@ +--- +title: useBladeNotifications +category: composables +group: notifications +--- + +# useBladeNotifications + +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. + +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. + +## When to use + +- A list blade needs to refresh when an entity is created, updated, or deleted elsewhere. +- A long-running operation has a dedicated blade and the blade should update as `processedCount` / `errorCount` flow in. +- A blade wants to surface an inline "N new" badge for messages of a specific type. +- 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. + +## Quick Start + +```ts +import { useBladeNotifications } from "@vc-shell/framework"; + +useBladeNotifications({ + types: ["OfferDeletedDomainEvent"], + onMessage: () => reload(), +}); +``` + +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. + +## API + +### Parameters + +```typescript +interface BladeNotificationOptions { + types: string[]; + filter?: (msg: T) => boolean; + onMessage?: (msg: T) => void; +} +``` + +| Field | Type | Required | Description | +| ----------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `types` | `string[]` | Yes | Notification types to subscribe to. Must match the `notifyType` field on incoming messages. | +| `filter` | `(msg: T) => boolean` | No | Narrow the subscription further (for example, only events for the entity this blade is editing). | +| `onMessage` | `(msg: T) => void` | No | Callback fired once per matching message. Use it to refresh data, mark progress, or update local state. | + +### Returns + +```typescript +interface BladeNotificationReturn { + messages: ComputedRef; + unreadCount: ComputedRef; + markAsRead: (msg: T) => void; +} +``` + +| Property | Type | Description | +| ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `messages` | `ComputedRef` | Realtime messages matching `types` and `filter` that are still unread. Updates reactively as new messages arrive. | +| `unreadCount` | `ComputedRef` | `messages.value.length`. Bind to a badge. | +| `markAsRead` | `(msg: T) => void` | Mark a specific message as read. Removes it from `messages` (and reduces the global unread badge in the bell dropdown). | + +## Typed payloads + +Notification payloads often extend `PushNotification` with domain fields. Pass the type parameter so `onMessage` and `messages` are typed: + +```ts +import type { PushNotification } from "@vc-shell/framework"; + +interface ImportPushNotification extends PushNotification { + jobId: string; + profileId: string; + profileName?: string; + processedCount: number; + errorCount: number; + finished: boolean; +} + +const { messages, markAsRead } = useBladeNotifications({ + types: ["ImportPushNotification"], + onMessage: (message) => { + if (message.finished) { + reload(); + markAsRead(message); + } + }, +}); +``` + +## Common patterns + +### Refresh a list on any matching event + +```ts +useBladeNotifications({ + types: ["OfferDeletedDomainEvent"], + onMessage: () => reload(), +}); +``` + +Drop-in for a list blade that needs to stay in sync with deletions happening anywhere in the app. + +### Filter to the entity this blade owns + +```ts +useBladeNotifications({ + types: ["ImportPushNotification"], + onMessage: (message) => { + if (message.profileId !== param.value) return; // not our job + if (!message.finished) updateProgress(message); + else finalizeImport(message); + }, +}); +``` + +Two open import blades will both receive the stream; each one filters by its own `profileId` so they do not step on each other. + +### Drive a manual progress toast + +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. + +Set the Level 1 type to `silent` and drive the toast yourself: + +```ts title="src/modules/import/index.ts" +defineAppModule({ + notifications: { + ImportPushNotification: { toast: { mode: "silent" } }, + }, + // ... +}); +``` + +```ts title="pages/import-process.vue" +import { useBladeNotifications, notification } from "@vc-shell/framework"; + +let toastId: string | undefined; + +useBladeNotifications({ + types: ["ImportPushNotification"], + onMessage: (message) => { + const content = message.profileName ? `${message.profileName}: ${message.title}` : message.title; + + if (!toastId) { + toastId = notification(content, { timeout: false }); + } else if (!message.finished) { + notification.update(toastId, { content }); + } else { + notification.update(toastId, { + content, + timeout: 5000, + type: message.errorCount ? "error" : "success", + onClose: () => (toastId = undefined), + }); + } + }, +}); +``` + +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. + +## Lifecycle + +`useBladeNotifications` calls `useNotificationStore().subscribe(...)` and registers `onScopeDispose(unsub)` against the current effect scope. Inside a Vue `setup()` (component or ` + + +``` + +## API + +### Parameters + +None. The composable is always called without arguments. Generic type parameter narrows the payload shape: + +```typescript +function useNotificationContext(): ComputedRef; +``` + +### Returns + +`ComputedRef` — 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`). + +## Common patterns + +### Compute display strings from payload fields + +```vue + +``` + +### Open a blade on click + +```vue + + + +``` + +`NotificationTemplate` re-emits the click event so the host (dropdown row, toast) can close itself before your handler runs. + +### Color the template by status + +```ts +const notificationStyle = computed(() => { + switch (notification.value.newStatus) { + case "Approved": + return { color: "var(--success-400)", icon: "lucide-check-circle" }; + case "RequestChanges": + return { color: "var(--danger-400)", icon: "lucide-alert-circle" }; + case "WaitForApproval": + return { color: "var(--warning-600)", icon: "lucide-clock" }; + default: + return { color: "var(--primary-400)", icon: "lucide-bell" }; + } +}); +``` + +`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. + +## Where the template runs + +Notification templates render in two places: + +- **In the bell dropdown**, as one row in the history list. +- **As a toast**, when the type's `toast.mode` is `"auto"` or `"progress"` (set the mode to `"silent"` to render only in the dropdown). + +The template component must be **the same** in both — register it once on `defineAppModule({ notifications })` and the framework reuses it everywhere. + +## Tips + +- **Always type the generic.** `useNotificationContext()` enables autocompletion on payload fields. Without it everything degrades to `PushNotification`. +- **`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. +- **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`. + +## Related + +- [useBladeNotifications](./useBladeNotifications.md) — subscribe to types inside a blade. +- [useNotificationStore](./useNotificationStore.md) — access the underlying store. +- [Notifications concept page.](../../concepts/notifications.md) diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md new file mode 100644 index 000000000..05bc5a41b --- /dev/null +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/composables/useNotificationStore.docs.md @@ -0,0 +1,114 @@ +--- +title: useNotificationStore +category: composables +group: notifications +--- + +# useNotificationStore + +!!! warning "Advanced — most apps do not need this" +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. + +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. + +## When to use + +There are only two cases where this composable belongs in app code: + +- **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. +- **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. + +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). + +## Quick Start + +```ts +import { useNotificationStore } from "@vc-shell/framework"; + +const store = useNotificationStore(); + +await store.loadHistory(50); +``` + +## API + +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`. + +| Member | Type | Description | +| ------------------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `registry` | `Map` | Notification types registered through `defineAppModule({ notifications })`. | +| `history` | `Ref` | Full history. `loadHistory` replaces it with the server's returned order; `ingest` appends new messages in arrival order. Not sorted — sort in the consumer for newest-first display. | +| `realtime` | `Ref` | Session-only realtime queue from the SignalR hub. Drives `messages` in `useBladeNotifications`. | +| `unreadCount` | `ComputedRef` | Count of unread items in `history`. Drives the bell badge. | +| `hasUnread` | `ComputedRef` | Convenience boolean over `unreadCount`. | +| `registerType(t, cfg)` | `(string, NotificationTypeConfig) => void` | Register a type. Called by the framework when `defineAppModule({ notifications })` runs — rarely needed manually. | +| `ingest(msg, opts?)` | `(PushNotification, { broadcast?: boolean }?) => void` | Push a message through the same pipeline SignalR uses. Broadcasts pass through the active broadcast filter. | +| `setBroadcastFilter(fn)` | `((PushNotification) => boolean) => void` | Install a filter for broadcast messages. Prefer [useBroadcastFilter](./useBroadcastFilter.md) — same method. | +| `clearBroadcastFilter()` | `() => void` | Remove the broadcast filter. | +| `markAsRead(msg)` | `(PushNotification) => void` | Mark one message as read locally (flips `isNew` on the matching history and realtime entries). Local-only — no server call; server read-state is reconciled by `markAllAsRead`/`loadHistory`. | +| `markAllAsRead()` | `() => Promise` | Flips local state to read immediately and syncs to the server. Server failure is logged only (no rollback) and reconciled on the next `loadHistory`. | +| `loadHistory(take?)` | `(number?) => Promise` | Fetch history from the platform. Default page size is 10. The shell already calls this at bootstrap. | +| `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. | +| `getByType(type)` | `(string) => PushNotification[]` | Filter `history` by `notifyType`. | + +## Escape-hatch patterns + +### Manually ingest a message (tests, scripted replay) + +```ts +const store = useNotificationStore(); + +store.ingest({ + id: "test-1", + notifyType: "OrderCreatedDomainEvent", + title: "Test order", + isNew: true, + created: new Date().toISOString(), +} as PushNotification); +``` + +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. + +`ingest` with `{ broadcast: true }` simulates `SendSystemEvents` and runs the broadcast filter; without it the message is treated as targeted and bypasses the filter. + +### Custom shell surface (replace the bell dropdown) + +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: + +```ts +import { useNotificationStore } from "@vc-shell/framework"; +import { onMounted } from "vue"; + +const store = useNotificationStore(); + +onMounted(() => store.loadHistory(100)); +// Use store.history, store.unreadCount, store.markAllAsRead in your own components. +``` + +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. + +## Resolution + +`useNotificationStore()` resolves in this order: + +1. If called inside a Vue component's `setup` (or `app.runWithContext()`), it uses `inject(NotificationStoreKey)`. +2. Otherwise it returns a module-level singleton created on first call. + +The fallback exists so module-federation remotes and standalone scripts see the same store the host app uses. + +## Tips + +- **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. +- **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. +- **`loadHistory` replaces.** It overwrites `history` with the fetched page (`take` controls page size, default 10); it does not merge with or accumulate onto existing entries. +- **`markAllAsRead` flips local state immediately.** The change is kept even if the server call fails — the failure is logged only (no rollback, no error toast) and reconciled on the next `loadHistory`. +- **Direct store mutations can bypass invariants.** The broadcast filter only runs through `ingest({ broadcast: true })`. Pushing arrays around `history.value` directly is not supported. +- **`createNotificationStore({ toastHandle })` is exported.** `useNotificationStore()` always builds a default store wired to the real toast controller. For tests or a custom shell, construct a store directly with `createNotificationStore({ toastHandle })` to inject a stub toast handler. + +## Related + +- [useBladeNotifications](./useBladeNotifications.md) — recommended scope-aware subscription. **Start here.** +- [useBroadcastFilter](./useBroadcastFilter.md) — broadcast acceptance gate. +- [useNotificationContext](./useNotificationContext.md) — payload access inside templates. +- [Notifications concept page.](../../concepts/notifications.md) +- [Notifications plugin reference.](../../plugins/notifications.md) diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/notifications.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/notifications.docs.md index a7e0eb735..3243462ad 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/notifications.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/notifications/notifications.docs.md @@ -51,22 +51,22 @@ Returns the shared `NotificationStore` singleton. Resolution order: ### `NotificationStore` Interface -| Member | Type | Description | -| ---------------------------- | -------------------------------------------- | -------------------------------------------------------------------- | -| `registry` | `Map` | Registered notification type configurations | -| `history` | `Ref` | All notifications (loaded from API + ingested) | -| `realtime` | `Ref` | Session-only notifications from SignalR | -| `unreadCount` | `ComputedRef` | Count of unread notifications in history | -| `hasUnread` | `ComputedRef` | Whether any unread notifications exist | -| `registerType(type, config)` | `(string, NotificationTypeConfig) => void` | Register a notification type with toast/template config | -| `ingest(message, opts?)` | `(PushNotification, {broadcast?}?) => void` | Process an incoming notification; broadcast messages are filtered | -| `setBroadcastFilter(fn)` | `((PushNotification) => boolean) => void` | Set filter for broadcast messages (SendSystemEvents) | -| `clearBroadcastFilter()` | `() => void` | Remove the broadcast filter | -| `markAsRead(message)` | `(PushNotification) => void` | Mark a single notification as read | -| `markAllAsRead()` | `() => Promise` | Optimistic mark-all-as-read with server sync and rollback on failure | -| `loadHistory(take?)` | `(number?) => Promise` | Load notification history from the API (default: 10) | -| `subscribe(opts)` | `({types, filter?, handler?}) => () => void` | Subscribe to notification types; returns unsubscribe function | -| `getByType(type)` | `(string) => PushNotification[]` | Filter history by `notifyType` | +| Member | Type | Description | +| ---------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `registry` | `Map` | Registered notification type configurations | +| `history` | `Ref` | All notifications (loaded from API + ingested) | +| `realtime` | `Ref` | Session-only notifications from SignalR | +| `unreadCount` | `ComputedRef` | Count of unread notifications in history | +| `hasUnread` | `ComputedRef` | Whether any unread notifications exist | +| `registerType(type, config)` | `(string, NotificationTypeConfig) => void` | Register a notification type with toast/template config | +| `ingest(message, opts?)` | `(PushNotification, {broadcast?}?) => void` | Process an incoming notification; broadcast messages are filtered | +| `setBroadcastFilter(fn)` | `((PushNotification) => boolean) => void` | Set filter for broadcast messages (SendSystemEvents) | +| `clearBroadcastFilter()` | `() => void` | Remove the broadcast filter | +| `markAsRead(message)` | `(PushNotification) => void` | Mark a single notification as read | +| `markAllAsRead()` | `() => Promise` | Mark-all-as-read: flips local state immediately and syncs to the server; server failure is logged only, no rollback | +| `loadHistory(take?)` | `(number?) => Promise` | Load notification history from the API (default: 10) | +| `subscribe(opts)` | `({types, filter?, handler?}) => () => void` | Subscribe to notification types; returns unsubscribe function | +| `getByType(type)` | `(string) => PushNotification[]` | Filter history by `notifyType` | **File:** `store.ts` @@ -150,7 +150,7 @@ clearBroadcastFilter(); ### Toast Controller -Handles toast popup display based on `NotificationTypeConfig.toast` settings. Created internally by the store. +Handles toast popup display based on `NotificationTypeConfig.toast` settings. Created internally by the store. `createToastController()` and `createNotificationStore(options?)` are both exported factories for custom shells and test harnesses — pass `createNotificationStore({ toastHandle })` to inject a stub toast handler. **File:** `toast-controller.ts` @@ -162,20 +162,40 @@ Handles toast popup display based on `NotificationTypeConfig.toast` settings. Cr | `"progress"` | Persistent toast updated on each message; auto-completes when `isComplete()` returns `true` | | `"silent"` | No toast displayed | +### Imperative `notification` API + +Low-level toast helper exported from `index.ts` for firing toasts directly (used by the Toast Controller for progress toasts and by framework internals). Call `notification(content, options?)` for a default toast, or use the typed helpers. + +**File:** `notification.ts` + +| Method | Description | +| --------------------------------- | --------------------------------------------------------------------------- | +| `notification(content, opts?)` | Show a default toast; returns the notification id | +| `notification.error(...)` | Show an error-styled toast | +| `notification.warning(...)` | Show a warning-styled toast | +| `notification.success(...)` | Show a success-styled toast | +| `notification.update(id, opts)` | Update an existing toast by id (used for progress toasts) | +| `notification.remove(id?)` | Dismiss one toast by id, or all if id omitted | +| `notification.clearAll()` | Dismiss all toasts | +| `notification.setPosition(pos)` | Set the default toast position | +| `notification.clearPosition(pos)` | Dismiss all toasts at the given position | +| `notification.debug()` | Log active toasts and default options; returns `{ active, defaultOptions }` | + ### Types **File:** `types.ts` -| Type | Description | -| ----------------------------- | --------------------------------------------------------------------------------------- | -| `Severity` | `"info" \| "warning" \| "error" \| "critical"` | -| `ToastConfig` | Toast behavior: mode, severity (static or function), timeout, isComplete, completedType | -| `NotificationTypeConfig` | Per-type config: optional Vue `template` component, `toast` config, `groupBy` field | -| `ModuleNotificationsConfig` | Record mapping `notifyType` strings to `NotificationTypeConfig` | -| `NotificationAction` | Action button in notification UI: label, icon, handler, visibility | -| `NotificationSubscription` | Internal subscriber record: id, types, filter, handler | -| `SEVERITY_TIMEOUTS` | Default timeouts: info=5s, warning=8s, error=persistent, critical=persistent | -| `EXCLUDED_NOTIFICATION_TYPES` | Types excluded from ingestion (e.g. `"IndexProgressPushNotification"`) | +| Type | Description | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `Severity` | `"info" \| "warning" \| "error" \| "critical"` | +| `ToastConfig` | Toast behavior: mode, severity (static or function), timeout, isComplete, completedType | +| `NotificationTypeConfig` | Per-type config: optional Vue `template` component, `toast` config, `groupBy` field | +| `ModuleNotificationsConfig` | Record mapping `notifyType` strings to `NotificationTypeConfig` | +| `NotificationAction` | Action button in notification UI: label, icon, handler, visibility | +| `NotificationSubscription` | Internal subscriber record: id, types, filter, handler | +| `SEVERITY_TIMEOUTS` | Default timeouts: info=5s, warning=8s, error=persistent, critical=persistent | +| `EXCLUDED_NOTIFICATION_TYPES` | Types excluded from ingestion (e.g. `"IndexProgressPushNotification"`) | +| `NotificationContextKey` | `InjectionKey>` — injection key `useNotificationContext()` reads to get the current template payload | ## Usage Examples @@ -233,13 +253,13 @@ const store = useNotificationStore(); await store.loadHistory(20); console.log(`${store.unreadCount.value} unread notifications`); -await store.markAllAsRead(); // optimistic update with rollback on failure +await store.markAllAsRead(); // flips local state immediately; server failure is logged only, no rollback ``` ## Related -- `framework/injection-keys.ts` -- `NotificationStoreKey`, `NotificationTemplatesKey` -- `framework/shared/components/notifications/` -- Notification dropdown UI +- `framework/injection-keys.ts` -- `NotificationStoreKey` +- `framework/shell/components/notification-dropdown/` -- Notification dropdown UI - `framework/core/api/platform.ts` -- `PushNotification`, `PushNotificationClient` diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md index 658a1860e..f7d9e1bd7 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md @@ -49,18 +49,37 @@ app.use(aiAgentPlugin, { ### `IAiAgentConfig` -| Field | Type | Default | Description | -| ---------------- | ---------- | ------------ | ------------------------------------------ | -| `url` | `string` | `""` | Chatbot iframe URL (required) | -| `title` | `string` | `"Virto OZ"` | Panel header title | -| `width` | `number` | `362` | Panel width in pixels | -| `expandedWidth` | `number` | `500` | Panel width when expanded | -| `allowedOrigins` | `string[]` | `["*"]` | Allowed origins for postMessage validation | +| Field | Type | Default | Description | +| ---------------- | ---------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `url` | `string` | `""` | Chatbot iframe URL (required) | +| `title` | `string` | `"Virto OZ"` | Panel header title | +| `width` | `number` | `362` | Panel width in pixels | +| `expandedWidth` | `number` | `500` | Panel width when expanded | +| `allowedOrigins` | `string[]` | `[]` | Origins accepted for **incoming** postMessage; the first entry is the target origin for **outbound** messages to the chatbot iframe. Required — see below | +| `parentOrigin` | `string` | -- | Explicit parent origin for **outbound** embedded postMessage. Required in embedded mode — see below | + +!!! danger "Origins must be configured — secure by default" +The bridge is locked down by default. Set both fields to explicit origins; a wildcard `"*"` or empty value is refused (the message is dropped and logged), never trusted. + +- **`allowedOrigins`** governs messages the shell **accepts**, and its first entry is the target origin the shell uses to **send** `INIT_CONTEXT`/`UPDATE_CONTEXT` to the chatbot iframe. With the default `[]`, all incoming postMessages are ignored and outbound iframe messages are dropped (these carry the access token, so they are never sent to `"*"`). Set it to the chatbot's origin(s), e.g. `["https://chat.example.com"]`. A `"*"` entry is rejected outright — replace it with explicit origins. +- **`parentOrigin`** governs messages the shell **sends to the parent frame** in embedded mode (`AI_CONTEXT_UPDATE`, `AI_TOGGLE_PANEL`, etc.). These payloads can carry the access token, so with no explicit `parentOrigin` (or `"*"`) the outbound message is dropped. Set it to the exact host origin, e.g. `"https://host.example.com"`. + +```ts +app.use(AiAgentPlugin, { + config: { + url: "https://chat.example.com", + allowedOrigins: ["https://chat.example.com"], // incoming + parentOrigin: "https://host.example.com", // outbound (embedded mode) + }, +}); +``` ### Composable: `useAiAgent()` Access the AI agent service from any component within the app. +Returns `UseAiAgentReturn | undefined`. When `provideAiAgentService()` has not run, `useAiAgent()` logs an error and returns `undefined`, so guard the result before destructuring (e.g. `const ai = useAiAgent(); ai?.togglePanel()`). + | Return | Type | Description | | ----------------- | ------------------------------------------- | ---------------------------------------------- | | `panelState` | `Ref<"closed" \| "open" \| "expanded">` | Current panel state | @@ -123,19 +142,19 @@ useAiAgentContext({ dataRef: selectedItems }); ### Toggling the Panel Programmatically ```typescript -const { togglePanel, isOpen } = useAiAgent(); +const ai = useAiAgent(); function onAiButtonClick() { - togglePanel(); + ai?.togglePanel(); } ``` ### Listening for Chatbot Messages ```typescript -const { onMessage } = useAiAgent(); +const ai = useAiAgent(); -onMessage((message) => { +ai?.onMessage((message) => { if (message.type === "NAVIGATE_TO_APP") { console.log("Chatbot wants to navigate to:", message.payload); } diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md index 0b72fc1e1..631fa4139 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md @@ -582,7 +582,7 @@ function defineExtensionPoint>(name: string, options interface DefineExtensionPointReturn { /** Sorted list of registered components (by priority, ascending) */ - components: ComputedRef>; + components: ComputedRef & { meta?: M }>>; /** True when at least one component is registered */ hasComponents: ComputedRef; } diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md index 25da03b28..33cee0ccd 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md @@ -33,7 +33,6 @@ All caught errors are parsed into human-readable messages via `parseError()` and ```typescript // Automatic -- called by the framework during app initialization. -import { setupGlobalErrorHandlers } from "@vc-shell/framework"; setupGlobalErrorHandlers(app); ``` @@ -150,6 +149,6 @@ try { ## Related - `framework/core/utilities/error.ts` -- `parseError()` function that extracts readable messages -- `framework/shared/components/notifications/core/notification.ts` -- `notification.error()` toast API +- `framework/core/notifications/notification.ts` -- `notification.error()` toast API - `framework/core/utilities/logger.ts` -- `createLogger()` for structured console logging -- `framework/shared/components/error-interceptor/` -- blade-level error boundary that intercepts errors before the global handler +- `framework/shell/components/error-interceptor/` -- blade-level error boundary that intercepts errors before the global handler diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md index cbc3077d4..a2db878e7 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md @@ -176,12 +176,12 @@ export default defineAppModule({ Key differences: -| | `createAppModule` | `defineAppModule` | -| ----------------- | --------------------------------------- | ---------------------------------- | -| API style | Positional args | Named options object | -| Notifications | `notificationTemplates` (legacy) | `notifications` (new typed config) | -| Global components | 4th arg registers via `app.component()` | Not supported (use provide/inject) | -| Status | **Deprecated** -- will be removed | **Current** -- use this | +| | `createAppModule` | `defineAppModule` | +| ----------------- | ----------------------------------------------------------------------------------- | ---------------------------------- | +| API style | Positional args | Named options object | +| Notifications | `notificationTemplates` (legacy) | `notifications` (new typed config) | +| Global components | 4th arg accepted for backward compatibility but ignored (no `app.component()` call) | Not supported (use provide/inject) | +| Status | **Deprecated** -- will be removed | **Current** -- use this | Migration is a one-line change: @@ -278,7 +278,7 @@ defineBlade({ title: "ORDERS.MENU.TITLE", // i18n key or plain string icon: "lucide-shopping-cart", // Icon name (lucide or fas) priority: 1, // Lower = higher in menu - permissions: ["seller:orders:view"], // Optional override + permissions: ["seller:orders:view"], // Optional fallback (used only when the blade has no permissions) }, }); @@ -317,7 +317,7 @@ defineBlade({ ``` -Under the hood, `defineAppModule` calls `addMenuItem()` from `useMenuService` with: +Under the hood, the BladeRegistry registration function (invoked during `defineAppModule`'s install) calls `addMenuItem()` from `useMenuService` with: ```typescript addMenuItem({ @@ -383,7 +383,7 @@ export default defineAppModule({ | Field | Type | Default | Description | | --------------- | ---------------------------------- | ----------------- | ------------------------------------------------------------------------------------- | | `mode` | `"auto" \| "progress" \| "silent"` | required | `auto` = show and auto-dismiss; `progress` = stay until complete; `silent` = no toast | -| `severity` | `Severity \| (msg) => Severity` | `"info"` | Toast type: `"info"`, `"success"`, `"warning"`, `"error"` | +| `severity` | `Severity \| (msg) => Severity` | `"info"` | Toast type: `"info"`, `"warning"`, `"error"`, `"critical"` | | `timeout` | `number` | varies | Auto-dismiss timeout in milliseconds | | `isComplete` | `(msg) => boolean` | `msg.finished` | For `progress` mode: when to close the toast | | `completedType` | `(msg) => "success" \| "error"` | `() => "success"` | For `progress` mode: final toast type | @@ -507,7 +507,7 @@ flowchart TD Non-OK manifest responses (401 / 403 / 404 / 5xx) are treated as "no plugins" — the host logs a `console.warn`, sets `modulesReady=true`, and mounts without extensions. Only network or parse failures set `modulesLoadError=true`. -See the [Module Federation guide](../guides/module-federation/index.md) for the full plugin-author + host-app walkthrough. +See [Plugin Loading (Module Federation)](#plugin-loading-module-federation) above and the `packages/mf-host/` package for the full plugin-author + host-app walkthrough. The host shares singleton instances of core dependencies (Vue, Vue Router, vue-i18n, @vc-shell/framework) so that remote modules use the same runtime. This is critical for reactivity, routing, and DI to work across module boundaries. @@ -853,12 +853,12 @@ interface DashboardWidget { **`MenuItemConfig`:** -| Field | Type | Description | -| ------------- | ---------- | --------------------------------------------------------------- | -| `title` | `string` | Display text or i18n key | -| `icon` | `string` | Icon name (lucide-_ or fas fa-_) | -| `priority` | `number` | Sort order (lower = higher in menu) | -| `permissions` | `string[]` | Permission override (optional, falls back to blade permissions) | +| Field | Type | Description | +| ------------- | ---------- | ---------------------------------------------------------------------------------------------------- | +| `title` | `string` | Display text or i18n key | +| `icon` | `string` | Icon name (lucide-_ or fas fa-_) | +| `priority` | `number` | Sort order (lower = higher in menu) | +| `permissions` | `string[]` | Optional fallback used only when the blade has no `permissions`; blade `permissions` take precedence | --- @@ -893,13 +893,13 @@ interface ToastConfig { ## Related -| Resource | Path | Description | -| ----------------------- | ------------------------------------- | ------------------------------------------------ | -| Extension Points Plugin | `core/plugins/extension-points/` | Cross-module UI extension system | -| BladeRegistry | `core/composables/useBladeRegistry/` | Where blades are stored and looked up | -| useMenuService | `core/composables/useMenuService/` | `addMenuItem()` for sidebar navigation | -| useDashboard | `core/composables/useDashboard/` | Dashboard widget management | -| NotificationStore | `core/notifications/` | Notification type registration and dispatch | -| i18n Plugin | `core/plugins/i18n/` | vue-i18n singleton for locale merging | -| mf-host | `packages/mf-host/` | Module Federation host that loads remote modules | -| Blade Navigation | `shared/components/blade-navigation/` | Blade stack rendering and navigation | +| Resource | Path | Description | +| ----------------------- | ------------------------------------ | ------------------------------------------------ | +| Extension Points Plugin | `core/plugins/extension-points/` | Cross-module UI extension system | +| BladeRegistry | `core/composables/useBladeRegistry/` | Where blades are stored and looked up | +| useMenuService | `core/composables/useMenuService/` | `addMenuItem()` for sidebar navigation | +| useDashboard | `core/composables/useDashboard/` | Dashboard widget management | +| NotificationStore | `core/notifications/` | Notification type registration and dispatch | +| i18n Plugin | `core/plugins/i18n/` | vue-i18n singleton for locale merging | +| mf-host | `packages/mf-host/` | Module Federation host that loads remote modules | +| Blade Navigation | `core/blade-navigation/` | Blade stack logic and navigation | diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md index 72786984a..23853fd7f 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md @@ -13,7 +13,7 @@ Permission-based access control plugin. Exposes a global `$hasAccess` helper for The VirtoCommerce platform uses a role-based permission system where each user is assigned one or more roles, and each role grants a set of named permissions (e.g., `order:read`, `order:create`, `catalog:manage`). The permissions plugin bridges this system into the Vue application layer so that UI elements can be shown or hidden based on the current user's access rights. -The plugin installs the `hasAccess()` function from the `usePermissions()` composable as both a Vue global property (`$hasAccess`) and a provide/inject injectable (`"$hasAccess"`). This means permission checks are available everywhere: in templates via `$hasAccess()`, in composition API code via `usePermissions()`, and in any injecting component. +The plugin installs the `hasAccess()` function from the `usePermissions()` composable as a Vue global property (`$hasAccess`). This means permission checks are available everywhere: in templates via `$hasAccess()` and in composition API code via `usePermissions()`. ## When to Use @@ -28,14 +28,13 @@ The framework installs this plugin automatically during app setup. Module develo ```typescript // Automatic -- installed by the framework during app setup. // The plugin source is straightforward: -import { usePermissions } from "@core/composables/usePermissions"; +import { usePermissions } from "@vc-shell/framework"; import { App } from "vue"; export const permissions = { install(app: App) { const { hasAccess } = usePermissions(); app.config.globalProperties.$hasAccess = hasAccess; - app.provide("$hasAccess", hasAccess); }, }; ``` diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md index 83fc76290..9a8922406 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md @@ -21,8 +21,8 @@ The VirtoCommerce platform uses ASP.NET SignalR to push real-time notifications The SignalR plugin establishes a persistent WebSocket connection to the platform hub and listens for two event channels: -- **`Send`** -- broadcast messages sent to all connected clients -- **`SendSystemEvents`** -- messages filtered by `creator` field, scoped to a specific user or organization +- **`Send`** -- targeted messages ingested with default routing +- **`SendSystemEvents`** -- ingested as broadcast (`store.ingest(message, { broadcast: true })`), no creator filtering Incoming messages are ingested into the `NotificationStore`, which dispatches them to registered notification type handlers and triggers toast notifications based on per-type configuration. @@ -32,38 +32,21 @@ The connection lifecycle is tied to the user's authentication state: it connects ```typescript // Automatic -- installed by the framework during app setup. -// The `creator` option is typically set to the current user/organization ID. -app.use(signalR, { creator: currentUserId }); +// The plugin takes no options. +app.use(signalR); ``` Module developers do not install this plugin. It is registered once by the framework during bootstrap. ## API -### Plugin Options - -| Option | Type | Description | -| --------- | --------------------- | -------------------------------------------------------------------------- | -| `creator` | `string \| undefined` | Filters `SendSystemEvents` messages to only those matching this creator ID | - ### Exports -| Export | Type | Description | -| ---------------------------- | -------------- | --------------------------------------------- | -| `signalR` | `Plugin` | Vue plugin object with `install()` method | -| `updateSignalRCreatorSymbol` | `InjectionKey` | Injection key for the creator update function | - -### Provided Injectables - -| Key | Type | Description | -| ---------------------------- | ---------------------------------------- | --------------------------------------------- | -| `updateSignalRCreatorSymbol` | `(creator: string \| undefined) => void` | Updates the SignalR creator filter at runtime | - -### Global Properties +| Export | Type | Description | +| --------- | -------- | ----------------------------------------- | +| `signalR` | `Plugin` | Vue plugin object with `install()` method | -| Property | Type | Description | -| ----------------------- | ---------------------------------------- | ------------------------------------------------------------------- | -| `$updateSignalRCreator` | `(creator: string \| undefined) => void` | Same as the injectable, accessible via `this.$updateSignalRCreator` | +The plugin installs with no options: `app.use(signalR)`. ## Connection Lifecycle @@ -80,7 +63,7 @@ sequenceDiagram Hub-->>C: "Send" (targeted) C->>Store: ingest(message) - Hub-->>C: "SendSystemEvents" (broadcast, filtered by creator) + Hub-->>C: "SendSystemEvents" (broadcast) C->>Store: ingest(message, { broadcast: true }) Store->>Store: toast • subscriber callbacks • history update @@ -98,24 +81,6 @@ sequenceDiagram ## Usage -### Updating the Creator Filter - -After login or when switching organizations, update the creator to scope system events: - -```typescript -import { inject } from "vue"; -import { updateSignalRCreatorSymbol } from "@vc-shell/framework"; - -const updateCreator = inject(updateSignalRCreatorSymbol); -updateCreator?.(currentUser.id); -``` - -Or via the global property in Options API (legacy): - -```typescript -this.$updateSignalRCreator(currentUser.id); -``` - ### How Messages Flow 1. Platform sends a `PushNotification` via SignalR @@ -156,13 +121,9 @@ cy.signalR("pushNotificationHub").invoke("Send", { }); ``` -## Tip: Creator vs. Broadcast - -If your notification type should reach all connected clients regardless of who triggered it, use the `Send` channel (the platform sends it as a broadcast). For user-scoped notifications (e.g., "your export is complete"), use `SendSystemEvents` with the `creator` field set to the user's ID on the server side. - -## Common Mistake +## Tip: Send vs. SendSystemEvents -Do not call `updateCreator` before the SignalR connection is established. The plugin watches the `currentCreator` ref and re-registers the `SendSystemEvents` handler only when the connection state is `"Connected"`. If you set the creator before authentication, it will be applied when the connection starts. +Both channels ingest into the same `NotificationStore`. `Send` messages are ingested with default routing; `SendSystemEvents` messages are ingested as broadcast (`{ broadcast: true }`). The plugin does not filter either channel by a creator field -- any scoping to a specific user or organization must happen server-side before the message is pushed. ## Related diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/validation/validation.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/validation/validation.docs.md index b182376d4..b8e2f7d33 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/plugins/validation/validation.docs.md @@ -21,7 +21,7 @@ This plugin auto-registers every rule from `@vee-validate/rules` (required, emai ## When to Use - Validate form fields in blades using `` components or `useField()` with declarative rules (`required`, `email`, `min`, etc.) -- Use custom vc-shell rules: image dimensions (`validateImageMinSize`), file weight, date comparisons, BigInt safety +- Use custom vc-shell rules: image dimensions (`mindimensions`), file weight, date comparisons, BigInt safety - When NOT to use: for API-level validation -- that belongs server-side; for simple presence checks on non-form data -- use plain conditionals The framework imports this module during setup. No manual installation is needed by module developers. @@ -34,11 +34,11 @@ The framework imports this module during setup. No manual installation is needed // Under the hood, the plugin does: import { defineRule } from "vee-validate"; -import * as allRules from "@vee-validate/rules"; +import { all } from "@vee-validate/rules"; // Register all standard rules -Object.entries(allRules).forEach(([name, rule]) => { - defineRule(name, rule); +Object.keys(all).forEach((rule) => { + defineRule(rule, all[rule]); }); // Then register custom vc-shell rules @@ -187,7 +187,7 @@ const { handleSubmit, errors } = useForm({ }, }); -const onSubmit = handleSubmit((values) => { +const onSubmit = handleSubmit(async (values) => { // values is fully validated here await saveProduct(values); }); @@ -197,12 +197,15 @@ const onSubmit = handleSubmit((values) => { All custom rules use localized error messages via `i18n.global.t()`. To customize messages, add the corresponding keys to your module's locale file: +`mindimensions` only treats files with a `.jpg`, `.svg`, `.jpeg`, `.png`, `.bmp`, or `.gif` extension as images; any other file fails with `messages.min_dimensions.not_image_error`. + ```typescript // locales/en.ts export default { messages: { min_dimensions: { error: "Image must be at least {width}x{height} pixels", + not_image_error: "File is not a supported image", }, file_weight: "File must not exceed {size} KB", before: "Date must be before {target}", @@ -241,5 +244,5 @@ Do not forget that `mindimensions` and `fileWeight` rules work with File objects - [vee-validate docs](https://vee-validate.logaretm.com/v4/) -- upstream validation library - `framework/core/plugins/i18n/` -- error messages use `i18n.global.t()` for localization -- Locale keys: `messages.min_dimensions.error`, `messages.file_weight`, `messages.before`, `messages.after`, `messages.bigint` +- Locale keys: `messages.min_dimensions.error`, `messages.min_dimensions.not_image_error`, `messages.file_weight`, `messages.before`, `messages.after`, `messages.bigint` - `framework/ui/components/molecules/vc-field/` -- VcField wrapper component for validation display diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/services/services.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/services/services.docs.md index 39c31d1bb..f9902cf9b 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/services/services.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/services/services.docs.md @@ -32,17 +32,17 @@ Every service follows the same architecture: ## Services -| Service | Factory | Module-level API | Key Type | -| ------------------------------ | ------------------------------------ | --------------------------------------------------------------- | ---------------------- | -| **MenuService** | `createMenuService()` | `addMenuItem()`, `removeRegisteredMenuItem()`, `setMenuBadge()` | `MenuItem` | -| **WidgetService** | `createWidgetService()` | `registerWidget()`, `registerExternalWidget()` | `IWidget` | -| **ToolbarService** | `createToolbarService()` | `registerToolbarItem()` | `IToolbarItem` | -| **DashboardService** | `createDashboardService()` | `registerDashboardWidget()` | `DashboardWidget` | -| **SettingsMenuService** | `createSettingsMenuService()` | `addSettingsMenuItem()` | `ISettingsMenuItem` | -| **AppBarWidgetService** | `createAppBarWidgetService()` | `addAppBarWidget()` | `AppBarWidget` | -| **AppBarMobileButtonsService** | `createAppBarMobileButtonsService()` | (direct API) | `AppBarButtonContent` | -| **GlobalSearchService** | `createGlobalSearchService()` | (direct API) | per-blade search state | -| **LanguageService** | `createLanguageService()` | (direct API) | locale strings | +| Service | Factory | Module-level API | Key Type | +| ------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **MenuService** | `createMenuService()` | `addMenuItem()`, `removeRegisteredMenuItem()`, `setMenuBadge()`, `removeMenuBadge()`, `getMenuBadge()`, `getMenuBadges()` | `MenuItem` | +| **WidgetService** | `createWidgetService()` | `registerWidget()`, `registerExternalWidget()` | `IWidget` / `IExternalWidgetRegistration` | +| **ToolbarService** | `createToolbarService()` | `registerToolbarItem()` | `IToolbarItem` | +| **DashboardService** | `createDashboardService({ hasAccess? })` | `registerDashboardWidget()` | `DashboardWidget` | +| **SettingsMenuService** | `createSettingsMenuService()` | `addSettingsMenuItem()` | `ISettingsMenuItem` | +| **AppBarWidgetService** | `createAppBarWidgetService()` | `addAppBarWidget()` | `AppBarWidget` | +| **AppBarMobileButtonsService** | `createAppBarMobileButtonsService()` | (direct API) | `AppBarButtonContent` | +| **GlobalSearchService** | `createGlobalSearchService()` | (direct API) | per-blade search state | +| **LanguageService** | `createLanguageService()` | (direct API) | locale strings | ## Usage @@ -59,7 +59,9 @@ addMenuItem({ priority: 10, }); -registerWidget({ id: "order-stats", component: OrderStatsWidget }, "OrderDetails"); +// The `component` field on IWidget is deprecated; prefer headless widgets (via +// `useBladeWidgets()`) or `registerExternalWidget()`. +registerWidget({ id: "order-stats", headless: { icon: "lucide-chart-bar" } }, "OrderDetails"); ``` ### Using inside a component @@ -76,7 +78,7 @@ const { toolbarItems } = useToolbar(); // toolbar items for current blade ```typescript import { setMenuBadge, removeMenuBadge } from "@vc-shell/framework"; -setMenuBadge("OrdersList", { value: computed(() => pendingCount.value) }); +setMenuBadge("OrdersList", { content: computed(() => pendingCount.value) }); removeMenuBadge("OrdersList"); ``` diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/types/types.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/types/types.docs.md index efe8daac5..be4554ac7 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/types/types.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/types/types.docs.md @@ -27,60 +27,58 @@ All types are re-exported from `@vc-shell/framework`, so module developers impor ### Menu System (`menu-types.ts`) -| Type | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `MenuItemConfig` | Configuration for registering a menu item: `title`, `icon`, `priority`, `permissions`, `group`/`groupConfig`, `badge`. | -| `MenuItemBadge` | Badge display config: `content` (static, ref, computed, or function), `variant`, `isDot`. | -| `MenuItemBadgeConfig` | Union: full `MenuItemBadge` object or shorthand `number \| string \| Ref \| ComputedRef \| function`. | -| `MenuItem` | Runtime menu item extending `MenuItemConfig` with `routeId`, `url`, `children[]`, `groupIcon`, `groupId`. | +| Type | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MenuItemConfig` | Configuration for registering a menu item: `title`, `icon`, `priority`, `permissions`, `groupConfig`, `badge`. (`group` and `inGroupPriority` are deprecated — use `groupConfig`.) | +| `MenuItemBadge` | Badge display config: `content` (static, ref, computed, or function), `variant`, `isDot`. | +| `MenuItemBadgeConfig` | Union: full `MenuItemBadge` object or shorthand `number \| string \| Ref \| ComputedRef \| function`. | +| `MenuItem` | Runtime menu item extending `MenuItemConfig` with `routeId`, `url`, `children[]`, `groupId`. (`groupIcon` is deprecated — use `groupConfig.icon`.) | ### Toolbar -| Type | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------ | -| `IBladeToolbar` | Toolbar button config: `id`, `icon`, `disabled`, `title`, `isVisible`, `clickHandler`, `separator`, `permissions`. | -| `ToolbarMenu` | Generic toolbar menu item that infers component props from the provided component type. | +| Type | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------ | +| `IBladeToolbar` | Toolbar button config: `id`, `icon`, `disabled`, `title`, `isVisible`, `clickHandler`, `separator`, `permissions`. | ### Table Columns -| Type | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ITableColumnsBase` | Column definition: `id`, `title`, `width`, `field`, `type` (money, date, image, status, etc.), `sortable`, `editable`, `rules`, `filter`, `mobilePosition`, `mobileVisible`. | -| `ITableColumns` | Union of `ITableColumnsBase` with specialized image and money column variants. | -| `IColumnFilterConfig` | Column filter: `true` (text), `string` (custom field), `{ options }` (select), `{ range }` (date range). | -| `IFilterOption` | Select filter option: `{ value, label }`. | +| Type | Description | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ITableColumnsBase` | Column definition: `id`, `title`, `width`, `field`, `type` (`money`, `date-ago`, `date`, `time`, `image`, `date-time`, `status`, `status-icon`, `number`, `link`, `html`), `sortable`, `editable`, `rules`, `filter`, `mobilePosition`, `mobileVisible`. | +| `ITableColumns` | Union of `ITableColumnsBase` with specialized image and money column variants. | +| `IColumnFilterConfig` | Column filter: `true` (text), `string` (custom field), `{ options }` (select), `{ range }` (date range). | +| `IFilterOption` | Select filter option: `{ value, label }`. | ### Assets -| Type | Description | -| ------------------ | ------------------------------------------------------------------------------------------ | -| `ICommonAsset` | Asset record: `url`, `relativeUrl`, `title`, `name`, `size`, `sortOrder`, `typeId`, dates. | -| `AssetsHandler` | Asset operations: `upload()`, `edit()`, `remove()`, `loading` ref, `noRemoveConfirmation`. | +| Type | Description | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ICommonAsset` | Alias of `AssetLike`: declares only `url`, `name`, `sortOrder` plus an open `[key: string]: any` index signature (other fields are untyped passthrough). **Deprecated** (removed in v3.0) — use `AssetLike`. | +| `AssetsHandler` | Asset operations: `upload()`, `edit()`, `remove()`, `loading` ref, `noRemoveConfirmation`. **Deprecated** (removed in v3.0) — use `UseAssetsManagerReturn`. | ### Service Abstractions (`services.ts`) -| Type | Description | -| ------------------------------ | -------------------------------------------------------------------------------------------------- | -| `ServiceLifecycle` | Service create/provide/cleanup lifecycle. | -| `PreregistrationQueue` | Queue for items registered before a service initializes: `register()`, `flush()`, `clear()`. | -| `RegistryService` | Generic registry: `register()`, `unregister()`, `get()`, `has()`, `getAll()`, `clear()`. | -| `ListService` | Reactive list: `items` ref, `add()`, `remove()`, `find()`, `filter()`. | -| `OrderedListService` | Extends `ListService` with `reorder()` and `getSorted()`. | -| `ObservableService` | Subscribe/getValue pattern for change observation. | -| `ServiceResult` | Async operation result: `success`, `data?`, `error?` with code/message/details. | -| `WidgetRegistration` | Widget entry: `id`, `component`, `props`, `order`, `isVisible`. | -| `MenuItemRegistration` | Menu item entry: `id`, `title`, `icon`, `priority`, `permissions`, `routeId`, `group`, `children`. | -| `ToolbarItemRegistration` | Toolbar item entry: `id`, `title`, `icon`, `order`, `disabled`, `clickHandler`, `permissions`. | +| Type | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------- | +| `ServiceLifecycle` | Service create/provide/cleanup lifecycle. | +| `PreregistrationQueue` | Queue for items registered before a service initializes: `register()`, `flush()`, `clear()`. | +| `RegistryService` | Generic registry: `register()`, `unregister()`, `get()`, `has()`, `getAll()`, `clear()`. | +| `ListService` | Reactive list: `items` ref, `add()`, `remove()`, `find()`, `filter()`. | +| `OrderedListService` | Extends `ListService` with `reorder()` and `getSorted()`. | +| `ObservableService` | Subscribe/getValue pattern for change observation. | +| `ServiceResult` | Async operation result: `success`, `data?`, `error?` with code/message/details. | +| `WidgetRegistration` | Widget entry: `id`, `component`, `props`, `order`, `isVisible`. | +| `MenuItemRegistration` | Menu item entry: `id`, `title`, `icon`, `priority`, `permissions`, `routeId`, `url`, `group`, `children`. | +| `ToolbarItemRegistration` | Toolbar item entry: `id`, `title`, `icon`, `order`, `disabled`, `clickHandler`, `permissions`. | ### Other -| Type | Description | -| --------------------------------- | --------------------------------------------------------------------------------------------- | -| `IBladeDropdownItem` | Blade dropdown option: `id`, `title`, `icon`, `clickHandler`. | -| `IMenuItem` | Generic menu item with optional component slot. | -| `NotificationTemplateConstructor` | Component constructor with a `notifyType` static field. | -| `IActionBuilderResult` | Row action definition: `icon`, `title`, `type` (danger/success/warning/info), `clickHandler`. | -| `RequestPasswordResult` | Password reset result: `succeeded`, `error`, `errorCode`. | +| Type | Description | +| ------------------------- | --------------------------------------------------------------------------------------------- | +| `IBladeDropdownItem` | Blade dropdown option: `id`, `title`, `icon`, `clickHandler`. | +| `IMenuItem` | Generic menu item with optional component slot. | +| `IActionBuilderResult` | Row action definition: `icon`, `title`, `type` (danger/success/warning/info), `clickHandler`. | +| `RequestPasswordResult` | Password reset result: `succeeded`, `error`, `errorCode`. | ## Usage Examples @@ -94,7 +92,6 @@ const columns: ITableColumns[] = [ id: "name", title: "Product Name", sortable: true, - type: "text", }, { id: "price", diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/date/date-utilities.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/date/date-utilities.docs.md index 4ff8df904..3a0951e21 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/date/date-utilities.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/date/date-utilities.docs.md @@ -103,5 +103,4 @@ The locale resolver tries the full code first (e.g., `pt-BR`), then the base lan ## Related -- `framework/ui/components/organisms/vc-table/components/cells/` -- cell renderers use these for date/datetime/date-ago columns -- `framework/shared/modules/assets-manager/` -- uses `formatDateRelative` for asset timestamps +- `framework/ui/components/organisms/vc-data-table/components/cells/` -- cell renderers use these for date/datetime/date-ago columns diff --git a/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/shared-utilities.docs.md b/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/shared-utilities.docs.md index a51aaaf5a..03a58bb19 100644 --- a/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/shared-utilities.docs.md +++ b/cli/vc-app-skill/runtime/knowledge/docs/core/utilities/shared-utilities.docs.md @@ -11,39 +11,39 @@ Helper functions and TypeScript type utilities used across the shared layer of t ## Overview -This directory provides small, focused utility modules for asset handling, color manipulation, badge formatting, and Vue component type extraction. +This directory provides small, focused utility modules for asset handling, color manipulation, and badge formatting. ## Exports -| Export | Module | Description | -| ----------------------------- | --------------------- | --------------------------------------------------------------------------- | -| `isImage(name)` | `assets.ts` | Returns `true` if the file extension is an image (png, jpg, jpeg, svg, gif) | -| `getFileThumbnail(name)` | `assets.ts` | Returns a Bootstrap icon class for the file type (e.g., `bi-filetype-pdf`) | -| `readableSize(bytes)` | `assets.ts` | Formats byte count as human-readable string (e.g., `"1.5 MB"`) | -| `convertColorNameToHex(name)` | `colorUtils.ts` | Converts a CSS color name to hex using the Canvas API | -| `isValidHexColor(value)` | `colorUtils.ts` | Validates a 6-digit hex color string | -| `normalizeHexColor(hex)` | `colorUtils.ts` | Ensures hex string starts with `#` | -| `formatBadgeCount(value)` | `formatBadgeCount.ts` | Truncates numbers > 99 to `"99+"` for badge display | -| `ComponentProps` | `vueUtils.ts` | Extracts props type from a Vue component | -| `ComponentSlots` | `vueUtils.ts` | Extracts slots type from a Vue component | -| `ComponentEmit` | `vueUtils.ts` | Extracts emit type from a Vue component | +| Export | Module | Description | +| ----------------------------- | --------------------- | ---------------------------------------------------------------------------- | +| `isImage(name)` | `assets.ts` | Returns `true` if the file extension is an image (png, jpg, jpeg, svg, gif) | +| `readableSize(bytes)` | `assets.ts` | Formats byte count as human-readable string (e.g., `"1.5 MB"`) | +| `getExtensionColor(name)` | `assets.ts` | Returns a CSS color for the file type, falling back to `var(--neutrals-400)` | +| `getExtensionLabel(name)` | `assets.ts` | Returns the uppercased file extension, or `"FILE"` when unknown | +| `convertColorNameToHex(name)` | `colorUtils.ts` | Converts a CSS color name to hex using the Canvas API | +| `isValidHexColor(value)` | `colorUtils.ts` | Validates a 6-digit hex color string | +| `normalizeHexColor(hex)` | `colorUtils.ts` | Ensures hex string starts with `#` | +| `formatBadgeCount(value)` | `formatBadgeCount.ts` | Truncates numbers > 99 to `"99+"` for badge display | ## Usage ### Asset helpers ```typescript -import { isImage, getFileThumbnail, readableSize } from "@vc-shell/framework"; +import { isImage, readableSize, getExtensionColor, getExtensionLabel } from "@vc-shell/framework"; isImage("photo.jpg"); // true isImage("document.pdf"); // false -getFileThumbnail("report.xlsx"); // "bi-filetype-xls" -getFileThumbnail("archive.zip"); // "bi-file-zip" -getFileThumbnail("unknown.abc"); // "bi-file-earmark" - readableSize(1536); // "1.5 KB" readableSize(0); // "0 Bytes" + +getExtensionColor("report.pdf"); // "#e74c3c" +getExtensionColor("unknown.abc"); // "var(--neutrals-400)" + +getExtensionLabel("report.xlsx"); // "XLSX" +getExtensionLabel(undefined); // "FILE" ``` ### Color utilities @@ -71,23 +71,14 @@ formatBadgeCount(150); // "99+" formatBadgeCount(undefined); // undefined ``` -### Vue type utilities - -```typescript -import type { ComponentProps, ComponentSlots } from "@vc-shell/framework"; - -type MyProps = ComponentProps; -type MySlots = ComponentSlots; -``` - ## Tips - `convertColorNameToHex` creates a temporary Canvas element -- only use in browser context, not SSR. - `readableSize` defaults to 2 decimal places; pass a second argument to change precision. - `formatBadgeCount` is used by `VcWidget` and `WidgetDropdownItem` for consistent badge truncation. -- The `vueUtils.ts` types work with both `defineComponent` and `