@xen-orchestra/web-core 0.52.0 → 0.53.0

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.
Files changed (32) hide show
  1. package/lib/components/console/VtsRemoteConsole.vue +7 -3
  2. package/lib/components/form/VtsForm.vue +15 -0
  3. package/lib/components/status/VtsStatus.vue +1 -7
  4. package/lib/components/table/cells/VtsTagCell.vue +2 -4
  5. package/lib/components/tag/VtsTag.vue +30 -0
  6. package/lib/components/ui/chip/UiChip.vue +52 -29
  7. package/lib/components/ui/info/UiInfo.vue +17 -6
  8. package/lib/components/ui/link/UiLink.vue +6 -2
  9. package/lib/components/ui/tag/UiTag.vue +10 -17
  10. package/lib/components/ui/tag/UiTertiaryTag.vue +39 -0
  11. package/lib/icons/action-icons.ts +1 -1
  12. package/lib/locales/en.json +4 -0
  13. package/lib/locales/fr.json +4 -0
  14. package/lib/packages/form-validation/README.md +273 -0
  15. package/lib/packages/form-validation/custom-rules/out-of-range.rule.ts +15 -0
  16. package/lib/packages/form-validation/index.ts +8 -0
  17. package/lib/packages/form-validation/merge-validation-configs.ts +46 -0
  18. package/lib/packages/form-validation/types.ts +104 -0
  19. package/lib/packages/form-validation/use-form-validation.ts +196 -0
  20. package/lib/packages/modal/types.ts +1 -2
  21. package/lib/packages/remote-resource/define-remote-resource.ts +2 -1
  22. package/lib/packages/remote-resource/types.ts +1 -3
  23. package/lib/packages/request/define-request.ts +1 -2
  24. package/lib/packages/validated-form/README.md +389 -0
  25. package/lib/packages/validated-form/index.ts +2 -0
  26. package/lib/packages/validated-form/use-multi-step-validated-form.ts +203 -0
  27. package/lib/packages/validated-form/use-validated-form.ts +180 -0
  28. package/lib/utils/parse-tag.util.ts +22 -0
  29. package/package.json +11 -8
  30. package/tsconfig.json +2 -3
  31. package/lib/components/ui/chip/ChipIcon.vue +0 -21
  32. package/lib/components/ui/chip/ChipRemoveIcon.vue +0 -16
@@ -1,8 +1,7 @@
1
1
  import { useTimeoutPoll } from '@vueuse/core'
2
- import type { MaybeRefOrGetter } from '@vueuse/shared'
3
2
  // eslint-disable-next-line import/namespace,import/default,import/no-named-as-default,import/no-named-as-default-member -- https://github.com/pamelafox/ndjson-readablestream/pull/13
4
3
  import readNDJSONStream from 'ndjson-readablestream'
5
- import { computed, isRef, toValue, watch } from 'vue'
4
+ import { computed, isRef, toValue, watch, type MaybeRefOrGetter } from 'vue'
6
5
 
7
6
  export function defineRequest<TState>(options: {
8
7
  url: string
@@ -0,0 +1,389 @@
1
+ # `validated-form` package
2
+
3
+ Provides two composables for building validated forms: `useValidatedForm` for single-step forms and `useMultiStepValidatedForm` for wizard-style multi-step forms.
4
+
5
+ > **Note:** Always use `VtsForm` instead of a raw `<form>` element when using these composables. `VtsForm` sets `novalidate` unconditionally, preventing browser-native validation from interfering with the custom validation logic.
6
+ >
7
+ > ```vue
8
+ > <VtsForm @submit="onSubmit">…</VtsForm>
9
+ > ```
10
+ >
11
+ > If you must use a raw `<form>`, add `novalidate` manually:
12
+ >
13
+ > ```html
14
+ > <form novalidate @submit.prevent="onSubmit">…</form>
15
+ > ```
16
+
17
+ ---
18
+
19
+ ## `useValidatedForm` composable
20
+
21
+ Combines `useFormBindings` and `useFormValidation` into a single composable so each field key appears exactly once — no more passing the same key to both a binding and a metadata helper.
22
+
23
+ ### Usage
24
+
25
+ ```typescript
26
+ import { required, outOfRange } from '@core/packages/form-validation'
27
+ import { useValidatedForm } from '@core/packages/validated-form'
28
+
29
+ const { useField, useFormSelect, useSelect, validate, reset, handleBlur } = useValidatedForm(formData, {
30
+ errors: {
31
+ onSubmit: () => ({
32
+ title: { required },
33
+ }),
34
+ },
35
+ warnings: {
36
+ onBlur: () => ({
37
+ quantity: { outOfRange: outOfRange(1, 999) },
38
+ }),
39
+ },
40
+ })
41
+ ```
42
+
43
+ ### Parameters
44
+
45
+ | | Required | Type | Description |
46
+ | -------- | :------: | ----------------------------- | ------------------------------------------------------------------------- |
47
+ | `data` | ✓ | `TData` | A reactive object holding the form field values |
48
+ | `config` | ✓ | `FormValidationConfig<TData>` | Validation rule configuration — same shape as `useFormValidation` accepts |
49
+
50
+ `FormValidationConfig` is documented in detail in `@core/packages/form-validation`.
51
+
52
+ ### Return value
53
+
54
+ #### Binding factories
55
+
56
+ | Property | Signature | Description |
57
+ | --------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
58
+ | `useField` | `(key, extras?) => ComputedRef<ModelBinding & FieldMetadata & E>` | Creates a combined v-model + validation binding for a text/number/checkbox input |
59
+ | `useFormSelect` | `(key, sources, config?) => UseFormSelectReturn` | Registers a select, auto-wires `model`, and records the key→id mapping in the registry |
60
+ | `useSelect` | see overloads below | Creates a binding for a `VtsSelect`-like component |
61
+
62
+ ##### `useField(key, extras?)`
63
+
64
+ Returns a `ComputedRef` merging:
65
+
66
+ - `modelValue` / `onUpdate:modelValue` — the v-model pair for the field
67
+ - `error`, `warning`, `onBlur` — from `FieldMetadata`
68
+ - Any additional props returned by the optional `extras` factory (e.g. `label`, `required`, `info`)
69
+
70
+ ##### `useFormSelect(key, sources, config?)`
71
+
72
+ A wrapper around `useFormSelect` from `@core/packages/form-select` that:
73
+
74
+ 1. Automatically sets `model: toRef(data, key)` — no need to pass it manually
75
+ 2. Records the `id → key` mapping so `useSelect(id)` can resolve validation metadata without an explicit key
76
+
77
+ `config` accepts all `useFormSelect` options **except** `model` (which is injected).
78
+
79
+ ##### `useSelect` overloads
80
+
81
+ | Overload | When to use |
82
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
83
+ | `useSelect(id, extras?)` | Select was registered with `useFormSelect` in the same `useValidatedForm` call — key is resolved from the registry |
84
+ | `useSelect(id, key, extras?)` | Select was created externally (e.g. inside a nested composable) — key cannot be inferred and must be passed explicitly |
85
+
86
+ Both overloads return a `ComputedRef` merging `{ id }`, `FieldMetadata`, and any extras.
87
+
88
+ #### Delegated from `useFormValidation`
89
+
90
+ | Property | Type | Description |
91
+ | ------------ | ------------------------------ | -------------------------------------------------------------------------------- |
92
+ | `validate` | `() => Promise<boolean>` | Validates all error rules and touches warning fields. Returns `true` when valid |
93
+ | `reset` | `() => void` | Resets all dirty flags and clears all messages |
94
+ | `handleBlur` | `(field: keyof TData) => void` | Marks a field dirty in the `onBlur` rule groups (called automatically by fields) |
95
+
96
+ ### Example: form with text inputs and a managed select
97
+
98
+ ```typescript
99
+ import { useValidatedForm } from '@core/packages/validated-form'
100
+ import { reactive } from 'vue'
101
+ import { useI18n } from 'vue-i18n'
102
+
103
+ type ProductFormData = {
104
+ category: string | undefined
105
+ title: string
106
+ }
107
+
108
+ export function useProductForm() {
109
+ const { t } = useI18n()
110
+
111
+ const formData = reactive<ProductFormData>({ category: undefined, title: '' })
112
+
113
+ const { useField, useFormSelect, useSelect, validate } = useValidatedForm(formData, {
114
+ errors: {
115
+ onSubmit: () => ({
116
+ category: { required },
117
+ title: { required },
118
+ }),
119
+ },
120
+ })
121
+
122
+ // model is wired automatically; id→key mapping is recorded in the registry
123
+ const { id: categorySelectId } = useFormSelect('category', categories, {
124
+ searchable: true,
125
+ required: true,
126
+ option: { label: 'name', value: 'id' },
127
+ })
128
+
129
+ // key is inferred from the registry — no need to repeat 'category'
130
+ const categorySelectBindings = useSelect(categorySelectId, () => ({ label: t('category') }))
131
+ const titleInputBindings = useField('title', () => ({ label: t('title'), required: true }))
132
+
133
+ return { categorySelectBindings, titleInputBindings, validate }
134
+ }
135
+ ```
136
+
137
+ ### Example: select created by an external composable (explicit key)
138
+
139
+ When `useFormSelect` is called inside a nested composable (outside the current `useValidatedForm` scope), the id→key mapping cannot be inferred automatically. Pass the key as the second argument to `useSelect`:
140
+
141
+ ```typescript
142
+ const { useField, useSelect, validate } = useValidatedForm(formData, {
143
+ errors: {
144
+ onSubmit: () => ({
145
+ author: { required },
146
+ quantity: { required },
147
+ }),
148
+ },
149
+ })
150
+
151
+ // authorSelectId comes from a composable that called useFormSelect internally
152
+ const { authorSelectId } = useAuthorSelect(toRef(formData, 'author'))
153
+
154
+ // key 'author' must be provided explicitly because it was registered outside this scope
155
+ const authorSelectBindings = useSelect(authorSelectId, 'author', () => ({ label: t('author') }))
156
+ const quantityInputBindings = useField('quantity', () => ({ label: t('quantity'), required: true }))
157
+ ```
158
+
159
+ ---
160
+
161
+ ## `defineFormSteps`
162
+
163
+ Helper that creates a typed reactive object for multi-step forms. It is the recommended way to declare form data for `useMultiStepValidatedForm` — TypeScript infers the full nested shape so no separate type declaration is needed.
164
+
165
+ ```typescript
166
+ import { defineFormSteps } from '@core/packages/validated-form'
167
+
168
+ const formData = defineFormSteps({
169
+ general: { category: undefined as string | undefined, title: '' },
170
+ details: { region: undefined as string | undefined, quantity: undefined as number | undefined },
171
+ })
172
+ ```
173
+
174
+ Top-level keys become step names. Values are plain objects whose keys become field names. The returned object is reactive (equivalent to wrapping with `reactive()`).
175
+
176
+ ---
177
+
178
+ ## `useMultiStepValidatedForm` composable
179
+
180
+ Single entry point for wizard-style forms. The form data is declared as a nested reactive object where each top-level key is a step name — the structure itself encodes step membership, so no fields array is needed and no step prefix is required when creating bindings.
181
+
182
+ TypeScript infers step names from the data and config keys, giving full autocompletion on `currentStep`, `validateStep('stepName')`, and `isStepValid('stepName')`.
183
+
184
+ ### Usage
185
+
186
+ ```typescript
187
+ import { defineFormSteps, useMultiStepValidatedForm } from '@core/packages/validated-form'
188
+
189
+ const formData = defineFormSteps({
190
+ general: { category: undefined as string | undefined, title: '' },
191
+ details: { region: undefined as string | undefined, quantity: undefined as number | undefined },
192
+ })
193
+
194
+ const { useField, currentStep, next, back, isStepValid, areAllStepsValid, validateAllSteps } =
195
+ useMultiStepValidatedForm(formData, {
196
+ general: {
197
+ errors: { onSubmit: () => ({ category: { required }, title: { required } }) },
198
+ },
199
+ details: {
200
+ errors: {
201
+ onSubmit: () => ({ region: { required }, quantity: { required } }),
202
+ onBlur: () => ({ quantity: { outOfRange: outOfRange(1, 999) } }),
203
+ },
204
+ },
205
+ })
206
+ ```
207
+
208
+ ### Parameters
209
+
210
+ | | Required | Type | Description |
211
+ | ------------- | :------: | -------------------------------------------------------- | ------------------------------------------------------------------------------- |
212
+ | `data` | ✓ | `TData` | A nested reactive object — top-level keys are step names, values are field maps |
213
+ | `stepConfigs` | ✓ | `{ [K in keyof TData]: FormValidationConfig<TData[K]> }` | Validation config for each step, typed to that step's fields only |
214
+
215
+ Each step config is a `FormValidationConfig` scoped to its own sub-object: writing a field key from another step inside a step's rules is a TypeScript error.
216
+
217
+ ### Return value
218
+
219
+ #### Binding factories
220
+
221
+ | Property | Signature | Description |
222
+ | --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
223
+ | `useField` | `(key, extras?) => ComputedRef<ModelBinding & FieldMetadata & E>` | Same as `useValidatedForm`'s `useField` — step routing is automatic from the data structure |
224
+ | `useFormSelect` | `(key, sources, config?) => UseFormSelectReturn` | Same as `useValidatedForm`'s `useFormSelect` — step routing is automatic |
225
+ | `useSelect` | see overloads in `useValidatedForm` | Same overloads as `useValidatedForm`'s `useSelect` — resolves step from registry or explicit key |
226
+
227
+ `key` for `useField` and `useFormSelect` accepts any field name from any step's sub-object. The composable resolves which step's validation instance to use based on where the field is declared in `data`.
228
+
229
+ #### Navigation
230
+
231
+ | Property | Type | Description |
232
+ | ------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
233
+ | `currentStep` | `ComputedRef<keyof TSteps & string>` | The currently active step name |
234
+ | `next` | `() => Promise<boolean>` | Validates the current step; advances to the next step only if validation passes. Returns `true` if the step was valid |
235
+ | `back` | `() => void` | Goes to the previous step without validation |
236
+
237
+ #### Validation state
238
+
239
+ | Property | Type | Description |
240
+ | ------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
241
+ | `areAllStepsValid` | `ComputedRef<boolean>` | `true` only when every step has been validated and all pass |
242
+ | `isStepValid` | `(step: keyof TSteps) => boolean \| undefined` | Last known validation result for a step; `undefined` if the step has never been validated |
243
+ | `isValidating` | `Ref<boolean>` | `true` while `validateAllSteps()` is in flight; guards against concurrent calls |
244
+ | `validateStep` | `(step: keyof TSteps) => Promise<boolean>` | Validates a single step by name and stores the result |
245
+ | `validateAllSteps` | `() => Promise<boolean>` | Validates all steps concurrently. Returns `true` only if every step passes |
246
+
247
+ `validateAllSteps` is a no-op (returns `false`) when `isValidating` is already `true`.
248
+
249
+ ### Example: wizard form with navigation and per-step validation
250
+
251
+ ```typescript
252
+ import { required, outOfRange } from '@core/packages/form-validation'
253
+ import { defineFormSteps, useMultiStepValidatedForm } from '@core/packages/validated-form'
254
+ import { useI18n } from 'vue-i18n'
255
+
256
+ export function useCreateItemWizard() {
257
+ const { t } = useI18n()
258
+
259
+ const formData = defineFormSteps({
260
+ general: {
261
+ category: undefined as string | undefined,
262
+ title: '',
263
+ summary: '',
264
+ featured: false,
265
+ },
266
+ details: {
267
+ region: undefined as string | undefined,
268
+ quantity: undefined as number | undefined,
269
+ },
270
+ })
271
+
272
+ const {
273
+ useField,
274
+ useFormSelect,
275
+ useSelect,
276
+ currentStep,
277
+ next,
278
+ back,
279
+ isStepValid,
280
+ areAllStepsValid,
281
+ validateAllSteps,
282
+ } = useMultiStepValidatedForm(formData, {
283
+ general: {
284
+ errors: {
285
+ onSubmit: () => ({
286
+ category: { required },
287
+ title: { required },
288
+ }),
289
+ },
290
+ },
291
+ details: {
292
+ errors: {
293
+ onSubmit: () => ({
294
+ region: { required },
295
+ quantity: { required },
296
+ }),
297
+ onBlur: () => ({ quantity: { outOfRange: outOfRange(1, 999) } }),
298
+ },
299
+ },
300
+ })
301
+
302
+ // Bindings — no step prefix needed, routing is automatic
303
+ const { id: categorySelectId } = useFormSelect('category', categories, {
304
+ searchable: true,
305
+ option: { label: 'name', value: 'id' },
306
+ })
307
+ const categorySelectBindings = useSelect(categorySelectId, () => ({ label: t('category') }))
308
+ const titleInputBindings = useField('title', () => ({ label: t('title'), required: true }))
309
+ const summaryInputBindings = useField('summary', () => ({ label: t('summary') }))
310
+ const featuredCheckboxBindings = useField('featured')
311
+
312
+ const { id: regionSelectId } = useFormSelect('region', regions, {
313
+ searchable: true,
314
+ option: { label: 'name', value: 'id' },
315
+ })
316
+ const regionSelectBindings = useSelect(regionSelectId, () => ({ label: t('region') }))
317
+ const quantityInputBindings = useField('quantity', () => ({ label: t('quantity'), required: true }))
318
+
319
+ async function onSubmit() {
320
+ const isValid = await validateAllSteps()
321
+
322
+ if (!isValid) {
323
+ return
324
+ }
325
+
326
+ return {
327
+ categoryId: formData.general.category!,
328
+ title: formData.general.title,
329
+ ...(formData.general.summary !== '' && { summary: formData.general.summary }),
330
+ ...(formData.general.featured && { featured: true }),
331
+ region: formData.details.region!,
332
+ quantity: formData.details.quantity!,
333
+ }
334
+ }
335
+
336
+ return {
337
+ formData,
338
+ currentStep,
339
+ next,
340
+ back,
341
+ isStepValid,
342
+ areAllStepsValid,
343
+ categorySelectBindings,
344
+ titleInputBindings,
345
+ summaryInputBindings,
346
+ featuredCheckboxBindings,
347
+ regionSelectBindings,
348
+ quantityInputBindings,
349
+ onSubmit,
350
+ }
351
+ }
352
+ ```
353
+
354
+ ```vue
355
+ <script lang="ts" setup>
356
+ const {
357
+ currentStep,
358
+ next,
359
+ back,
360
+ areAllStepsValid,
361
+ categorySelectBindings,
362
+ titleInputBindings,
363
+ summaryInputBindings,
364
+ featuredCheckboxBindings,
365
+ regionSelectBindings,
366
+ quantityInputBindings,
367
+ onSubmit,
368
+ } = useCreateItemWizard()
369
+ </script>
370
+
371
+ <template>
372
+ <VtsForm @submit="onSubmit">
373
+ <div v-if="currentStep === 'general'">
374
+ <VtsSelect v-bind="categorySelectBindings" />
375
+ <VtsInput v-bind="titleInputBindings" />
376
+ <VtsInput v-bind="summaryInputBindings" />
377
+ <VtsCheckbox v-bind="featuredCheckboxBindings" />
378
+ </div>
379
+ <div v-else-if="currentStep === 'details'">
380
+ <VtsSelect v-bind="regionSelectBindings" />
381
+ <VtsInput v-bind="quantityInputBindings" />
382
+ </div>
383
+
384
+ <button :disabled="currentStep === 'general'" @click="back">Back</button>
385
+ <button v-if="currentStep !== 'details'" @click="next">Next</button>
386
+ <button v-else :disabled="!areAllStepsValid" @click="onSubmit">Submit</button>
387
+ </VtsForm>
388
+ </template>
389
+ ```
@@ -0,0 +1,2 @@
1
+ export * from './use-multi-step-validated-form.ts'
2
+ export * from './use-validated-form.ts'
@@ -0,0 +1,203 @@
1
+ import type { CollectionItemProperties } from '@core/packages/collection'
2
+ import type {
3
+ ExtractValue,
4
+ FormSelectId,
5
+ GetOptionValue,
6
+ UseFormSelectReturn,
7
+ } from '@core/packages/form-select/types.ts'
8
+ import type { FormValidationConfig } from '@core/packages/form-validation/types.ts'
9
+ import type { EmptyObject } from '@core/types/utility.type.ts'
10
+ import { computed, reactive, ref, shallowReactive, type ComputedRef, type MaybeRefOrGetter } from 'vue'
11
+ import {
12
+ useValidatedForm,
13
+ type FieldMetadata,
14
+ type ModelBinding,
15
+ type UseFormSelectConfig,
16
+ } from './use-validated-form.ts'
17
+
18
+ export type NestedFormData = Record<string, Record<string, unknown>>
19
+
20
+ type FlatKeys<TData extends NestedFormData> = { [K in keyof TData]: keyof TData[K] & string }[keyof TData]
21
+
22
+ type FieldValue<TData extends NestedFormData, K extends string> = {
23
+ [S in keyof TData]: K extends keyof TData[S] ? TData[S][K] : never
24
+ }[keyof TData]
25
+
26
+ type StepForms<TData extends NestedFormData> = {
27
+ [K in keyof TData]: ReturnType<typeof useValidatedForm<TData[K]>>
28
+ }
29
+
30
+ export function useMultiStepValidatedForm<
31
+ TData extends NestedFormData,
32
+ TSteps extends { [K in keyof TData]: FormValidationConfig<TData[K]> },
33
+ >(data: TData, stepConfigs: TSteps) {
34
+ const stepForms = Object.fromEntries(
35
+ (Object.keys(stepConfigs) as (keyof TData & string)[]).map(stepKey => [
36
+ stepKey,
37
+ useValidatedForm(data[stepKey], stepConfigs[stepKey]),
38
+ ])
39
+ ) as unknown as StepForms<TData>
40
+
41
+ const fieldToStep = new Map<FlatKeys<TData>, keyof TData & string>()
42
+
43
+ for (const stepKey of Object.keys(data) as (keyof TData & string)[]) {
44
+ for (const fieldKey of Object.keys(data[stepKey]) as FlatKeys<TData>[]) {
45
+ if (fieldToStep.has(fieldKey)) {
46
+ throw new Error(`useMultiStepValidatedForm: field "${fieldKey}" is declared in multiple steps`)
47
+ }
48
+
49
+ fieldToStep.set(fieldKey, stepKey)
50
+ }
51
+ }
52
+
53
+ const idToStep = new Map<FormSelectId, keyof TData & string>()
54
+
55
+ const stepKeys = Object.keys(stepConfigs) as (keyof TSteps & string)[]
56
+ const currentStepIndex = ref(0)
57
+ const currentStep = computed(() => stepKeys[currentStepIndex.value])
58
+
59
+ const stepValidStates = shallowReactive(new Map<keyof TSteps, boolean>())
60
+ const isValidating = ref(false)
61
+
62
+ const areAllStepsValid = computed(
63
+ () => stepKeys.length === stepValidStates.size && [...stepValidStates.values()].every(Boolean)
64
+ )
65
+
66
+ function isStepValid(stepName: keyof TSteps): boolean | undefined {
67
+ return stepValidStates.get(stepName)
68
+ }
69
+
70
+ async function validateStep(stepName: keyof TSteps & string): Promise<boolean> {
71
+ const isValid = await stepForms[stepName].validate()
72
+ stepValidStates.set(stepName, isValid)
73
+ return isValid
74
+ }
75
+
76
+ async function validateAllSteps(): Promise<boolean> {
77
+ if (isValidating.value) {
78
+ return false
79
+ }
80
+
81
+ isValidating.value = true
82
+
83
+ try {
84
+ const results = await Promise.all(stepKeys.map(key => stepForms[key].validate()))
85
+ stepValidStates.clear()
86
+ stepKeys.forEach((key, index) => stepValidStates.set(key, results[index]))
87
+ return results.every(Boolean)
88
+ } finally {
89
+ isValidating.value = false
90
+ }
91
+ }
92
+
93
+ async function next(): Promise<boolean> {
94
+ const isValid = await validateStep(stepKeys[currentStepIndex.value])
95
+
96
+ if (isValid && currentStepIndex.value < stepKeys.length - 1) {
97
+ currentStepIndex.value++
98
+ }
99
+
100
+ return isValid
101
+ }
102
+
103
+ function back(): void {
104
+ if (currentStepIndex.value > 0) {
105
+ currentStepIndex.value--
106
+ }
107
+ }
108
+
109
+ function resolveStepFromField<K extends FlatKeys<TData>>(key: K): keyof TData & string {
110
+ const stepKey = fieldToStep.get(key)
111
+
112
+ if (stepKey === undefined) {
113
+ throw new Error(`useMultiStepValidatedForm: field "${String(key)}" is not declared in any step`)
114
+ }
115
+
116
+ return stepKey
117
+ }
118
+
119
+ function useField<K extends FlatKeys<TData>>(key: K): ComputedRef<ModelBinding<FieldValue<TData, K>> & FieldMetadata>
120
+ function useField<K extends FlatKeys<TData>, E extends Record<string, unknown>>(
121
+ key: K,
122
+ extras: () => E
123
+ ): ComputedRef<ModelBinding<FieldValue<TData, K>> & FieldMetadata & E>
124
+ function useField<K extends FlatKeys<TData>, E extends Record<string, unknown>>(key: K, extras?: () => E): unknown {
125
+ const stepKey = resolveStepFromField(key)
126
+ const stepForm = stepForms[stepKey] as StepForms<TData>[keyof TData] & {
127
+ useField: (key: string, extras?: () => Record<string, unknown>) => unknown
128
+ }
129
+
130
+ return extras !== undefined ? stepForm.useField(key, extras) : stepForm.useField(key)
131
+ }
132
+
133
+ function useFormSelect<
134
+ TSource,
135
+ TCustomProperties extends CollectionItemProperties = EmptyObject,
136
+ TGetValue extends GetOptionValue<TSource, TCustomProperties> = undefined,
137
+ TMultiple extends boolean = false,
138
+ TEmptyValue = never,
139
+ $TValue = ExtractValue<TSource, TGetValue>,
140
+ >(
141
+ key: FlatKeys<TData>,
142
+ sources: MaybeRefOrGetter<TSource[]>,
143
+ formSelectConfig?: UseFormSelectConfig<TSource, TCustomProperties>
144
+ ): UseFormSelectReturn<TCustomProperties, TSource, $TValue | TEmptyValue, TMultiple> {
145
+ const stepKey = resolveStepFromField(key)
146
+ const result = stepForms[stepKey].useFormSelect(key as never, sources, formSelectConfig) as UseFormSelectReturn<
147
+ TCustomProperties,
148
+ TSource,
149
+ $TValue | TEmptyValue,
150
+ TMultiple
151
+ >
152
+ idToStep.set(result.id, stepKey)
153
+ return result
154
+ }
155
+
156
+ function useSelect(id: FormSelectId): ComputedRef<{ id: FormSelectId } & FieldMetadata>
157
+ function useSelect<E extends Record<string, unknown>>(
158
+ id: FormSelectId,
159
+ extras: () => E
160
+ ): ComputedRef<{ id: FormSelectId } & FieldMetadata & E>
161
+ function useSelect(id: FormSelectId, key: FlatKeys<TData>): ComputedRef<{ id: FormSelectId } & FieldMetadata>
162
+ function useSelect<E extends Record<string, unknown>>(
163
+ id: FormSelectId,
164
+ key: FlatKeys<TData>,
165
+ extras: () => E
166
+ ): ComputedRef<{ id: FormSelectId } & FieldMetadata & E>
167
+ function useSelect<E extends Record<string, unknown> = Record<string, unknown>>(
168
+ id: FormSelectId,
169
+ keyOrExtras?: FlatKeys<TData> | (() => E),
170
+ extras?: () => E
171
+ ) {
172
+ if (typeof keyOrExtras === 'string') {
173
+ const stepKey = resolveStepFromField(keyOrExtras)
174
+ return stepForms[stepKey].useSelect(id, keyOrExtras as never, extras as never)
175
+ }
176
+
177
+ const stepKey = idToStep.get(id)
178
+
179
+ if (stepKey === undefined) {
180
+ throw new Error('useSelect: could not resolve step for select id — ensure useFormSelect was called first')
181
+ }
182
+
183
+ return stepForms[stepKey].useSelect(id, keyOrExtras as never)
184
+ }
185
+
186
+ return {
187
+ useField,
188
+ useFormSelect,
189
+ useSelect,
190
+ currentStep,
191
+ next,
192
+ back,
193
+ isStepValid,
194
+ areAllStepsValid,
195
+ isValidating,
196
+ validateStep,
197
+ validateAllSteps,
198
+ }
199
+ }
200
+
201
+ export function defineFormSteps<T extends NestedFormData>(steps: T): T {
202
+ return reactive(steps) as T
203
+ }