@vc-shell/vc-app-skill 2.0.0-alpha.23 → 2.0.0-alpha.25
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 +12 -0
- package/bin/knowledge-stats.cjs +135 -0
- package/bin/sync-docs.cjs +62 -0
- package/package.json +5 -1
- package/runtime/VERSION +1 -1
- package/runtime/agents/details-blade-generator.md +75 -14
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/api/platform.docs.md +14 -7
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useApiClient/useApiClient.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useAssetsManager/useAssetsManager.docs.md +149 -0
- package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +7 -0
- package/runtime/knowledge/docs/core/composables/useBladeWidgets.docs.md +164 -9
- package/runtime/knowledge/docs/core/composables/useDashboard/useDashboard.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useMenuService/useMenuService.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/usePermissions/usePermissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +6 -0
- package/runtime/knowledge/docs/core/composables/useWidgets/useWidgets.docs.md +2 -2
- package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/extension-points/extension-points.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/global-error-handler/global-error-handler.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/i18n/i18n.docs.md +74 -0
- package/runtime/knowledge/docs/core/plugins/modularity/modularity.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/permissions/permissions.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/signalR/signalR.docs.md +6 -0
- package/runtime/knowledge/docs/core/plugins/validation/validation.docs.md +6 -0
- package/runtime/knowledge/docs/injection-keys.docs.md +1 -1
- package/runtime/knowledge/docs/modules/assets-manager/assets-manager.docs.md +29 -33
- package/runtime/knowledge/docs/shell/components/logout-button/logout-button.docs.md +3 -3
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +11 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-table/vc-data-table.docs.md +12 -0
- package/runtime/knowledge/index.md +60 -0
- package/runtime/knowledge/patterns/assets-management.md +213 -0
- package/runtime/knowledge/patterns/child-blade-flow.md +277 -0
- package/runtime/knowledge/patterns/details-blade-pattern.md +350 -3
- package/runtime/knowledge/patterns/extension-points-usage.md +308 -0
- package/runtime/knowledge/patterns/form-validation.md +377 -0
- package/runtime/knowledge/patterns/multilanguage-fields.md +239 -0
- package/runtime/knowledge/patterns/signalr-notifications.md +237 -0
- package/runtime/vc-app.md +44 -5
- package/runtime/knowledge/docs/core/composables/useWidget/useWidget.docs.md +0 -159
- package/runtime/knowledge/docs/shell/components/app-switcher/app-switcher.docs.md +0 -104
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Form Validation Pattern
|
|
2
|
+
|
|
3
|
+
Reference source: `apps/vendor-portal/src/modules/products/components/ProductDetailsBase.vue`
|
|
4
|
+
Secondary source: `apps/vendor-portal/src/modules/offers/pages/offers-details.vue`
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Form validation in vc-shell uses vee-validate v4 with a declarative `<Field>` wrapper around each input. The framework auto-registers all standard rules plus custom vc-shell rules at startup -- no per-component imports needed. `useForm()` provides reactive form-level validity metadata for controlling toolbar buttons and save guards.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Core Wiring Pattern: Field + VcInput
|
|
13
|
+
|
|
14
|
+
Every validated field follows the same three-part contract:
|
|
15
|
+
|
|
16
|
+
1. `<Field>` tracks the value and applies rules
|
|
17
|
+
2. The inner component uses `v-model` for two-way binding
|
|
18
|
+
3. `handleChange` keeps vee-validate in sync on every update
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<Field
|
|
22
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
23
|
+
:model-value="item.name"
|
|
24
|
+
name="name"
|
|
25
|
+
rules="required"
|
|
26
|
+
>
|
|
27
|
+
<VcInput
|
|
28
|
+
v-model="item.name"
|
|
29
|
+
label="Name"
|
|
30
|
+
required
|
|
31
|
+
:error="!!errors.length"
|
|
32
|
+
:error-message="errorMessage"
|
|
33
|
+
@update:model-value="handleChange"
|
|
34
|
+
/>
|
|
35
|
+
</Field>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Critical:** Without `@update:model-value="handleChange"`, vee-validate never learns the value changed and validation will not trigger. Without `:model-value` on `<Field>`, the rule has nothing to validate against.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Wiring Other Input Components
|
|
43
|
+
|
|
44
|
+
The same pattern applies to every vc-shell input. Only the inner component changes.
|
|
45
|
+
|
|
46
|
+
### VcSelect
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<Field
|
|
50
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
51
|
+
:model-value="item.categoryId"
|
|
52
|
+
name="categoryId"
|
|
53
|
+
rules="required"
|
|
54
|
+
>
|
|
55
|
+
<VcSelect
|
|
56
|
+
v-model="item.categoryId"
|
|
57
|
+
label="Category"
|
|
58
|
+
:options="categories"
|
|
59
|
+
option-value="id"
|
|
60
|
+
option-label="name"
|
|
61
|
+
required
|
|
62
|
+
:clearable="false"
|
|
63
|
+
:error="!!errors.length"
|
|
64
|
+
:error-message="errorMessage"
|
|
65
|
+
@update:model-value="handleChange"
|
|
66
|
+
/>
|
|
67
|
+
</Field>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### VcEditor (Rich Text)
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<Field
|
|
74
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
75
|
+
:model-value="item.description"
|
|
76
|
+
name="description"
|
|
77
|
+
rules="required"
|
|
78
|
+
>
|
|
79
|
+
<VcEditor
|
|
80
|
+
v-model="item.description"
|
|
81
|
+
label="Description"
|
|
82
|
+
required
|
|
83
|
+
:error="!!errors.length"
|
|
84
|
+
:error-message="errorMessage"
|
|
85
|
+
@update:model-value="handleChange"
|
|
86
|
+
/>
|
|
87
|
+
</Field>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### VcTextarea
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<Field
|
|
94
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
95
|
+
:model-value="item.notes"
|
|
96
|
+
name="notes"
|
|
97
|
+
rules="required|min:10"
|
|
98
|
+
>
|
|
99
|
+
<VcTextarea
|
|
100
|
+
v-model="item.notes"
|
|
101
|
+
label="Notes"
|
|
102
|
+
required
|
|
103
|
+
:error="!!errors.length"
|
|
104
|
+
:error-message="errorMessage"
|
|
105
|
+
@update:model-value="handleChange"
|
|
106
|
+
/>
|
|
107
|
+
</Field>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Fields That Do Not Need Validation
|
|
111
|
+
|
|
112
|
+
Simple toggles and checkboxes typically skip the `<Field>` wrapper:
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<VcSwitch v-model="item.isActive" label="Active" />
|
|
116
|
+
<VcCheckbox v-model="item.acceptTerms" label="I accept the terms" />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Available Validation Rules
|
|
122
|
+
|
|
123
|
+
Rules are composed with `|` (pipe) in the `rules` string.
|
|
124
|
+
|
|
125
|
+
### Standard Rules (from @vee-validate/rules)
|
|
126
|
+
|
|
127
|
+
| Rule | Example | Description |
|
|
128
|
+
|------|---------|-------------|
|
|
129
|
+
| `required` | `"required"` | Must have a value |
|
|
130
|
+
| `email` | `"required\|email"` | Valid email format |
|
|
131
|
+
| `min:N` | `"required\|min:3"` | Minimum string length |
|
|
132
|
+
| `max:N` | `"max:255"` | Maximum string length |
|
|
133
|
+
| `min_value:N` | `"min_value:0"` | Minimum numeric value |
|
|
134
|
+
| `between:N,M` | `"between:1,100"` | Numeric range |
|
|
135
|
+
| `numeric` | `"numeric"` | Digits only |
|
|
136
|
+
| `alpha_dash` | `"alpha_dash"` | Letters, digits, dashes, underscores |
|
|
137
|
+
| `regex:P` | `"regex:^[A-Z]+"` | Custom regex match |
|
|
138
|
+
| `confirmed:F` | `"confirmed:password"` | Must match another field |
|
|
139
|
+
| `url` | `"url"` | Valid URL format |
|
|
140
|
+
|
|
141
|
+
### Custom vc-shell Rules
|
|
142
|
+
|
|
143
|
+
| Rule | Params | Description |
|
|
144
|
+
|------|--------|-------------|
|
|
145
|
+
| `bigint` | -- | Value is a safe integer (`Number.isSafeInteger`) |
|
|
146
|
+
| `mindimensions` | `[width, height]` | Image meets minimum pixel dimensions |
|
|
147
|
+
| `fileWeight` | `[sizeInKB]` | File size under limit (KB) |
|
|
148
|
+
| `before` | `[targetDate]` | Date is before target |
|
|
149
|
+
| `after` | `[targetDate]` | Date is after target |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Form-Level Validation with useForm
|
|
154
|
+
|
|
155
|
+
`useForm()` provides reactive metadata about all `<Field>` components in the current component tree.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { useForm, Field } from "vee-validate";
|
|
159
|
+
|
|
160
|
+
const { meta, setFieldError } = useForm({
|
|
161
|
+
validateOnMount: false, // Always false -- avoids errors on blade open
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// meta.value.valid -- true when ALL fields pass
|
|
165
|
+
// meta.value.dirty -- true when ANY field has changed
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Disabling the Save Button
|
|
169
|
+
|
|
170
|
+
Wire `meta.value.valid` into the toolbar's `disabled` computed:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
174
|
+
{
|
|
175
|
+
id: "save",
|
|
176
|
+
title: "Save",
|
|
177
|
+
icon: "lucide-save",
|
|
178
|
+
disabled: computed(() => !meta.value.valid || !modified.value),
|
|
179
|
+
async clickHandler() {
|
|
180
|
+
if (meta.value.valid) {
|
|
181
|
+
await saveItem();
|
|
182
|
+
callParent("reload");
|
|
183
|
+
closeSelf();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Tip:** Use `modified.value` from your details composable instead of `meta.value.dirty`. The `dirty` flag only tracks vee-validate Field changes and misses switches, checkboxes, and other non-validated inputs.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Dynamic and Conditional Rules
|
|
195
|
+
|
|
196
|
+
When rules depend on reactive state, use a computed or `:rules` binding:
|
|
197
|
+
|
|
198
|
+
```vue
|
|
199
|
+
<!-- Conditionally required -->
|
|
200
|
+
<Field
|
|
201
|
+
:rules="item.trackInventory ? 'required|bigint|min_value:0' : 'bigint|min_value:0'"
|
|
202
|
+
:model-value="item.quantity"
|
|
203
|
+
name="quantity"
|
|
204
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
205
|
+
>
|
|
206
|
+
<VcInput
|
|
207
|
+
v-model="item.quantity"
|
|
208
|
+
type="number"
|
|
209
|
+
label="Quantity"
|
|
210
|
+
:required="item.trackInventory"
|
|
211
|
+
:error="!!errors.length"
|
|
212
|
+
:error-message="errorMessage"
|
|
213
|
+
@update:model-value="handleChange"
|
|
214
|
+
/>
|
|
215
|
+
</Field>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Cross-Field Validation
|
|
221
|
+
|
|
222
|
+
Use the `after` / `before` custom rules with reactive parameters, or the `confirmed` standard rule.
|
|
223
|
+
|
|
224
|
+
### Date Range (start must be before end)
|
|
225
|
+
|
|
226
|
+
```vue
|
|
227
|
+
<script setup lang="ts">
|
|
228
|
+
import { ref, computed } from "vue";
|
|
229
|
+
import { Field, useForm } from "vee-validate";
|
|
230
|
+
|
|
231
|
+
const { meta } = useForm({ validateOnMount: false });
|
|
232
|
+
|
|
233
|
+
const startDate = ref("");
|
|
234
|
+
const endDate = ref("");
|
|
235
|
+
const endDateRules = computed(() => `required|after:${startDate.value}`);
|
|
236
|
+
</script>
|
|
237
|
+
|
|
238
|
+
<template>
|
|
239
|
+
<VcForm>
|
|
240
|
+
<Field
|
|
241
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
242
|
+
:model-value="startDate"
|
|
243
|
+
name="startDate"
|
|
244
|
+
rules="required"
|
|
245
|
+
>
|
|
246
|
+
<VcInput
|
|
247
|
+
v-model="startDate"
|
|
248
|
+
type="date"
|
|
249
|
+
label="Start Date"
|
|
250
|
+
required
|
|
251
|
+
:error="!!errors.length"
|
|
252
|
+
:error-message="errorMessage"
|
|
253
|
+
@update:model-value="handleChange"
|
|
254
|
+
/>
|
|
255
|
+
</Field>
|
|
256
|
+
|
|
257
|
+
<Field
|
|
258
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
259
|
+
:model-value="endDate"
|
|
260
|
+
name="endDate"
|
|
261
|
+
:rules="endDateRules"
|
|
262
|
+
>
|
|
263
|
+
<VcInput
|
|
264
|
+
v-model="endDate"
|
|
265
|
+
type="date"
|
|
266
|
+
label="End Date"
|
|
267
|
+
required
|
|
268
|
+
:error="!!errors.length"
|
|
269
|
+
:error-message="errorMessage"
|
|
270
|
+
@update:model-value="handleChange"
|
|
271
|
+
/>
|
|
272
|
+
</Field>
|
|
273
|
+
</VcForm>
|
|
274
|
+
</template>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Password Confirmation
|
|
278
|
+
|
|
279
|
+
```vue
|
|
280
|
+
<Field
|
|
281
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
282
|
+
:model-value="password"
|
|
283
|
+
name="password"
|
|
284
|
+
rules="required|min:8"
|
|
285
|
+
>
|
|
286
|
+
<VcInput
|
|
287
|
+
v-model="password"
|
|
288
|
+
type="password"
|
|
289
|
+
label="Password"
|
|
290
|
+
required
|
|
291
|
+
:error="!!errors.length"
|
|
292
|
+
:error-message="errorMessage"
|
|
293
|
+
@update:model-value="handleChange"
|
|
294
|
+
/>
|
|
295
|
+
</Field>
|
|
296
|
+
|
|
297
|
+
<Field
|
|
298
|
+
v-slot="{ errorMessage, handleChange, errors }"
|
|
299
|
+
:model-value="confirmPassword"
|
|
300
|
+
name="confirmPassword"
|
|
301
|
+
rules="required|confirmed:password"
|
|
302
|
+
>
|
|
303
|
+
<VcInput
|
|
304
|
+
v-model="confirmPassword"
|
|
305
|
+
type="password"
|
|
306
|
+
label="Confirm Password"
|
|
307
|
+
required
|
|
308
|
+
:error="!!errors.length"
|
|
309
|
+
:error-message="errorMessage"
|
|
310
|
+
@update:model-value="handleChange"
|
|
311
|
+
/>
|
|
312
|
+
</Field>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Server-Side Validation Errors
|
|
318
|
+
|
|
319
|
+
Use `setFieldError()` to display API-returned errors on specific fields:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const { setFieldError } = useForm({ validateOnMount: false });
|
|
323
|
+
|
|
324
|
+
async function save() {
|
|
325
|
+
try {
|
|
326
|
+
await api.updateItem(item.value);
|
|
327
|
+
} catch (e: unknown) {
|
|
328
|
+
if (e instanceof ApiValidationError) {
|
|
329
|
+
for (const [field, messages] of Object.entries(e.errors)) {
|
|
330
|
+
setFieldError(field, messages.join("\n"));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Validation Schema (Alternative to Inline Rules)
|
|
340
|
+
|
|
341
|
+
For forms with many fields, define all rules in one object:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const { handleSubmit } = useForm({
|
|
345
|
+
validateOnMount: false,
|
|
346
|
+
validationSchema: {
|
|
347
|
+
name: "required|min:3",
|
|
348
|
+
email: "required|email",
|
|
349
|
+
sku: "required|alpha_dash|min:3|max:64",
|
|
350
|
+
price: "required|numeric|bigint",
|
|
351
|
+
startDate: "required",
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
When using a schema, `<Field>` components still need `name` but can omit `rules` -- the schema supplies them.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Common Mistakes
|
|
361
|
+
|
|
362
|
+
1. **Missing `handleChange`** -- vee-validate stays stale; validation never fires on input.
|
|
363
|
+
2. **Missing `:model-value` on Field** -- rule has no value to validate; always shows as valid.
|
|
364
|
+
3. **`validateOnMount: true`** -- shows errors on every field the moment the blade opens. Always use `false`.
|
|
365
|
+
4. **Using `meta.value.dirty` for save button** -- misses non-validated fields (switches, checkboxes). Use a dedicated `modified` ref from your composable.
|
|
366
|
+
5. **Static rule string for cross-field** -- `rules="after:${startDate.value}"` captures the value once at template compile. Use `:rules="endDateRules"` with a computed.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Key Rules
|
|
371
|
+
|
|
372
|
+
1. Always set `validateOnMount: false` in `useForm()`.
|
|
373
|
+
2. Every validated field must wire all three: `:model-value`, `v-model`, and `@update:model-value="handleChange"`.
|
|
374
|
+
3. Use pipe-delimited rule strings (`"required|email|min:3"`) for inline rules.
|
|
375
|
+
4. Use `:rules` binding with computed for dynamic/cross-field rules.
|
|
376
|
+
5. Disable save via `computed(() => !meta.value.valid || !modified.value)` on the toolbar item.
|
|
377
|
+
6. Fields without validation rules (switches, checkboxes) do not need a `<Field>` wrapper.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Multilanguage Fields Pattern
|
|
2
|
+
|
|
3
|
+
Add multilanguage content editing to a detail blade. This is for switching which language version of entity data the user edits — distinct from the UI locale (app translation language).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Entity has multilingual fields stored as `Record<string, string>` (keyed by locale code like `"en-US"`, `"de-DE"`)
|
|
10
|
+
- Module registers its own locale files via `defineAppModule({ locales })`
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Step 1 — Load Available Languages
|
|
15
|
+
|
|
16
|
+
Create or reuse a composable that fetches content languages from the API and exposes a reactive `currentLocale`, options list, and flag URLs.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { ref, computed, watchEffect, onBeforeUnmount, Ref } from "vue";
|
|
20
|
+
import { useLanguages } from "@vc-shell/framework";
|
|
21
|
+
|
|
22
|
+
const currentLocale = ref("en-US");
|
|
23
|
+
const languages = ref<string[]>([]);
|
|
24
|
+
const localesOptions = ref<{ value: string; label: string }[]>([]);
|
|
25
|
+
|
|
26
|
+
export function useMultilanguage() {
|
|
27
|
+
const { getLocaleByTag, getFlag } = useLanguages();
|
|
28
|
+
|
|
29
|
+
const languageOptionsWithFlags = ref<{ value: string; label: string; flag?: string }[]>([]);
|
|
30
|
+
const isMultilanguage = computed(() => localesOptions.value.length > 1);
|
|
31
|
+
|
|
32
|
+
async function getLanguages() {
|
|
33
|
+
// Fetch from your API client
|
|
34
|
+
languages.value = await fetchAvailableLanguages();
|
|
35
|
+
localesOptions.value = languages.value.map((x) => ({
|
|
36
|
+
label: getLocaleByTag(x) || x,
|
|
37
|
+
value: x,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolve flags in parallel
|
|
42
|
+
watchEffect(async () => {
|
|
43
|
+
if (!isMultilanguage.value) return;
|
|
44
|
+
languageOptionsWithFlags.value = await Promise.all(
|
|
45
|
+
localesOptions.value.map(async (lang) => ({
|
|
46
|
+
...lang,
|
|
47
|
+
flag: await getFlag(lang.value),
|
|
48
|
+
})),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const setLocale = (locale: string) => {
|
|
53
|
+
currentLocale.value = locale;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Reset on unmount so next blade starts fresh
|
|
57
|
+
onBeforeUnmount(() => {
|
|
58
|
+
currentLocale.value = "en-US";
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { currentLocale, localesOptions, languageOptionsWithFlags, isMultilanguage, setLocale, getLanguages };
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Key points:
|
|
66
|
+
- `currentLocale` and `languages` are module-level refs (shared across blades in the same module).
|
|
67
|
+
- Deduplicate concurrent `getLanguages` calls with a promise guard if multiple blades mount simultaneously.
|
|
68
|
+
- `useLanguages()` from the framework provides `getLocaleByTag` (display name) and `getFlag` (async flag URL).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 2 — Place MultilanguageSelector in the Blade
|
|
73
|
+
|
|
74
|
+
Use the blade's `#actions` slot to position the language picker in the toolbar area.
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<template>
|
|
78
|
+
<VcBlade :title="bladeTitle" :toolbar-items="bladeToolbar">
|
|
79
|
+
<template #actions>
|
|
80
|
+
<MultilanguageSelector
|
|
81
|
+
v-if="isMultilanguage"
|
|
82
|
+
v-model="currentLocale"
|
|
83
|
+
:options="languageOptionsWithFlags"
|
|
84
|
+
/>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<!-- form fields below -->
|
|
88
|
+
</VcBlade>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Import `MultilanguageSelector` from `@vc-shell/framework`. It renders as a compact circular flag button that opens a dropdown. Only show it when `isMultilanguage` is true (more than one language available).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Step 3 — Create Localized Computed Properties
|
|
97
|
+
|
|
98
|
+
For each multilingual field on your entity, create a writable computed that reads/writes the value for `currentLocale`.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const { currentLocale } = useMultilanguage();
|
|
102
|
+
|
|
103
|
+
// Assuming item.value is the entity with multilingual fields
|
|
104
|
+
const localizedName = computed({
|
|
105
|
+
get: () => item.value?.names?.[currentLocale.value] ?? "",
|
|
106
|
+
set: (val: string) => {
|
|
107
|
+
if (item.value) {
|
|
108
|
+
if (!item.value.names) item.value.names = {};
|
|
109
|
+
item.value.names[currentLocale.value] = val;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const localizedDescription = computed({
|
|
115
|
+
get: () => item.value?.descriptions?.[currentLocale.value] ?? "",
|
|
116
|
+
set: (val: string) => {
|
|
117
|
+
if (item.value) {
|
|
118
|
+
if (!item.value.descriptions) item.value.descriptions = {};
|
|
119
|
+
item.value.descriptions[currentLocale.value] = val;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Guard against undefined with `??` — when a language has no content yet, the field shows empty.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Step 4 — Wire Form Fields with Multilanguage Props
|
|
130
|
+
|
|
131
|
+
`VcInput` and `VcEditor` both accept `:multilanguage` and `:current-language` props. These add a visual language indicator on the field label.
|
|
132
|
+
|
|
133
|
+
```vue
|
|
134
|
+
<VcInput
|
|
135
|
+
v-model="localizedName"
|
|
136
|
+
:label="t('MY_MODULE.FIELDS.NAME')"
|
|
137
|
+
multilanguage
|
|
138
|
+
:current-language="currentLocale"
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<VcEditor
|
|
142
|
+
v-model="localizedDescription"
|
|
143
|
+
:label="t('MY_MODULE.FIELDS.DESCRIPTION')"
|
|
144
|
+
multilanguage
|
|
145
|
+
:current-language="currentLocale"
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `multilanguage` (boolean) — enables the language indicator badge on the label.
|
|
150
|
+
- `current-language` (string) — the locale code shown in the indicator (e.g., `"de-DE"`).
|
|
151
|
+
- The actual value switching is handled by your localized computed, not by the component itself.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Step 5 — Module Locale Files
|
|
156
|
+
|
|
157
|
+
Register translation files in your module definition so the framework merges them into the global i18n instance.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
modules/my-module/
|
|
161
|
+
locales/
|
|
162
|
+
en.json
|
|
163
|
+
de.json
|
|
164
|
+
index.ts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// modules/my-module/index.ts
|
|
169
|
+
import * as en from "./locales/en.json";
|
|
170
|
+
import * as de from "./locales/de.json";
|
|
171
|
+
|
|
172
|
+
export default defineAppModule({
|
|
173
|
+
locales: { en, de },
|
|
174
|
+
// ...other module config
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Locale files use nested JSON with a module namespace:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"MY_MODULE": {
|
|
183
|
+
"FIELDS": {
|
|
184
|
+
"NAME": "Name",
|
|
185
|
+
"DESCRIPTION": "Description"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The framework calls `i18n.global.mergeLocaleMessage()` during module installation — no manual merge needed.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Complete Blade Example
|
|
196
|
+
|
|
197
|
+
```vue
|
|
198
|
+
<script setup lang="ts">
|
|
199
|
+
import { computed } from "vue";
|
|
200
|
+
import { useI18n } from "vue-i18n";
|
|
201
|
+
import { MultilanguageSelector } from "@vc-shell/framework";
|
|
202
|
+
import { useMultilanguage } from "../composables/useMultilanguage";
|
|
203
|
+
|
|
204
|
+
const { t } = useI18n();
|
|
205
|
+
const { currentLocale, languageOptionsWithFlags, isMultilanguage, getLanguages } = useMultilanguage();
|
|
206
|
+
|
|
207
|
+
const props = defineProps<{ param?: { item: Record<string, any> } }>();
|
|
208
|
+
const item = computed(() => props.param?.item);
|
|
209
|
+
|
|
210
|
+
// Call once on blade open
|
|
211
|
+
getLanguages();
|
|
212
|
+
|
|
213
|
+
const localizedName = computed({
|
|
214
|
+
get: () => item.value?.names?.[currentLocale.value] ?? "",
|
|
215
|
+
set: (val: string) => {
|
|
216
|
+
if (item.value) item.value.names[currentLocale.value] = val;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<template>
|
|
222
|
+
<VcBlade :title="t('MY_MODULE.BLADE_TITLE')">
|
|
223
|
+
<template #actions>
|
|
224
|
+
<MultilanguageSelector
|
|
225
|
+
v-if="isMultilanguage"
|
|
226
|
+
v-model="currentLocale"
|
|
227
|
+
:options="languageOptionsWithFlags"
|
|
228
|
+
/>
|
|
229
|
+
</template>
|
|
230
|
+
|
|
231
|
+
<VcInput
|
|
232
|
+
v-model="localizedName"
|
|
233
|
+
:label="t('MY_MODULE.FIELDS.NAME')"
|
|
234
|
+
multilanguage
|
|
235
|
+
:current-language="currentLocale"
|
|
236
|
+
/>
|
|
237
|
+
</VcBlade>
|
|
238
|
+
</template>
|
|
239
|
+
```
|