@vc-shell/vc-app-skill 2.0.0-alpha.30 → 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 +8 -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/useBladeForm/useBladeForm.docs.md +113 -0
- package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md +104 -105
- package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +4 -4
- 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/ui/components/atoms/vc-image/vc-image.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-image-tile/vc-image-tile.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +27 -2
- package/runtime/knowledge/docs/ui/components/organisms/vc-gallery/vc-gallery.docs.md +55 -16
- package/runtime/knowledge/docs/core/composables/useGlobalSearch/useGlobalSearch.docs.md +0 -146
- package/runtime/knowledge/docs/shell/pages/ChangePasswordPage/change-password-page.docs.md +0 -102
- /package/runtime/knowledge/docs/core/composables/{useBladeContext.docs.md → bladeContext/index.docs.md} +0 -0
- /package/runtime/knowledge/docs/core/composables/{useBladeWidgets.docs.md → useBladeWidgets/index.docs.md} +0 -0
- /package/runtime/knowledge/docs/core/composables/{useMenuExpanded.docs.md → useMenuExpanded/index.docs.md} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
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)
|
|
6
|
+
|
|
7
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
8
|
+
|
|
1
9
|
# [2.0.0-alpha.30](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.29...v2.0.0-alpha.30) (2026-03-30)
|
|
2
10
|
|
|
3
11
|
|
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,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.
|
package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md
CHANGED
|
@@ -1,163 +1,162 @@
|
|
|
1
1
|
# useDynamicProperties
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable for managing dynamic (runtime-defined) property values with support for multilanguage, multivalue, dictionary, color, and measurement property types.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Internally uses a strategy pattern — each property type (regular, boolean, dictionary, measure, color) has a dedicated handler with `get`/`set` methods.
|
|
6
6
|
|
|
7
7
|
## When to Use
|
|
8
8
|
|
|
9
9
|
- In product, category, or any entity detail blades that display platform dynamic properties
|
|
10
10
|
- When properties are defined at runtime (not compile-time) and may be multilanguage or dictionary-based
|
|
11
11
|
- When you need to render and edit property values that can be text, boolean, number, datetime, dictionary selection, color picker, or measurement with units
|
|
12
|
-
- When NOT to use: for static, compile-time form fields, use standard Vue reactive state
|
|
12
|
+
- When NOT to use: for static, compile-time form fields, use standard Vue reactive state
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
import { useDynamicProperties } from
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
|
|
24
|
-
PropertyDictionaryItem,
|
|
25
|
-
(measureId, locale) => api.searchMeasurements(measureId, locale),
|
|
26
|
-
);
|
|
17
|
+
import { useDynamicProperties } from "@vc-shell/framework";
|
|
18
|
+
|
|
19
|
+
const { getPropertyValue, setPropertyValue, loadDictionaries, loadMeasurements, loading } =
|
|
20
|
+
useDynamicProperties({
|
|
21
|
+
searchDictionary: (criteria) => api.searchPropertyDictionaryItems(criteria),
|
|
22
|
+
searchMeasurements: (measureId, locale) => api.searchMeasurements(measureId, locale),
|
|
23
|
+
});
|
|
27
24
|
```
|
|
28
25
|
|
|
29
26
|
## API
|
|
30
27
|
|
|
31
|
-
###
|
|
28
|
+
### Options
|
|
32
29
|
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `PropertyDictionaryItemConstructor` | `new (data?) => TPropertyDictionaryItem` | Yes | Constructor class for creating dictionary item instances. Used when localizing dictionary items. |
|
|
38
|
-
| `searchMeasurementFunction` | `(measureId, locale?) => Promise<TMeasurement[] \| undefined>` | No | API function for loading measurement/unit-of-measure dictionaries. Only needed if you have measure-type properties. |
|
|
30
|
+
| Option | Type | Required | Description |
|
|
31
|
+
|--------|------|----------|-------------|
|
|
32
|
+
| `searchDictionary` | `(criteria: IBasePropertyDictionaryItemSearchCriteria) => Promise<IBasePropertyDictionaryItem[] \| undefined>` | Yes | API function to search dictionary items by property ID and keyword |
|
|
33
|
+
| `searchMeasurements` | `(measureId: string, locale?: string) => Promise<IBaseMeasurementDictionaryItem[] \| undefined>` | No | API function for loading measurement units. Only needed for measure-type properties |
|
|
39
34
|
|
|
40
35
|
### Returns
|
|
41
36
|
|
|
42
37
|
| Property | Type | Description |
|
|
43
38
|
|----------|------|-------------|
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
39
|
+
| `getPropertyValue` | `(property, locale) => PropertyDisplayValue` | Read display value for a property. Returns string, boolean, or array depending on type. Does NOT mutate the property. |
|
|
40
|
+
| `setPropertyValue` | `(params: SetPropertyValueParams) => void` | Write value to a property. Handles type-specific transformation and empty cleanup. |
|
|
41
|
+
| `loadDictionaries` | `(propertyId, keyword?, locale?) => Promise<...>` | Load dictionary items. If locale is provided, resolves localized values. |
|
|
42
|
+
| `loadMeasurements` | `(measureId, keyword?, locale?) => Promise<...>` | Load measurement units. No-op if `searchMeasurements` was not provided. |
|
|
43
|
+
| `loading` | `ComputedRef<boolean>` | Whether a dictionary lookup is in progress. |
|
|
49
44
|
|
|
50
45
|
### SetPropertyValueParams
|
|
51
46
|
|
|
52
47
|
| Field | Type | Description |
|
|
53
48
|
|-------|------|-------------|
|
|
54
|
-
| `property` | `
|
|
55
|
-
| `value` | `string \|
|
|
56
|
-
| `dictionary` | `
|
|
49
|
+
| `property` | `IBaseProperty` | The property object to update. Modified in place. |
|
|
50
|
+
| `value` | `string \| IBasePropertyValue[] \| (IBasePropertyDictionaryItem & { value: string })[]` | The new value. Type depends on property configuration. |
|
|
51
|
+
| `dictionary` | `IBasePropertyDictionaryItem[]?` | Dictionary items. Required when setting a dictionary value. |
|
|
57
52
|
| `locale` | `string?` | Current locale for multilanguage properties. |
|
|
58
|
-
| `initialProp` | `TProperty?` | Original property state. Used for empty-value detection -- if the original was empty and the new value is empty, a placeholder is preserved; otherwise, the value array is cleared. |
|
|
59
53
|
| `unitOfMeasureId` | `string?` | Unit of measure ID for measure-type properties. |
|
|
60
54
|
| `colorCode` | `string?` | Color hex code for color-type properties. |
|
|
61
55
|
|
|
56
|
+
### PropertyDisplayValue
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
type PropertyDisplayValue = string | IBasePropertyValue[] | boolean;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
62
|
## How It Works
|
|
63
63
|
|
|
64
|
-
###
|
|
64
|
+
### Strategy Resolution
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
The composable resolves a strategy handler based on property flags:
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
| Priority | Condition | Strategy | Handles |
|
|
69
|
+
|----------|-----------|----------|---------|
|
|
70
|
+
| 1 | `valueType === "Measure"` | measureStrategy | Numeric input with unit dropdown |
|
|
71
|
+
| 2 | `valueType === "Color"` && !dictionary | colorStrategy | Color picker, multivalue colors |
|
|
72
|
+
| 3 | `valueType === "Boolean"` | booleanStrategy | Checkbox/switch |
|
|
73
|
+
| 4 | `dictionary === true` | dictionaryStrategy | Select dropdowns with localized options |
|
|
74
|
+
| 5 | (default) | regularStrategy | ShortText, LongText, Number, Integer, DateTime |
|
|
71
75
|
|
|
72
|
-
### Value
|
|
76
|
+
### Value Getting
|
|
73
77
|
|
|
74
|
-
`
|
|
75
|
-
1. **Measure** (`valueType === "Measure"`): Creates a single value entry with `unitOfMeasureId`.
|
|
76
|
-
2. **Color** (`valueType === "Color"`, no dictionary): Creates value entry/entries with `colorCode`.
|
|
77
|
-
3. **Dictionary**: Resolves the selected dictionary item(s), expanding `localizedValues` into per-locale value entries for multilanguage dictionaries.
|
|
78
|
-
4. **Regular**: Handles the remaining cases -- simple text, number, boolean, datetime.
|
|
78
|
+
Each strategy's `get()` reads from `property.values` without mutation:
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
- **Multilanguage**: Finds value matching locale, falls back to value without languageCode
|
|
81
|
+
- **Multivalue**: Returns full array (filtered by locale for multilanguage)
|
|
82
|
+
- **Dictionary**: Returns `valueId` instead of `value`
|
|
83
|
+
- **Boolean**: Returns `false` (not `""`) when no value exists
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
### Value Setting
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
Each strategy's `set()` writes to `property.values`:
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import { ref, watch } from 'vue';
|
|
90
|
-
|
|
91
|
-
const { currentLocale } = useLanguages();
|
|
92
|
-
const { loading, loadDictionaries, getPropertyValue, setPropertyValue } =
|
|
93
|
-
useDynamicProperties(searchDictionaryItems, PropertyValue, PropertyDictionaryItem);
|
|
94
|
-
|
|
95
|
-
const properties = ref<Property[]>([]);
|
|
96
|
-
|
|
97
|
-
// Display values, keyed by property ID
|
|
98
|
-
function displayValue(property: Property) {
|
|
99
|
-
return getPropertyValue(property, currentLocale.value);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Handle value change from a form input
|
|
103
|
-
async function onValueChange(property: Property, newValue: string) {
|
|
104
|
-
if (property.dictionary) {
|
|
105
|
-
const dictItems = await loadDictionaries(property.id!, '', currentLocale.value);
|
|
106
|
-
setPropertyValue({
|
|
107
|
-
property,
|
|
108
|
-
value: newValue,
|
|
109
|
-
dictionary: dictItems ?? [],
|
|
110
|
-
locale: currentLocale.value,
|
|
111
|
-
});
|
|
112
|
-
} else {
|
|
113
|
-
setPropertyValue({
|
|
114
|
-
property,
|
|
115
|
-
value: newValue,
|
|
116
|
-
locale: currentLocale.value,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
</script>
|
|
121
|
-
```
|
|
89
|
+
- **Empty values are cleaned up automatically** via `cleanEmptyValues()`. When user clears a field, scaffolding objects are removed and `property.values` becomes `[]`. This prevents false modification detection in `useBladeForm`.
|
|
90
|
+
- **Multilanguage cleanup**: Only the value for the current locale is removed; other locales are preserved.
|
|
91
|
+
- **Dictionary**: Expands `localizedValues` into per-locale value entries.
|
|
122
92
|
|
|
123
|
-
|
|
93
|
+
### Empty Value Cleanup (cleanEmptyValues)
|
|
124
94
|
|
|
125
|
-
|
|
126
|
-
<script setup lang="ts">
|
|
127
|
-
import { useDynamicProperties } from '@vc-shell/framework';
|
|
128
|
-
import { ref } from 'vue';
|
|
95
|
+
When `setPropertyValue` receives an empty value (`""`, `null`, `undefined`), it cleans up `property.values` instead of leaving scaffolding objects:
|
|
129
96
|
|
|
130
|
-
|
|
97
|
+
```
|
|
98
|
+
Before: values: [{ value: "", languageCode: "en", propertyId: "abc", ... }]
|
|
99
|
+
After: values: []
|
|
100
|
+
```
|
|
131
101
|
|
|
132
|
-
|
|
102
|
+
This ensures that `useBladeForm`'s deep comparison correctly detects "no change" when user clears a field that was originally empty.
|
|
133
103
|
|
|
134
|
-
|
|
135
|
-
const items = await loadDictionaries(property.id!, keyword, locale);
|
|
136
|
-
dictOptions.value = items ?? [];
|
|
137
|
-
}
|
|
138
|
-
</script>
|
|
104
|
+
## Recipe: With VcDynamicProperty
|
|
139
105
|
|
|
106
|
+
```vue
|
|
140
107
|
<template>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
108
|
+
<VcDynamicProperty
|
|
109
|
+
v-for="property in properties"
|
|
110
|
+
:key="property.id"
|
|
111
|
+
:property="property"
|
|
112
|
+
:model-value="getPropertyValue(property, currentLocale)"
|
|
113
|
+
:options-getter="loadDictionaries"
|
|
114
|
+
:measurements-getter="loadMeasurements"
|
|
115
|
+
:current-language="currentLocale"
|
|
116
|
+
:value-type="property.valueType ?? ''"
|
|
117
|
+
:dictionary="property.dictionary"
|
|
118
|
+
:multivalue="property.multivalue"
|
|
119
|
+
:multilanguage="property.multilanguage"
|
|
120
|
+
@update:model-value="(ev) => setPropertyValue({ property, ...ev })"
|
|
147
121
|
/>
|
|
148
122
|
</template>
|
|
123
|
+
|
|
124
|
+
<script setup lang="ts">
|
|
125
|
+
import { useDynamicProperties } from "@vc-shell/framework";
|
|
126
|
+
|
|
127
|
+
const { getPropertyValue, setPropertyValue, loadDictionaries, loadMeasurements } =
|
|
128
|
+
useDynamicProperties({
|
|
129
|
+
searchDictionary: searchDictionaryItems,
|
|
130
|
+
searchMeasurements: searchMeasurementItems,
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
149
133
|
```
|
|
150
134
|
|
|
151
135
|
## Tips
|
|
152
136
|
|
|
153
|
-
- **`setPropertyValue` mutates the property in place.**
|
|
154
|
-
- **Always pass `dictionary` when setting dictionary values.** Without
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
|
|
137
|
+
- **`setPropertyValue` mutates the property in place.** `property.values` is modified directly. This is by design because dynamic properties are typically part of a larger entity object saved as a whole.
|
|
138
|
+
- **Always pass `dictionary` when setting dictionary values.** Without dictionary items, the composable cannot resolve `valueId` to the correct alias and localized values.
|
|
139
|
+
- **Boolean properties always have a value entry.** Unlike other types where clearing removes the value, boolean properties maintain a value entry (`value: false`). This ensures checkboxes render correctly.
|
|
140
|
+
- **No class constructors needed.** Values are created as plain objects via `createValue()`. No factory classes required.
|
|
141
|
+
|
|
142
|
+
## File Structure
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
useDynamicProperties/
|
|
146
|
+
index.ts — composable entry point (~60 lines)
|
|
147
|
+
types.ts — all interfaces
|
|
148
|
+
utils.ts — isEmptyValue, createValue, cleanEmptyValues, type guards
|
|
149
|
+
strategies/
|
|
150
|
+
index.ts — resolveStrategy() registry
|
|
151
|
+
types.ts — PropertyValueStrategy interface
|
|
152
|
+
regular.ts — ShortText, LongText, Number, Integer, DateTime
|
|
153
|
+
boolean.ts — Boolean
|
|
154
|
+
dictionary.ts — Dictionary (all multi/single × language/value)
|
|
155
|
+
measure.ts — Measure
|
|
156
|
+
color.ts — Color
|
|
157
|
+
```
|
|
158
158
|
|
|
159
159
|
## Related
|
|
160
160
|
|
|
161
|
-
- [
|
|
162
|
-
-
|
|
163
|
-
- `PropertyValue`, `PropertyDictionaryItem` from `@core/api/platform` -- concrete types for the standard platform
|
|
161
|
+
- [useBladeForm](../useBladeForm/useBladeForm.docs.md) — form state management that uses `semanticEqual` for modification detection. `cleanEmptyValues` ensures compatibility.
|
|
162
|
+
- [VcDynamicProperty](../../../../ui/components/organisms/vc-dynamic-property/vc-dynamic-property.docs.md) — UI component that renders dynamic properties
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# useSlowNetworkDetection
|
|
2
2
|
|
|
3
|
-
Detects slow network conditions and shows a persistent warning notification so users know why the UI is unresponsive. Two detection channels work together: a **proactive** channel reads `navigator.connection.effectiveType` to catch weak connections before any request is made, and a **reactive** channel flags API requests that have been pending for more than
|
|
3
|
+
Detects slow network conditions and shows a persistent warning notification so users know why the UI is unresponsive. Two detection channels work together: a **proactive** channel reads `navigator.connection.effectiveType` to catch weak connections before any request is made, and a **reactive** channel flags API requests that have been pending for more than 10 seconds. The notification auto-dismisses with a 3-second delay after conditions clear, preventing flicker. When the browser goes fully offline, the slow-network notification is suppressed in favor of the existing offline notification from `useConnectionStatus`.
|
|
4
4
|
|
|
5
5
|
Like `useConnectionStatus`, this is a module-level singleton — calling it from multiple components shares the same state and listeners.
|
|
6
6
|
|
|
@@ -41,14 +41,14 @@ const { isSlowNetwork } = useSlowNetworkDetection();
|
|
|
41
41
|
| Property | Type | Description |
|
|
42
42
|
|---|---|---|
|
|
43
43
|
| `isSlowNetwork` | `Readonly<Ref<boolean>>` | `true` when the network is slow (either channel active). Read-only. |
|
|
44
|
-
| `trackRequest` | `(id: string) => void` | Start tracking a request. If it isn't untracked within
|
|
44
|
+
| `trackRequest` | `(id: string) => void` | Start tracking a request. If it isn't untracked within 10 s, it counts as slow. |
|
|
45
45
|
| `untrackRequest` | `(id: string) => void` | Stop tracking a request. Cancels the timer or decrements the slow count. |
|
|
46
46
|
|
|
47
47
|
### Constants
|
|
48
48
|
|
|
49
49
|
| Name | Value | Purpose |
|
|
50
50
|
|---|---|---|
|
|
51
|
-
| `SLOW_REQUEST_THRESHOLD_MS` | `
|
|
51
|
+
| `SLOW_REQUEST_THRESHOLD_MS` | `10000` | Time before a pending request is considered slow |
|
|
52
52
|
| `DISMISS_DELAY_MS` | `3000` | Delay before hiding the notification after recovery |
|
|
53
53
|
| `SLOW_EFFECTIVE_TYPES` | `["slow-2g", "2g"]` | Connection types flagged as slow |
|
|
54
54
|
|
|
@@ -60,7 +60,7 @@ On first call, the composable checks `navigator.connection.effectiveType` (Netwo
|
|
|
60
60
|
|
|
61
61
|
### Channel 2: Request timers (reactive)
|
|
62
62
|
|
|
63
|
-
The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequest(id)` before every `/api/*` request and `untrackRequest(id)` in the `finally` block. Each tracked request gets a
|
|
63
|
+
The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequest(id)` before every `/api/*` request and `untrackRequest(id)` in the `finally` block. Each tracked request gets a 10-second timer. If the response arrives in time, the timer is cancelled. If not, `isSlowNetwork` becomes `true` and stays `true` until all slow requests complete.
|
|
64
64
|
|
|
65
65
|
### Notification lifecycle
|
|
66
66
|
|
|
@@ -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
|
|
@@ -28,6 +28,7 @@ An image display component with predefined sizes, aspect ratio control, and a pl
|
|
|
28
28
|
| `clickable` | `boolean` | `false` | Makes the image interactive with cursor and click event |
|
|
29
29
|
| `emptyIcon` | `string` | `"lucide-image"` | Icon shown when `src` is empty |
|
|
30
30
|
| `alt` | `string` | — | Accessible alt text |
|
|
31
|
+
| `thumbnailSize` | `ThumbnailSize` | — | Load a thumbnail variant instead of full-size image. Values: `"sm"`, `"md"`, `"lg"`, `"64x64"`, `"128x128"`, `"168x168"`, `"216x216"`, `"348x348"` |
|
|
31
32
|
|
|
32
33
|
## Size Reference
|
|
33
34
|
|
|
@@ -40,6 +40,7 @@ function deleteImage() { /* remove from list */ }
|
|
|
40
40
|
| `name` | `string` | — | File name displayed in the tray |
|
|
41
41
|
| `imageFit` | `"contain" \| "cover"` | `"contain"` | How the image fits within the tile |
|
|
42
42
|
| `actions` | `VcImageTileActions` | — | Which built-in action buttons to show |
|
|
43
|
+
| `thumbnailSize` | `ThumbnailSize` | — | Load a thumbnail variant instead of full-size image |
|
|
43
44
|
|
|
44
45
|
## VcImageTileActions Interface
|
|
45
46
|
|
|
@@ -62,7 +62,7 @@ A blade has four visual zones, rendered top-to-bottom:
|
|
|
62
62
|
|--------------------------------------|
|
|
63
63
|
| [Save] [Delete] [Refresh] [More >] | <-- Toolbar
|
|
64
64
|
|--------------------------------------|
|
|
65
|
-
| **
|
|
65
|
+
| ** Status banners (stacked) ** | <-- Status Banners
|
|
66
66
|
|--------------------------------------|
|
|
67
67
|
| |
|
|
68
68
|
| Content (default slot) | <-- Content Area
|
|
@@ -74,7 +74,7 @@ A blade has four visual zones, rendered top-to-bottom:
|
|
|
74
74
|
|
|
75
75
|
**Toolbar** -- Action buttons from the `toolbarItems` prop. Overflow items automatically collapse into a "More" dropdown (via `ResizeObserver`).
|
|
76
76
|
|
|
77
|
-
**Status Banners** --
|
|
77
|
+
**Status Banners** -- Unified, priority-sorted banner area. System banners: yellow when `modified` is `true`, red when the blade has an error (via `setError()`). Custom banners can be added programmatically via `useBlade().addBanner()` — see [useBlade docs](../../../core/composables/useBlade/useBlade.docs.md#banner-management).
|
|
78
78
|
|
|
79
79
|
**Content Area** -- The `default` slot. Scrolls independently of header and toolbar.
|
|
80
80
|
|
|
@@ -399,6 +399,31 @@ import { useBeforeUnload } from "@vc-shell/framework";
|
|
|
399
399
|
useBeforeUnload(hasChanges); // Browser tab close warning
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
## Custom Banners
|
|
403
|
+
|
|
404
|
+
Add informational, warning, or success banners to a blade programmatically. Banners appear between the header and toolbar, sorted by severity.
|
|
405
|
+
|
|
406
|
+
```vue
|
|
407
|
+
<script setup lang="ts">
|
|
408
|
+
import { useBlade } from "@vc-shell/framework";
|
|
409
|
+
|
|
410
|
+
const { addBanner, removeBanner, clearBanners } = useBlade();
|
|
411
|
+
|
|
412
|
+
// Info banner (e.g. read-only mode)
|
|
413
|
+
addBanner({ variant: "info", message: "This record is read-only" });
|
|
414
|
+
|
|
415
|
+
// Dismissible warning with action
|
|
416
|
+
addBanner({
|
|
417
|
+
variant: "warning",
|
|
418
|
+
message: "License expires in 7 days",
|
|
419
|
+
dismissible: true,
|
|
420
|
+
action: { label: "Renew", handler: () => openRenewal() },
|
|
421
|
+
});
|
|
422
|
+
</script>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Four variants are available: `danger`, `warning`, `info`, `success`. System banners (error and unsaved changes) are always present and cannot be removed by `clearBanners()`. For the full API reference, see [useBlade — Banner Management](../../../core/composables/useBlade/useBlade.docs.md#banner-management).
|
|
426
|
+
|
|
402
427
|
## Blade Width Control
|
|
403
428
|
|
|
404
429
|
```vue
|
|
@@ -13,17 +13,21 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
|
|
|
13
13
|
|
|
14
14
|
| Prop | Type | Default | Description |
|
|
15
15
|
|------|------|---------|-------------|
|
|
16
|
+
| `layout` | `"filmstrip" \| "grid"` | `"filmstrip"` | Layout mode — filmstrip shows a single scrollable row with expand/collapse; grid shows the classic multi-row auto-fill layout. |
|
|
17
|
+
| `label` | `string` | `undefined` | Label text displayed in the gallery header. |
|
|
18
|
+
| `required` | `boolean` | `false` | Shows a required indicator (`*`) on the label. |
|
|
16
19
|
| `images` | `ICommonAsset[]` | `[]` | Array of image assets to display. |
|
|
17
20
|
| `disabled` | `boolean` | `false` | Disables all interactive actions. |
|
|
18
21
|
| `multiple` | `boolean` | `false` | Allow selecting multiple files in upload dialog. |
|
|
19
|
-
| `loading` | `boolean` | `false` | Shows a loading spinner on the
|
|
22
|
+
| `loading` | `boolean` | `false` | Shows a loading overlay with spinner on the gallery. |
|
|
20
23
|
| `itemActions` | `{ preview?: boolean; edit?: boolean; remove?: boolean }` | `{ preview: true, edit: true, remove: true }` | Per-tile action visibility. |
|
|
21
24
|
| `rules` | `IValidationRules` | `undefined` | Validation rules for uploaded files. |
|
|
22
25
|
| `name` | `string` | `"Gallery"` | Field name for validation messages. |
|
|
23
26
|
| `accept` | `string` | `undefined` | Accepted file extensions. |
|
|
24
|
-
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. |
|
|
27
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. Sizes are smaller on mobile. |
|
|
25
28
|
| `gap` | `number` | `8` | Gap between tiles in pixels. |
|
|
26
29
|
| `imagefit` | `"contain" \| "cover"` | `"contain"` | How images fit within tiles. |
|
|
30
|
+
| `thumbnailSize` | `ThumbnailSize` | auto from `size` | Thumbnail size for tile images. Auto-mapped: sm→128x128, md→216x216, lg→348x348. Preview thumbnails use 64x64. |
|
|
27
31
|
|
|
28
32
|
## Events
|
|
29
33
|
|
|
@@ -44,20 +48,24 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
|
|
|
44
48
|
|
|
45
49
|
## Features
|
|
46
50
|
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
51
|
+
- **Filmstrip layout (default)** -- Single-row scrollable strip powered by Swiper. Navigate with arrows, mouse wheel, or swipe. Click "Expand (N)" to show all images in a grid. "Collapse" returns to filmstrip.
|
|
52
|
+
- **Grid layout** -- Classic auto-fill grid that wraps images into multiple rows. Set `layout="grid"` to use this mode.
|
|
53
|
+
- **Drag-and-drop reorder** -- Drag tiles by the grip handle to reorder (powered by SortableJS). Works on both desktop and touch devices. In filmstrip mode, dragging to the edge auto-scrolls the strip. Emits `sort` with the new array.
|
|
54
|
+
- **External file drop** -- Drop files from the OS onto the gallery to upload. The dashed border acts as a visual drop zone indicator (desktop only). A full overlay appears during drag-over.
|
|
55
|
+
- **Fullscreen preview** -- Click the preview button to open a fullscreen carousel. Swipe or use arrow keys to navigate. Thumbnail strip at the bottom syncs with the main image.
|
|
56
|
+
- **Per-tile actions** -- Each tile shows preview, edit, and remove buttons. On desktop: visible on hover. On mobile: visible on tap. Tile name is shown in the top bar alongside the drag handle.
|
|
57
|
+
- **Loading state** -- When `loading` is true, a pulsing border and spinner overlay appear on the gallery. Upload button is disabled. Swiper navigation is frozen.
|
|
58
|
+
- **Mobile responsive** -- Smaller tile sizes, no drag-and-drop hints, no dashed borders, compact action buttons, tap-to-reveal overlays. Uses `useResponsive` throughout.
|
|
59
|
+
- **Lazy loading** -- Images use native `loading="lazy"` for deferred loading.
|
|
52
60
|
|
|
53
61
|
## Basic Usage
|
|
54
62
|
|
|
55
63
|
```vue
|
|
56
64
|
<VcGallery
|
|
65
|
+
label="Product Images"
|
|
66
|
+
required
|
|
57
67
|
:images="product.images"
|
|
58
|
-
size="md"
|
|
59
68
|
imagefit="cover"
|
|
60
|
-
:item-actions="{ preview: true, edit: true, remove: true }"
|
|
61
69
|
@upload="handleUpload"
|
|
62
70
|
@sort="handleSort"
|
|
63
71
|
@edit="handleEdit"
|
|
@@ -65,6 +73,31 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
|
|
|
65
73
|
/>
|
|
66
74
|
```
|
|
67
75
|
|
|
76
|
+
## Filmstrip Layout (Default)
|
|
77
|
+
|
|
78
|
+
```vue
|
|
79
|
+
<VcGallery
|
|
80
|
+
label="Images"
|
|
81
|
+
:images="product.images"
|
|
82
|
+
imagefit="cover"
|
|
83
|
+
@upload="handleUpload"
|
|
84
|
+
@sort="handleSort"
|
|
85
|
+
@remove="handleRemove"
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Classic Grid Layout
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
<VcGallery
|
|
93
|
+
layout="grid"
|
|
94
|
+
label="Attachments"
|
|
95
|
+
:images="product.images"
|
|
96
|
+
@upload="handleUpload"
|
|
97
|
+
@sort="handleSort"
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
68
101
|
## Recipe: Product Image Gallery in a Blade
|
|
69
102
|
|
|
70
103
|
```vue
|
|
@@ -99,6 +132,8 @@ function handleRemove(image: ICommonAsset) {
|
|
|
99
132
|
<template>
|
|
100
133
|
<VcBlade title="Product Images">
|
|
101
134
|
<VcGallery
|
|
135
|
+
label="Product Images"
|
|
136
|
+
required
|
|
102
137
|
:images="images"
|
|
103
138
|
multiple
|
|
104
139
|
accept=".jpg,.png,.webp"
|
|
@@ -152,20 +187,24 @@ function handleRemove(image: ICommonAsset) {
|
|
|
152
187
|
|
|
153
188
|
## Tips
|
|
154
189
|
|
|
155
|
-
- The `size` prop controls tile dimensions: `"sm"` is good for compact grids (e.g., thumbnails in a sidebar), `"md"` is the standard, and `"lg"` works well for hero image management.
|
|
190
|
+
- The `size` prop controls tile dimensions: `"sm"` is good for compact grids (e.g., thumbnails in a sidebar), `"md"` is the standard, and `"lg"` works well for hero image management. Tiles are automatically smaller on mobile.
|
|
156
191
|
- Use `imagefit="cover"` for photo galleries where cropping is acceptable, and `"contain"` for logos or icons where the full image must be visible.
|
|
157
|
-
- The
|
|
192
|
+
- The filmstrip layout is ideal for blades where vertical space is limited. Users can expand to see all images or scroll horizontally. The classic grid layout is better for dedicated media management pages.
|
|
193
|
+
- Use the `label` prop to display a header label integrated with the upload button. Add `required` to show a required indicator.
|
|
158
194
|
- The `startingSortOrder` parameter in the `upload` event tells you where the new files should be inserted in the sort order. Use it to maintain correct ordering when appending new images.
|
|
195
|
+
- On desktop, reorder by dragging the grip handle icon. In filmstrip mode, dragging to the strip edge auto-scrolls to reveal more tiles.
|
|
196
|
+
- On mobile, tap a tile to reveal action buttons and the image name. Drag-and-drop hints and dashed borders are hidden automatically.
|
|
159
197
|
|
|
160
198
|
## Accessibility
|
|
161
199
|
|
|
162
200
|
- Tiles are keyboard-navigable with Tab and action buttons are focusable
|
|
163
|
-
-
|
|
164
|
-
- The upload
|
|
165
|
-
-
|
|
201
|
+
- Fullscreen preview supports keyboard navigation (arrow keys, Escape to close) and swipe gestures
|
|
202
|
+
- The upload button in the header is keyboard-accessible
|
|
203
|
+
- On mobile, tile actions are revealed via tap with click-outside to dismiss
|
|
166
204
|
|
|
167
205
|
## Related Components
|
|
168
206
|
|
|
169
207
|
- **VcImageUpload** -- single-image upload component
|
|
170
|
-
- **VcImageTile** -- the internal tile component used for each image
|
|
171
|
-
- **
|
|
208
|
+
- **VcImageTile** -- the internal tile component used for each image (topbar with name + drag handle, bottom tray with actions)
|
|
209
|
+
- **VcFileUpload** -- the file upload drop zone used in empty gallery state
|
|
210
|
+
- **VcLabel** -- used internally when `label` prop is set
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# useGlobalSearch
|
|
2
|
-
|
|
3
|
-
Provides access to the global search service state: per-blade search visibility, search queries, and methods to toggle/close search. The service maintains a map of blade IDs to search state, allowing each blade in the stack to have its own independent search input. The state is shared through Vue's provide/inject system and automatically cleaned up when the scope is disposed.
|
|
4
|
-
|
|
5
|
-
Also exports `provideGlobalSearch()` for framework-level initialization.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- In blade toolbars or headers to toggle and read the search input visibility for a specific blade
|
|
10
|
-
- To programmatically set or read the search query for a specific blade (e.g., from a URL parameter or external trigger)
|
|
11
|
-
- To integrate search with the blade toolbar's search icon button
|
|
12
|
-
- When NOT to use: for table-level filtering within a blade, use VcDataTable's built-in search header. Global search is for blade-level search that affects the entire blade's content.
|
|
13
|
-
|
|
14
|
-
## Quick Start
|
|
15
|
-
|
|
16
|
-
```vue
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
19
|
-
import { computed, watch } from 'vue';
|
|
20
|
-
|
|
21
|
-
const props = defineProps<{ bladeId: string }>();
|
|
22
|
-
const { isSearchVisible, searchQuery, toggleSearch, setSearchQuery, closeSearch } = useGlobalSearch();
|
|
23
|
-
|
|
24
|
-
// Reactive accessors scoped to this blade
|
|
25
|
-
const isVisible = computed(() => isSearchVisible.value[props.bladeId] ?? false);
|
|
26
|
-
const query = computed(() => searchQuery.value[props.bladeId] ?? '');
|
|
27
|
-
|
|
28
|
-
// React to search query changes
|
|
29
|
-
watch(query, (newQuery) => {
|
|
30
|
-
if (newQuery.length >= 2) {
|
|
31
|
-
fetchResults(newQuery);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function onSearchToggle() {
|
|
36
|
-
toggleSearch(props.bladeId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function onSearchClose() {
|
|
40
|
-
closeSearch(props.bladeId);
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<template>
|
|
45
|
-
<div>
|
|
46
|
-
<VcButton icon="fas fa-search" @click="onSearchToggle" />
|
|
47
|
-
<VcInput
|
|
48
|
-
v-if="isVisible"
|
|
49
|
-
:model-value="query"
|
|
50
|
-
placeholder="Search..."
|
|
51
|
-
@update:model-value="(val) => setSearchQuery(bladeId, val)"
|
|
52
|
-
@keydown.escape="onSearchClose"
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
55
|
-
</template>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## API
|
|
59
|
-
|
|
60
|
-
### Parameters
|
|
61
|
-
|
|
62
|
-
None.
|
|
63
|
-
|
|
64
|
-
### Returns (`GlobalSearchState`)
|
|
65
|
-
|
|
66
|
-
| Property / Method | Type | Description |
|
|
67
|
-
|-------------------|------|-------------|
|
|
68
|
-
| `isSearchVisible` | `Ref<Record<string, boolean>>` | Map of blade IDs to search visibility state. Access via `isSearchVisible.value[bladeId]`. |
|
|
69
|
-
| `searchQuery` | `Ref<Record<string, string>>` | Map of blade IDs to current search query strings. Access via `searchQuery.value[bladeId]`. |
|
|
70
|
-
| `toggleSearch` | `(bladeId: string) => void` | Toggles search visibility for a blade. If visible, hides it (and clears the query). If hidden, shows it. |
|
|
71
|
-
| `setSearchQuery` | `(bladeId: string, query: string) => void` | Sets the search query for a blade. Does not affect visibility. |
|
|
72
|
-
| `closeSearch` | `(bladeId: string) => void` | Hides the search input for a blade and clears the query. |
|
|
73
|
-
|
|
74
|
-
### Additional Exports
|
|
75
|
-
|
|
76
|
-
| Export | Description |
|
|
77
|
-
|--------|-------------|
|
|
78
|
-
| `provideGlobalSearch()` | Creates and provides the global search service. Idempotent -- returns existing service if already provided. Cleans up all state on scope disposal. |
|
|
79
|
-
|
|
80
|
-
## How It Works
|
|
81
|
-
|
|
82
|
-
The service is a simple reactive state container with two `Ref<Record<string, ...>>` maps. Each blade ID is a key in these maps. This design means:
|
|
83
|
-
|
|
84
|
-
1. **Blades are isolated**: Each blade has its own search visibility and query. Opening search in one blade does not affect others in the stack.
|
|
85
|
-
2. **Lazy initialization**: A blade's entry in the map is created on first `toggleSearch` or `setSearchQuery` call. Reading a non-existent key returns `undefined`, which the consumer treats as `false`/empty.
|
|
86
|
-
3. **Cleanup**: When `provideGlobalSearch()` scope is disposed (e.g., app unmount), both maps are cleared.
|
|
87
|
-
|
|
88
|
-
## Recipe: Toolbar Search Button with Badge Indicator
|
|
89
|
-
|
|
90
|
-
```vue
|
|
91
|
-
<script setup lang="ts">
|
|
92
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
93
|
-
import { computed } from 'vue';
|
|
94
|
-
|
|
95
|
-
const props = defineProps<{ bladeId: string }>();
|
|
96
|
-
const { isSearchVisible, searchQuery, toggleSearch } = useGlobalSearch();
|
|
97
|
-
|
|
98
|
-
const hasActiveSearch = computed(() => {
|
|
99
|
-
const query = searchQuery.value[props.bladeId];
|
|
100
|
-
return query != null && query.length > 0;
|
|
101
|
-
});
|
|
102
|
-
</script>
|
|
103
|
-
|
|
104
|
-
<template>
|
|
105
|
-
<VcButton
|
|
106
|
-
:icon="hasActiveSearch ? 'fas fa-search-plus' : 'fas fa-search'"
|
|
107
|
-
:variant="hasActiveSearch ? 'primary' : 'ghost'"
|
|
108
|
-
@click="toggleSearch(bladeId)"
|
|
109
|
-
/>
|
|
110
|
-
</template>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Recipe: Pre-Populating Search from URL Query Parameter
|
|
114
|
-
|
|
115
|
-
```vue
|
|
116
|
-
<script setup lang="ts">
|
|
117
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
118
|
-
import { useRoute } from 'vue-router';
|
|
119
|
-
import { onMounted } from 'vue';
|
|
120
|
-
|
|
121
|
-
const props = defineProps<{ bladeId: string }>();
|
|
122
|
-
const route = useRoute();
|
|
123
|
-
const { setSearchQuery, toggleSearch } = useGlobalSearch();
|
|
124
|
-
|
|
125
|
-
onMounted(() => {
|
|
126
|
-
const urlQuery = route.query.search as string | undefined;
|
|
127
|
-
if (urlQuery) {
|
|
128
|
-
setSearchQuery(props.bladeId, urlQuery);
|
|
129
|
-
toggleSearch(props.bladeId); // make the search input visible
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
</script>
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Tips
|
|
136
|
-
|
|
137
|
-
- **`toggleSearch` clears the query when hiding.** This is by design -- when the user closes the search, the query is reset. If you need to preserve the query across toggle cycles, store it separately.
|
|
138
|
-
- **Use blade ID, not component instance ID.** The blade ID comes from the blade descriptor and is stable across re-renders. Using a component's `uid` would break if the component is recreated.
|
|
139
|
-
- **State is not persisted.** Unlike sidebar state, search state is in-memory only. Refreshing the page clears all search queries. Use URL query parameters if you need persistence.
|
|
140
|
-
- **Calling outside VcApp throws.** Like all provide/inject composables in the framework, `useGlobalSearch()` throws an `InjectionError` if the service has not been provided.
|
|
141
|
-
|
|
142
|
-
## Related
|
|
143
|
-
|
|
144
|
-
- VcDataTable search header -- table-level filtering built into the data table (different from global search)
|
|
145
|
-
- [useToolbar](../useToolbar/) -- toolbar service for blade action buttons (often includes a search toggle button)
|
|
146
|
-
- `framework/core/services/global-search-service.ts` -- underlying service implementation
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|