@vc-shell/vc-app-skill 2.0.0-alpha.31 → 2.0.0-alpha.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/composables/bladeContext/index.docs.md +111 -0
- package/runtime/knowledge/docs/core/composables/useBladeForm/useBladeForm.docs.md +113 -0
- package/runtime/knowledge/docs/core/composables/useBladeWidgets/index.docs.md +305 -0
- package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +83 -0
- package/runtime/knowledge/docs/core/utilities/thumbnail/thumbnail.docs.md +116 -0
- package/runtime/knowledge/docs/shell/components/change-password-button/change-password-button.docs.md +1 -1
- package/runtime/knowledge/docs/shell/pages/ChangePasswordPage/change-password-page.docs.md +0 -102
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# [2.0.0-alpha.32](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.31...v2.0.0-alpha.32) (2026-04-02)
|
|
2
|
+
|
|
3
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
4
|
+
|
|
1
5
|
# [2.0.0-alpha.31](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.30...v2.0.0-alpha.31) (2026-04-01)
|
|
2
6
|
|
|
3
7
|
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.32",
|
|
4
4
|
"description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
|
|
5
5
|
"bin": "./bin/install.cjs",
|
|
6
6
|
"files": [
|
package/runtime/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0.0-alpha.
|
|
1
|
+
2.0.0-alpha.32
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit fc29b1e1b on 2026-04-02T05:22:47.427Z
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# useBladeContext (defineBladeContext / injectBladeContext)
|
|
2
|
+
|
|
3
|
+
Exposes blade-level data to descendant widgets, extensions, and nested components via Vue's provide/inject mechanism. This pair of functions eliminates the need for prop drilling when child widgets or extension points need access to the parent blade's entity data, loading flags, or other shared state.
|
|
4
|
+
|
|
5
|
+
The pattern follows a "define once, inject anywhere" approach: the blade component calls `defineBladeContext` during setup, and any descendant (no matter how deeply nested) can call `injectBladeContext` to read that data reactively.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
- Share blade state (current entity, loading flags, disabled state) with child widgets without prop drilling
|
|
10
|
+
- Access parent blade data from an extension or widget component
|
|
11
|
+
- Expose selective fields to widgets (e.g., only the entity ID) via a computed getter
|
|
12
|
+
- When NOT to use: for cross-blade communication between sibling blades (use `useBlade` / blade messaging instead)
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// In a blade's <script setup>
|
|
18
|
+
import { defineBladeContext, injectBladeContext } from '@vc-shell/framework';
|
|
19
|
+
|
|
20
|
+
// Provide context — refs/computeds are auto-unwrapped for consumers
|
|
21
|
+
defineBladeContext({ item, disabled, loading });
|
|
22
|
+
|
|
23
|
+
// Or with a computed for selective exposure
|
|
24
|
+
defineBladeContext(computed(() => ({ id: item.value?.id })));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// In a widget or nested component
|
|
29
|
+
import { injectBladeContext } from '@vc-shell/framework';
|
|
30
|
+
|
|
31
|
+
const ctx = injectBladeContext();
|
|
32
|
+
// Refs are already unwrapped — access values directly, no .value needed
|
|
33
|
+
const entityId = computed(() => ctx.value.id as string);
|
|
34
|
+
const item = computed(() => ctx.value.item as { id: string; name: string });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### defineBladeContext
|
|
40
|
+
|
|
41
|
+
| Parameter | Type | Required | Description |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `data` | `MaybeRefOrGetter<Record<string, unknown>>` | Yes | Plain object, ref, or getter to expose |
|
|
44
|
+
|
|
45
|
+
Returns `void`. Must be called in the blade's `<script setup>`.
|
|
46
|
+
|
|
47
|
+
### injectBladeContext
|
|
48
|
+
|
|
49
|
+
Takes no parameters. Returns `ComputedRef<Record<string, unknown>>`.
|
|
50
|
+
|
|
51
|
+
Throws `InjectionError` if no ancestor blade has called `defineBladeContext`.
|
|
52
|
+
|
|
53
|
+
## Recipe: Widget Consuming Blade Context
|
|
54
|
+
|
|
55
|
+
A typical pattern is a sidebar widget that needs to load related data based on the current blade entity. The widget does not receive any props from the blade -- it reads the entity ID from the blade context:
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
// widgets/RelatedOrdersWidget.vue
|
|
60
|
+
import { computed, watch } from "vue";
|
|
61
|
+
import { injectBladeContext } from "@vc-shell/framework";
|
|
62
|
+
|
|
63
|
+
const ctx = injectBladeContext();
|
|
64
|
+
const customerId = computed(() => ctx.value.id as string | undefined);
|
|
65
|
+
|
|
66
|
+
// Reload orders whenever the customer changes
|
|
67
|
+
watch(customerId, async (id) => {
|
|
68
|
+
if (id) {
|
|
69
|
+
await loadOrders(id);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<script setup lang="ts">
|
|
77
|
+
// blades/CustomerDetailBlade.vue
|
|
78
|
+
import { ref, computed } from "vue";
|
|
79
|
+
import { defineBladeContext } from "@vc-shell/framework";
|
|
80
|
+
|
|
81
|
+
const customer = ref({ id: "cust-1", name: "Acme Corp" });
|
|
82
|
+
const loading = ref(false);
|
|
83
|
+
|
|
84
|
+
// Expose the customer data to all descendant widgets
|
|
85
|
+
defineBladeContext(computed(() => ({
|
|
86
|
+
id: customer.value?.id,
|
|
87
|
+
name: customer.value?.name,
|
|
88
|
+
loading: loading.value,
|
|
89
|
+
})));
|
|
90
|
+
</script>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Details
|
|
94
|
+
|
|
95
|
+
- **Automatic ref unwrapping**: `defineBladeContext` shallow-unwraps all ref/computed values in the provided object. Consumers get plain values directly (`ctx.value.item` instead of `ctx.value.item.value`). This works reactively — when the source ref changes, the context updates automatically.
|
|
96
|
+
- **Reactivity**: The provided context is always wrapped in a `computed`, so consumers receive a `ComputedRef` regardless of whether the provider passed a plain object, a ref, or a getter. Changes propagate automatically.
|
|
97
|
+
- **Injection key**: Uses `BladeContextKey` from `framework/injection-keys.ts`. This is a framework-level Symbol, so there is no risk of key collision with application code.
|
|
98
|
+
- **Error handling**: `injectBladeContext` throws an `InjectionError` with a descriptive message if called outside a blade component tree. This fails fast during development rather than silently returning `undefined`.
|
|
99
|
+
- **Scope**: The context is scoped to the Vue component subtree. Each blade in the stack has its own context, so nested blades do not leak data upward or sideways.
|
|
100
|
+
|
|
101
|
+
## Tips
|
|
102
|
+
|
|
103
|
+
- Prefer exposing a computed getter rather than the full reactive object when only a subset of fields is needed. This minimizes unnecessary re-renders in consuming widgets.
|
|
104
|
+
- The context value is untyped (`Record<string, unknown>`). Use type assertions or a typed wrapper in your module if you need type safety (e.g., `ctx.value.id as string`).
|
|
105
|
+
- If a blade does not call `defineBladeContext`, any descendant calling `injectBladeContext` will throw. Make sure all blades that host widgets define their context.
|
|
106
|
+
|
|
107
|
+
## Related
|
|
108
|
+
|
|
109
|
+
- `BladeContextKey` in `framework/injection-keys.ts`
|
|
110
|
+
- `useBladeWidgets` -- widgets that consume blade context
|
|
111
|
+
- `useBladeStack` -- manages the blade navigation stack
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# useBladeForm
|
|
2
|
+
|
|
3
|
+
Unified form state management for blades. Replaces manual combination of `useForm` + `useModificationTracker` + `useBeforeUnload` + `onBeforeClose` with a single composable.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { useBladeForm } from "@vc-shell/framework";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const { item, loadItem, saveItem } = useItemData();
|
|
15
|
+
|
|
16
|
+
const form = useBladeForm({ data: item });
|
|
17
|
+
|
|
18
|
+
onMounted(async () => {
|
|
19
|
+
await loadItem({ id: param.value });
|
|
20
|
+
form.setBaseline(); // snapshot current data as pristine
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Toolbar
|
|
24
|
+
const toolbar = ref<IBladeToolbar[]>([
|
|
25
|
+
{
|
|
26
|
+
id: "save",
|
|
27
|
+
title: "Save",
|
|
28
|
+
icon: "lucide-save",
|
|
29
|
+
disabled: computed(() => !form.canSave.value),
|
|
30
|
+
async clickHandler() {
|
|
31
|
+
await saveItem(item.value);
|
|
32
|
+
form.setBaseline(); // snapshot after save
|
|
33
|
+
callParent("reload");
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
|--------|------|---------|-------------|
|
|
45
|
+
| `data` | `Ref<T>` | required | Reactive data object for the form |
|
|
46
|
+
| `canSaveOverride` | `ComputedRef<boolean>` | — | Additional condition for canSave |
|
|
47
|
+
| `autoBeforeClose` | `boolean \| ComputedRef<boolean>` | `true` | Close guard behavior |
|
|
48
|
+
| `autoBeforeUnload` | `boolean` | `true` | Browser tab close guard |
|
|
49
|
+
| `closeConfirmMessage` | `MaybeRefOrGetter<string>` | — | Custom unsaved changes message |
|
|
50
|
+
| `onRevert` | `() => void \| Promise<void>` | — | Custom revert handler |
|
|
51
|
+
|
|
52
|
+
### Returns
|
|
53
|
+
|
|
54
|
+
| Property | Type | Description |
|
|
55
|
+
|----------|------|-------------|
|
|
56
|
+
| `setBaseline()` | `() => void` | Snapshot current data as pristine. Call after load and after save |
|
|
57
|
+
| `revert()` | `() => void \| Promise<void>` | Revert data to pristine (or call onRevert) |
|
|
58
|
+
| `canSave` | `ComputedRef<boolean>` | `isReady && valid && modified && canSaveOverride` |
|
|
59
|
+
| `isModified` | `ComputedRef<boolean>` | Data differs from pristine (false until setBaseline) |
|
|
60
|
+
| `isReady` | `ComputedRef<boolean>` | setBaseline() called at least once |
|
|
61
|
+
| `formMeta` | vee-validate meta | Form validation state |
|
|
62
|
+
| `setFieldError` | function | Set field error programmatically |
|
|
63
|
+
| `errorBag` | Ref | All field errors |
|
|
64
|
+
|
|
65
|
+
## Lifecycle
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Mount → Load data → setBaseline() → User edits → Save → setBaseline()
|
|
69
|
+
└→ Cancel → revert()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## VcBlade Integration
|
|
73
|
+
|
|
74
|
+
`useBladeForm` auto-provides form state to `VcBlade` via inject. No need to pass `:modified` prop:
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<!-- Before -->
|
|
78
|
+
<VcBlade :modified="isModified" :toolbar-items="toolbar">
|
|
79
|
+
|
|
80
|
+
<!-- After -->
|
|
81
|
+
<VcBlade :toolbar-items="toolbar">
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Advanced: Readonly Blade
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const disabled = computed(() => !!param.value && !item.value?.canBeModified);
|
|
88
|
+
|
|
89
|
+
const form = useBladeForm({
|
|
90
|
+
data: item,
|
|
91
|
+
canSaveOverride: computed(() => !disabled.value),
|
|
92
|
+
autoBeforeClose: computed(() => !disabled.value), // no prompt when readonly
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Advanced: Custom Revert
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const form = useBladeForm({
|
|
100
|
+
data: item,
|
|
101
|
+
onRevert: () => loadItem({ id: param.value }), // reload from server
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Constraints
|
|
106
|
+
|
|
107
|
+
- **Must be called from component `setup()`** (or `<script setup>`). Do NOT call from shared data-composables.
|
|
108
|
+
- Validation rules stay in template (`<Field rules="...">`).
|
|
109
|
+
- `setBaseline()` must be called after data is loaded — before that, `canSave` and `isModified` are `false`.
|
|
110
|
+
|
|
111
|
+
## Migration
|
|
112
|
+
|
|
113
|
+
See `MIGRATION_GUIDE.md` for step-by-step instructions on migrating existing modules.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# useBladeWidgets / useWidgetTrigger
|
|
2
|
+
|
|
3
|
+
Two composables for the widget system — one for the **blade side**, one for the **widget side**.
|
|
4
|
+
|
|
5
|
+
| Composable | Called from | Purpose |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `useBladeWidgets` | Blade component | Register headless widgets + get `refresh()` / `refreshAll()` |
|
|
8
|
+
| `useWidgetTrigger` | External widget component | Register trigger callbacks (`onRefresh`, `onClick`) via provide/inject |
|
|
9
|
+
|
|
10
|
+
Headless widgets are defined as plain configuration objects with reactive refs for dynamic values like badge counts and loading states. External component-based widgets use `useWidgetTrigger` to register their refresh callbacks so the hosting blade can trigger them.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- **`useBladeWidgets`**: Register sidebar widgets (counters, action buttons) for a blade without creating Vue components. Refresh widget data after blade operations (save, delete).
|
|
15
|
+
- **`useWidgetTrigger`**: Inside an external widget component (registered via `registerExternalWidget`) to register `onRefresh` / `onClick` callbacks. The blade can then call `refresh(widgetId)` or `refreshAll()` to trigger them.
|
|
16
|
+
- When NOT to use `useBladeWidgets`: for widgets that need their own template or complex UI (use `registerExternalWidget` + `useWidgetTrigger` instead).
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { useBladeWidgets } from '@vc-shell/framework';
|
|
22
|
+
|
|
23
|
+
const { refreshAll } = useBladeWidgets([
|
|
24
|
+
{
|
|
25
|
+
id: 'OffersWidget',
|
|
26
|
+
icon: 'lucide-tag',
|
|
27
|
+
title: 'OFFERS.TITLE',
|
|
28
|
+
badge: offersCount,
|
|
29
|
+
loading: offersLoading,
|
|
30
|
+
onClick: () => openBlade({ name: 'OffersList' }),
|
|
31
|
+
onRefresh: () => reloadOffers(),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'ReviewsWidget',
|
|
35
|
+
icon: 'lucide-star',
|
|
36
|
+
title: 'REVIEWS.TITLE',
|
|
37
|
+
badge: reviewsCount,
|
|
38
|
+
isVisible: computed(() => !!item.value?.id),
|
|
39
|
+
onClick: () => openBlade({ name: 'ReviewsList' }),
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// After a save, refresh all widget data
|
|
44
|
+
await saveEntity();
|
|
45
|
+
refreshAll();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Parameters
|
|
51
|
+
|
|
52
|
+
| Parameter | Type | Required | Description |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `widgets` | `HeadlessWidgetDeclaration[]` | Yes | Array of widget declarations |
|
|
55
|
+
|
|
56
|
+
### HeadlessWidgetDeclaration
|
|
57
|
+
|
|
58
|
+
| Field | Type | Required | Description |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| `id` | `string` | Yes | Unique widget identifier |
|
|
61
|
+
| `icon` | `string` | Yes | Icon name (e.g., `"lucide-tag"`) |
|
|
62
|
+
| `title` | `string` | Yes | i18n key or display title |
|
|
63
|
+
| `badge` | `Ref<number \| string>` | No | Badge counter value |
|
|
64
|
+
| `loading` | `Ref<boolean>` | No | Show loading indicator |
|
|
65
|
+
| `disabled` | `Ref<boolean> \| boolean` | No | Disable the widget |
|
|
66
|
+
| `isVisible` | `ComputedRef<boolean> \| boolean` | No | Toggle visibility |
|
|
67
|
+
| `onClick` | `() => void` | No | Action when widget is clicked |
|
|
68
|
+
| `onRefresh` | `() => void \| Promise<void>` | No | Called by `refresh(id)` or `refreshAll()` |
|
|
69
|
+
|
|
70
|
+
### Returns
|
|
71
|
+
|
|
72
|
+
| Property | Type | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `refresh` | `(widgetId: string) => void` | Trigger `onRefresh` on a specific widget |
|
|
75
|
+
| `refreshAll` | `() => void` | Trigger `onRefresh` on all widgets that have one |
|
|
76
|
+
|
|
77
|
+
## useWidgetTrigger
|
|
78
|
+
|
|
79
|
+
Widget-side composable for external component-based widgets. Registers a trigger contract (`onRefresh`, `onClick`, `badge`) via provide/inject — no props, IDs, or service knowledge required.
|
|
80
|
+
|
|
81
|
+
### Basic Usage
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { useWidgetTrigger } from '@vc-shell/framework';
|
|
85
|
+
|
|
86
|
+
// Inside an external widget component:
|
|
87
|
+
useWidgetTrigger({ onRefresh: loadData });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### IWidgetTrigger
|
|
91
|
+
|
|
92
|
+
| Field | Type | Required | Description |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `icon` | `string` | No | Lucide icon name for dropdown rendering |
|
|
95
|
+
| `title` | `string` | No | Display title (fallback: widget's title) |
|
|
96
|
+
| `badge` | `Ref<number \| string>` | No | Reactive badge value |
|
|
97
|
+
| `onClick` | `() => void` | No | Handler called when widget is clicked in dropdown |
|
|
98
|
+
| `onRefresh` | `() => void \| Promise<void>` | No | Handler called to refresh widget data |
|
|
99
|
+
| `disabled` | `Ref<boolean> \| boolean` | No | Disabled state |
|
|
100
|
+
|
|
101
|
+
### How It Works
|
|
102
|
+
|
|
103
|
+
1. `WidgetContainer` wraps each component-based widget in a `WidgetScope` provider
|
|
104
|
+
2. `WidgetScope` provides a `setTrigger` function scoped to the specific widget ID and blade ID
|
|
105
|
+
3. `useWidgetTrigger` injects this scope and calls `setTrigger` — no props or IDs needed
|
|
106
|
+
4. When the blade calls `refresh(widgetId)` or `refreshAll()`, the registered `onRefresh` is invoked
|
|
107
|
+
|
|
108
|
+
## Recipe: External Widget with Refresh
|
|
109
|
+
|
|
110
|
+
A complete example of an external widget that shows an unread message count and supports refresh from the blade:
|
|
111
|
+
|
|
112
|
+
**1. Register the external widget (module index.ts):**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { createAppModule, registerExternalWidget, BladeDescriptor } from "@vc-shell/framework";
|
|
116
|
+
import { markRaw } from "vue";
|
|
117
|
+
import { MessageWidget } from "./components/widgets";
|
|
118
|
+
|
|
119
|
+
registerExternalWidget({
|
|
120
|
+
id: "MessageWidget",
|
|
121
|
+
component: markRaw(MessageWidget),
|
|
122
|
+
targetBlades: ["ProductDetails", "OrderDetails"],
|
|
123
|
+
isVisible: (blade?: BladeDescriptor) => !!blade?.param,
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**2. Widget component (message-widget.vue):**
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<template>
|
|
131
|
+
<VcWidget
|
|
132
|
+
v-loading:500="loading"
|
|
133
|
+
:title="$t('MESSENGER.WIDGET.TITLE')"
|
|
134
|
+
icon="lucide-message-circle"
|
|
135
|
+
:value="messageCount"
|
|
136
|
+
@click="openMessageBlade"
|
|
137
|
+
/>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<script setup lang="ts">
|
|
141
|
+
import { ref, computed, onMounted } from "vue";
|
|
142
|
+
import {
|
|
143
|
+
loading as vLoading,
|
|
144
|
+
useBlade,
|
|
145
|
+
injectBladeContext,
|
|
146
|
+
useWidgetTrigger,
|
|
147
|
+
VcWidget,
|
|
148
|
+
} from "@vc-shell/framework";
|
|
149
|
+
|
|
150
|
+
const ctx = injectBladeContext();
|
|
151
|
+
const entityId = computed(() => (ctx.value.item as { id?: string })?.id ?? "");
|
|
152
|
+
|
|
153
|
+
const messageCount = ref(0);
|
|
154
|
+
const loading = ref(false);
|
|
155
|
+
|
|
156
|
+
const loadData = async () => {
|
|
157
|
+
loading.value = true;
|
|
158
|
+
try {
|
|
159
|
+
messageCount.value = await api.getUnreadCount(entityId.value);
|
|
160
|
+
} finally {
|
|
161
|
+
loading.value = false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Register refresh callback — blade can call refreshAll() after save
|
|
166
|
+
useWidgetTrigger({ onRefresh: loadData });
|
|
167
|
+
|
|
168
|
+
onMounted(() => {
|
|
169
|
+
if (entityId.value) loadData();
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**3. Blade refreshes widgets after save:**
|
|
175
|
+
|
|
176
|
+
```vue
|
|
177
|
+
<script setup lang="ts">
|
|
178
|
+
import { useBladeWidgets } from "@vc-shell/framework";
|
|
179
|
+
|
|
180
|
+
// Empty array — blade doesn't register headless widgets,
|
|
181
|
+
// but gets refresh/refreshAll for external widgets
|
|
182
|
+
const { refresh, refreshAll } = useBladeWidgets([]);
|
|
183
|
+
|
|
184
|
+
async function save() {
|
|
185
|
+
await api.saveProduct(product.value);
|
|
186
|
+
refreshAll(); // refresh all widgets (including MessageWidget)
|
|
187
|
+
// or: refresh("MessageWidget"); // refresh a specific widget by ID
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Recipe: Product Detail Blade with Multiple Widgets
|
|
193
|
+
|
|
194
|
+
```vue
|
|
195
|
+
<script setup lang="ts">
|
|
196
|
+
import { ref, computed } from "vue";
|
|
197
|
+
import { useBladeWidgets, defineBladeContext } from "@vc-shell/framework";
|
|
198
|
+
|
|
199
|
+
const product = ref({ id: "prod-1", name: "Widget A" });
|
|
200
|
+
const offersCount = ref(0);
|
|
201
|
+
const reviewsCount = ref(0);
|
|
202
|
+
const offersLoading = ref(false);
|
|
203
|
+
|
|
204
|
+
// Expose product data to widgets
|
|
205
|
+
defineBladeContext(computed(() => ({ id: product.value?.id })));
|
|
206
|
+
|
|
207
|
+
async function reloadOffers() {
|
|
208
|
+
offersLoading.value = true;
|
|
209
|
+
try {
|
|
210
|
+
const result = await api.searchOffers({ productId: product.value.id });
|
|
211
|
+
offersCount.value = result.totalCount;
|
|
212
|
+
} finally {
|
|
213
|
+
offersLoading.value = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { refreshAll } = useBladeWidgets([
|
|
218
|
+
{
|
|
219
|
+
id: "OffersWidget",
|
|
220
|
+
icon: "lucide-tag",
|
|
221
|
+
title: "PRODUCT.WIDGETS.OFFERS",
|
|
222
|
+
badge: offersCount,
|
|
223
|
+
loading: offersLoading,
|
|
224
|
+
isVisible: computed(() => !!product.value?.id),
|
|
225
|
+
onClick: () => openBlade({ name: "OffersList" }),
|
|
226
|
+
onRefresh: reloadOffers,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "ReviewsWidget",
|
|
230
|
+
icon: "lucide-star",
|
|
231
|
+
title: "PRODUCT.WIDGETS.REVIEWS",
|
|
232
|
+
badge: reviewsCount,
|
|
233
|
+
isVisible: computed(() => !!product.value?.id),
|
|
234
|
+
onClick: () => openBlade({ name: "ReviewsList" }),
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
async function save() {
|
|
239
|
+
await api.saveProduct(product.value);
|
|
240
|
+
// Refresh all widget counts after saving
|
|
241
|
+
refreshAll();
|
|
242
|
+
}
|
|
243
|
+
</script>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Prerequisites
|
|
247
|
+
|
|
248
|
+
**`useBladeWidgets`**:
|
|
249
|
+
- Must be called inside a blade component rendered by `VcBladeSlot` (requires `BladeDescriptorKey` injection).
|
|
250
|
+
- `WidgetService` must be provided in the component tree (automatically available in vc-shell apps).
|
|
251
|
+
- Calling outside a blade context throws an error with a descriptive message.
|
|
252
|
+
|
|
253
|
+
**`useWidgetTrigger`**:
|
|
254
|
+
- Must be called inside a widget component rendered by `WidgetContainer` (requires `WidgetScopeKey` injection).
|
|
255
|
+
- If called outside a widget scope, logs a warning and does nothing (does not throw).
|
|
256
|
+
|
|
257
|
+
## Details
|
|
258
|
+
|
|
259
|
+
- **Lifecycle management**: Widgets are registered in `onMounted` and unregistered in `onUnmounted`. This ensures the WidgetService always reflects the currently visible blades.
|
|
260
|
+
- **Blade ID resolution**: The composable injects `BladeDescriptorKey` to determine which blade the widgets belong to. Each blade has its own isolated widget list.
|
|
261
|
+
- **Trigger pattern**: The `onRefresh` callback is stored as a `trigger` on the registered widget. When `refresh(id)` or `refreshAll()` is called, the trigger is invoked. Widgets without `onRefresh` are silently skipped.
|
|
262
|
+
|
|
263
|
+
## Tips
|
|
264
|
+
|
|
265
|
+
- Use `refreshAll()` after any blade operation that might change widget badge counts (save, delete, import).
|
|
266
|
+
- The `badge` field accepts both numbers and strings. Use a string for non-numeric badges like "New" or "!".
|
|
267
|
+
- Keep widget IDs unique within a blade. Duplicate IDs will overwrite previous registrations.
|
|
268
|
+
- Combine with `defineBladeContext` to expose blade entity data that widget components (non-headless) can consume via `injectBladeContext`.
|
|
269
|
+
|
|
270
|
+
## Common Mistakes
|
|
271
|
+
|
|
272
|
+
### Calling useWidgetTrigger outside WidgetContainer scope
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Wrong — called in a standalone component, not rendered inside a blade widget slot
|
|
276
|
+
export default defineComponent({
|
|
277
|
+
setup() {
|
|
278
|
+
useWidgetTrigger({ onRefresh: loadData }); // ⚠️ Logs warning, trigger not registered
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Correct — called inside a widget component registered via registerExternalWidget
|
|
285
|
+
// and rendered by WidgetContainer within a blade
|
|
286
|
+
useWidgetTrigger({ onRefresh: loadData }); // ✓ WidgetScope provides context
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Forgetting to pass empty array to useBladeWidgets for refresh-only usage
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Wrong — useBladeWidgets requires an array argument
|
|
293
|
+
const { refreshAll } = useBladeWidgets(); // TS error
|
|
294
|
+
|
|
295
|
+
// Correct — pass empty array when you only need refresh/refreshAll
|
|
296
|
+
const { refreshAll } = useBladeWidgets([]);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Related
|
|
300
|
+
|
|
301
|
+
- `defineBladeContext` / `injectBladeContext` -- expose/consume blade data in external widgets
|
|
302
|
+
- `registerExternalWidget` -- register a component-based widget globally for target blades
|
|
303
|
+
- `WidgetService` in `@core/services/widget-service` -- underlying service
|
|
304
|
+
- `WidgetScope` in `vc-blade/_internal/widgets/WidgetScope.vue` -- provides `WidgetScopeKey` to widget components
|
|
305
|
+
- `VcBladeSlot` -- the blade wrapper that provides `BladeDescriptorKey`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# useMenuExpanded
|
|
2
|
+
|
|
3
|
+
Manages the sidebar menu expanded/collapsed and hover-expanded state with localStorage persistence. This is the low-level composable that powers the sidebar pin/unpin behavior in the vc-shell admin UI. It tracks two independent states: the permanent "pinned" state (persisted across sessions) and the transient "hover-expanded" state (active only while the user hovers over a collapsed sidebar).
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- Low-level control over sidebar pin and hover state
|
|
8
|
+
- Building a custom sidebar component that needs expand/collapse persistence
|
|
9
|
+
- When NOT to use: prefer `useSidebarState` which wraps this composable and adds mobile menu support, derived `isExpanded` computed, and responsive breakpoint handling
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { useMenuExpanded } from '@vc-shell/framework';
|
|
15
|
+
|
|
16
|
+
const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
|
|
17
|
+
|
|
18
|
+
// Toggle pin state (persisted to localStorage)
|
|
19
|
+
toggleExpanded();
|
|
20
|
+
|
|
21
|
+
// Hover expand with 200ms delay
|
|
22
|
+
toggleHoverExpanded(true); // opens after delay
|
|
23
|
+
toggleHoverExpanded(false); // closes immediately
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
### Returns
|
|
29
|
+
|
|
30
|
+
| Property | Type | Description |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `isExpanded` | `Ref<boolean>` | Pinned state, persisted via `useLocalStorage` |
|
|
33
|
+
| `toggleExpanded` | `() => void` | Toggle the pinned state |
|
|
34
|
+
| `isHoverExpanded` | `Ref<boolean>` | Temporary hover expansion (not persisted) |
|
|
35
|
+
| `toggleHoverExpanded` | `(shouldExpand?: boolean) => void` | Set hover state; opening has a 200ms delay, closing is immediate |
|
|
36
|
+
|
|
37
|
+
## Recipe: Custom Sidebar with Hover Expand
|
|
38
|
+
|
|
39
|
+
A common pattern is binding mouse events on the sidebar rail so the menu previews its full width on hover, without permanently pinning it open:
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed } from "vue";
|
|
44
|
+
import { useMenuExpanded } from "@vc-shell/framework";
|
|
45
|
+
|
|
46
|
+
const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
|
|
47
|
+
|
|
48
|
+
// The sidebar is visually expanded if pinned OR hover-expanded
|
|
49
|
+
const isVisuallyExpanded = computed(() => isExpanded.value || isHoverExpanded.value);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<aside
|
|
54
|
+
:class="{ 'sidebar--expanded': isVisuallyExpanded }"
|
|
55
|
+
@mouseenter="toggleHoverExpanded(true)"
|
|
56
|
+
@mouseleave="toggleHoverExpanded(false)"
|
|
57
|
+
>
|
|
58
|
+
<nav>
|
|
59
|
+
<!-- menu items -->
|
|
60
|
+
</nav>
|
|
61
|
+
<button @click="toggleExpanded">
|
|
62
|
+
{{ isExpanded ? 'Unpin' : 'Pin' }}
|
|
63
|
+
</button>
|
|
64
|
+
</aside>
|
|
65
|
+
</template>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Details
|
|
69
|
+
|
|
70
|
+
- **Storage key scoping**: The key is scoped per application using the first URL path segment: `VC_APP_MENU_EXPANDED_{appName}`. For example, if the app is hosted at `/vendor-portal/`, the key is `VC_APP_MENU_EXPANDED_vendor-portal`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
|
|
71
|
+
- **Hover delay**: Opening uses a 200ms debounce to prevent accidental expansion when the cursor briefly passes over the sidebar. Closing is immediate to feel responsive.
|
|
72
|
+
- **Cleanup**: Pending hover timeouts are cleaned up via `onScopeDispose` to prevent memory leaks when the composable's effect scope is destroyed.
|
|
73
|
+
- **Default state**: The sidebar starts pinned open (`true`) on first visit, which is the most user-friendly default for new users.
|
|
74
|
+
|
|
75
|
+
## Tips
|
|
76
|
+
|
|
77
|
+
- If you call `toggleHoverExpanded(true)` and then `toggleHoverExpanded(false)` within the 200ms window, the expansion is canceled -- the timeout is cleared before it fires.
|
|
78
|
+
- The composable does not handle mobile breakpoints. For responsive behavior, use `useSidebarState` instead, which combines `useMenuExpanded` with mobile detection.
|
|
79
|
+
- Each call to `useMenuExpanded()` creates a new instance with its own timeout tracking, but they share the same localStorage-backed `isExpanded` ref (via `useLocalStorage`). Multiple instances will stay in sync for the pinned state.
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- `useSidebarState` -- higher-level composable that adds mobile menu and derived `isExpanded` computed
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Thumbnail URL Utility
|
|
2
|
+
|
|
3
|
+
Transforms full-size image URLs into thumbnail variants by appending size suffixes before the file extension. VirtoCommerce backend generates thumbnails automatically with these suffixes.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- Use in any component that displays images at a known size smaller than the original
|
|
8
|
+
- Reduces bandwidth and improves page load time significantly
|
|
9
|
+
- Especially important for image lists (tables, galleries, cards)
|
|
10
|
+
|
|
11
|
+
## Available Sizes
|
|
12
|
+
|
|
13
|
+
### Named Presets
|
|
14
|
+
|
|
15
|
+
| Preset | Use Case |
|
|
16
|
+
|--------|----------|
|
|
17
|
+
| `sm` | Small icons, table cells, avatar thumbnails |
|
|
18
|
+
| `md` | Medium previews, cards |
|
|
19
|
+
| `lg` | Large previews, hero images |
|
|
20
|
+
|
|
21
|
+
### Pixel Sizes
|
|
22
|
+
|
|
23
|
+
| Size | Pixels | Use Case |
|
|
24
|
+
|------|--------|----------|
|
|
25
|
+
| `64x64` | 64px | Table cells, tiny thumbnails |
|
|
26
|
+
| `128x128` | 128px | Small tiles, list items |
|
|
27
|
+
| `168x168` | 168px | Medium tiles |
|
|
28
|
+
| `216x216` | 216px | Gallery tiles (md) |
|
|
29
|
+
| `348x348` | 348px | Large gallery tiles |
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### `getThumbnailUrl(url, size?)`
|
|
34
|
+
|
|
35
|
+
Transforms an image URL by inserting a size suffix before the file extension.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { getThumbnailUrl } from "@core/utilities/thumbnail";
|
|
39
|
+
|
|
40
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg", "sm")
|
|
41
|
+
// → "https://cdn.example.com/photo_sm.jpg"
|
|
42
|
+
|
|
43
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg", "128x128")
|
|
44
|
+
// → "https://cdn.example.com/photo_128x128.jpg"
|
|
45
|
+
|
|
46
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg")
|
|
47
|
+
// → "https://cdn.example.com/photo.jpg" (unchanged)
|
|
48
|
+
|
|
49
|
+
getThumbnailUrl(undefined, "sm")
|
|
50
|
+
// → undefined
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Parameters:**
|
|
54
|
+
- `url` — Original image URL (string or undefined)
|
|
55
|
+
- `size` — Thumbnail size preset or pixel dimensions (optional)
|
|
56
|
+
|
|
57
|
+
**Returns:** Transformed URL, or original URL if size not specified
|
|
58
|
+
|
|
59
|
+
### `getBestThumbnailSize(displaySize)`
|
|
60
|
+
|
|
61
|
+
Maps a CSS pixel display size to the best-fit thumbnail preset. Picks the smallest thumbnail that is >= the display size.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { getBestThumbnailSize } from "@core/utilities/thumbnail";
|
|
65
|
+
|
|
66
|
+
getBestThumbnailSize(48) // → "64x64"
|
|
67
|
+
getBestThumbnailSize(96) // → "128x128"
|
|
68
|
+
getBestThumbnailSize(200) // → "216x216"
|
|
69
|
+
getBestThumbnailSize(500) // → "lg"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage in Components
|
|
73
|
+
|
|
74
|
+
### VcImage
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<VcImage
|
|
78
|
+
:src="product.imgSrc"
|
|
79
|
+
thumbnail-size="sm"
|
|
80
|
+
size="s"
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### VcGallery
|
|
85
|
+
|
|
86
|
+
Gallery auto-maps `size` prop to thumbnail size. Override with `thumbnailSize`:
|
|
87
|
+
|
|
88
|
+
```vue
|
|
89
|
+
<!-- Auto: sm→128x128, md→216x216, lg→348x348 -->
|
|
90
|
+
<VcGallery :images="images" size="md" />
|
|
91
|
+
|
|
92
|
+
<!-- Explicit override -->
|
|
93
|
+
<VcGallery :images="images" thumbnail-size="128x128" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Gallery preview uses `64x64` for the thumbnail strip and full-size for the main image automatically.
|
|
97
|
+
|
|
98
|
+
### VcDataTable (CellImage)
|
|
99
|
+
|
|
100
|
+
Table image cells use `sm` thumbnail by default — no action needed.
|
|
101
|
+
|
|
102
|
+
## Types
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type ThumbnailPreset = "sm" | "md" | "lg";
|
|
106
|
+
type ThumbnailPixelSize = "64x64" | "128x128" | "168x168" | "216x216" | "348x348";
|
|
107
|
+
type ThumbnailSize = ThumbnailPreset | ThumbnailPixelSize;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- The utility only transforms the URL — the backend must have generated the thumbnail for the URL to resolve
|
|
113
|
+
- If a thumbnail doesn't exist on the server, the browser will show a broken image. Ensure your asset pipeline generates all required sizes.
|
|
114
|
+
- The suffix is inserted before the file extension: `photo.jpg` → `photo_sm.jpg`
|
|
115
|
+
- URLs without a file extension are returned unchanged
|
|
116
|
+
- Already-suffixed URLs are not double-transformed
|
|
@@ -91,4 +91,4 @@ export default {
|
|
|
91
91
|
- [SettingsMenu](../settings-menu/settings-menu.docs.md) -- parent container
|
|
92
92
|
- [SettingsMenuItem](../settings-menu-item/settings-menu-item.docs.md) -- base menu item used internally
|
|
93
93
|
- [LogoutButton](../logout-button/logout-button.docs.md) -- sibling account action
|
|
94
|
-
- [ChangePasswordPage](../../
|
|
94
|
+
- [ChangePasswordPage](../../auth/ChangePasswordPage/change-password-page.docs.md) -- full-page variant for expired passwords
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# ChangePasswordPage
|
|
2
|
-
|
|
3
|
-
Change password page with current, new, and confirm password fields. Supports a `forced` mode for expired passwords that displays an info banner and is triggered by post-login redirect. This full-page variant is used when the user must change their password before accessing the application (e.g., expired password policy). For voluntary password changes from within the app, the `ChangePasswordButton` in the settings menu opens a popup instead.
|
|
4
|
-
|
|
5
|
-
## When to Use
|
|
6
|
-
|
|
7
|
-
- When a signed-in user wants to change their password (full-page flow)
|
|
8
|
-
- In `forced` mode after login when the user's password has expired
|
|
9
|
-
- The standard vc-shell routing maps `/change-password` to this page
|
|
10
|
-
|
|
11
|
-
## Basic Usage
|
|
12
|
-
|
|
13
|
-
```vue
|
|
14
|
-
<template>
|
|
15
|
-
<ChangePassword />
|
|
16
|
-
</template>
|
|
17
|
-
|
|
18
|
-
<!-- Forced mode (expired password) -->
|
|
19
|
-
<template>
|
|
20
|
-
<ChangePassword forced />
|
|
21
|
-
</template>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
With custom branding:
|
|
25
|
-
|
|
26
|
-
```vue
|
|
27
|
-
<template>
|
|
28
|
-
<ChangePassword
|
|
29
|
-
forced
|
|
30
|
-
logo="/assets/my-company-logo.svg"
|
|
31
|
-
background="/assets/custom-background.jpg"
|
|
32
|
-
/>
|
|
33
|
-
</template>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Key Props
|
|
37
|
-
|
|
38
|
-
| Prop | Type | Default | Description |
|
|
39
|
-
|------|------|---------|-------------|
|
|
40
|
-
| `forced` | `boolean` | `false` | Show expired-password info banner and adjusted title |
|
|
41
|
-
| `logo` | `string` | - | Override logo image URL |
|
|
42
|
-
| `background` | `string` | - | Custom background image URL |
|
|
43
|
-
|
|
44
|
-
## Recipe: Router Configuration with Forced Mode
|
|
45
|
-
|
|
46
|
-
Set up the route so the login page can redirect here when the password is expired:
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import ChangePassword from "@vc-shell/framework/shared/pages/ChangePasswordPage";
|
|
50
|
-
|
|
51
|
-
const routes = [
|
|
52
|
-
{
|
|
53
|
-
path: "/change-password",
|
|
54
|
-
name: "ChangePassword",
|
|
55
|
-
component: ChangePassword,
|
|
56
|
-
props: (route) => ({
|
|
57
|
-
forced: route.query.forced === "true",
|
|
58
|
-
}),
|
|
59
|
-
},
|
|
60
|
-
];
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
In the login flow, redirect when the user's password has expired:
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
async function handleLogin() {
|
|
67
|
-
const result = await signIn(username.value, password.value);
|
|
68
|
-
if (result.passwordExpired) {
|
|
69
|
-
router.push({ name: "ChangePassword", query: { forced: "true" } });
|
|
70
|
-
} else {
|
|
71
|
-
router.push("/");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Features
|
|
77
|
-
|
|
78
|
-
- **Real-time password policy validation**: Uses `useUserManagement().validatePassword` to check the new password against the platform's policy as the user types (minimum length, required uppercase, lowercase, digits, special characters)
|
|
79
|
-
- **Equal password detection**: Shows a specific "Equal-passwords" error when the new password matches the current password, without making an API call
|
|
80
|
-
- **Confirm-password mismatch detection**: Validates that the new password and confirm password fields match
|
|
81
|
-
- **Forced mode banner**: When `forced` is `true`, displays an info banner explaining that the password has expired and must be changed
|
|
82
|
-
- **Cancel behavior**: Cancel button signs out the user and redirects to `/login`
|
|
83
|
-
- **Success redirect**: On successful password change, redirects to `/` (main application)
|
|
84
|
-
|
|
85
|
-
## Details
|
|
86
|
-
|
|
87
|
-
- **Auth layout**: Renders inside `VcAuthLayout`, providing the centered card design with logo and background.
|
|
88
|
-
- **Password policy**: The validation rules are fetched from the platform API and include configurable requirements (minimum length, character classes, etc.). The component displays these rules as a checklist.
|
|
89
|
-
- **Forced vs voluntary**: In forced mode (`forced=true`), the page title changes to reflect the expired password scenario, and an info banner explains why the change is required. The form fields and behavior are otherwise identical.
|
|
90
|
-
- **Sign-out on cancel**: If the user cancels during a forced password change, they are signed out. This prevents access to the application with an expired password.
|
|
91
|
-
|
|
92
|
-
## Tips
|
|
93
|
-
|
|
94
|
-
- The `forced` prop is typically set via a route query parameter, not hardcoded. The login page detects expired passwords and redirects with the appropriate flag.
|
|
95
|
-
- Password policy validation runs on keyup, providing immediate feedback. The submit button is disabled until all policy requirements are met and the passwords match.
|
|
96
|
-
- This page is distinct from the `ChangePasswordButton` popup. The page is for full-screen flow (forced changes), while the popup is for voluntary in-app password changes.
|
|
97
|
-
|
|
98
|
-
## Related Components
|
|
99
|
-
|
|
100
|
-
- **VcAuthLayout** - The underlying centered card layout
|
|
101
|
-
- **LoginPage** - Redirects here when `user.passwordExpired` is true
|
|
102
|
-
- **ChangePasswordButton** - Settings menu popup variant for voluntary password changes
|