@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
@@ -0,0 +1,180 @@
1
+ import type { InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
2
+ import type { CollectionItemProperties, GetItemId } from '@core/packages/collection'
3
+ import { useFormBindings } from '@core/packages/form-bindings'
4
+ import { useFormSelect as _useFormSelect } from '@core/packages/form-select'
5
+ import type {
6
+ ExtractValue,
7
+ FormSelectId,
8
+ GetOptionLabel,
9
+ GetOptionValue,
10
+ UseFormSelectReturn,
11
+ } from '@core/packages/form-select/types.ts'
12
+ import { useFormValidation } from '@core/packages/form-validation'
13
+ import type { FormValidationConfig } from '@core/packages/form-validation/types.ts'
14
+ import type { EmptyObject, MaybeArray } from '@core/types/utility.type.ts'
15
+ import { toRef, type MaybeRefOrGetter, type ComputedRef, type Ref } from 'vue'
16
+
17
+ export type ModelBinding<T> = { modelValue: T; 'onUpdate:modelValue': (value: T) => void }
18
+
19
+ export type FieldMetadata = {
20
+ error: InputWrapperMessage | undefined
21
+ warning: InputWrapperMessage | undefined
22
+ onBlur: () => void
23
+ }
24
+
25
+ function toMessage(content: string | undefined, accent: 'danger' | 'warning'): InputWrapperMessage | undefined {
26
+ return content !== undefined ? { content, accent } : undefined
27
+ }
28
+
29
+ export type UseFormSelectConfig<TSource, TCustomProperties extends CollectionItemProperties> = {
30
+ multiple?: MaybeRefOrGetter<boolean>
31
+ disabled?: MaybeRefOrGetter<boolean>
32
+ selectedLabel?: (count: number, labels: string[]) => string | undefined
33
+ placeholder?: MaybeRefOrGetter<string>
34
+ searchPlaceholder?: MaybeRefOrGetter<string>
35
+ loading?: MaybeRefOrGetter<boolean>
36
+ required?: MaybeRefOrGetter<boolean>
37
+ searchable?: MaybeRefOrGetter<boolean>
38
+ emptyOption?: MaybeRefOrGetter<{
39
+ value: unknown
40
+ properties?: TCustomProperties
41
+ label: string
42
+ selectedLabel?: string
43
+ }>
44
+ option?: {
45
+ id?: GetItemId<TSource>
46
+ value?:
47
+ | GetOptionValue<TSource, TCustomProperties>
48
+ | ((source: TSource, properties: TCustomProperties, index: number) => unknown)
49
+ properties?: (source: TSource) => TCustomProperties
50
+ label?: GetOptionLabel<TSource, TCustomProperties>
51
+ selectedLabel?: (source: TSource, properties: TCustomProperties) => string
52
+ disabled?: (source: TSource, properties: TCustomProperties) => boolean
53
+ searchableTerm?: (source: TSource, properties: TCustomProperties) => MaybeArray<string>
54
+ }
55
+ }
56
+
57
+ export function useValidatedForm<TData extends Record<string, unknown>>(
58
+ data: TData,
59
+ config: FormValidationConfig<TData>
60
+ ) {
61
+ const { useField: _useField, useSelect: _useSelect } = useFormBindings(data)
62
+ const { errors: rawErrors, warnings: rawWarnings, validate, reset, handleBlur } = useFormValidation(data, config)
63
+
64
+ const selectRegistry = new Map<FormSelectId, keyof TData>()
65
+
66
+ function fieldMetadata(key: keyof TData): () => FieldMetadata {
67
+ return () => ({
68
+ error: toMessage(rawErrors.value[key], 'danger'),
69
+ warning: toMessage(rawWarnings.value[key], 'warning'),
70
+ onBlur: () => handleBlur(key),
71
+ })
72
+ }
73
+
74
+ function fieldMetadataWithExtras<E extends Record<string, unknown>>(
75
+ key: keyof TData,
76
+ extras: () => E
77
+ ): () => FieldMetadata & E {
78
+ return () => ({
79
+ error: toMessage(rawErrors.value[key], 'danger'),
80
+ warning: toMessage(rawWarnings.value[key], 'warning'),
81
+ onBlur: () => handleBlur(key),
82
+ ...extras(),
83
+ })
84
+ }
85
+
86
+ function useField<K extends keyof TData>(key: K): ComputedRef<ModelBinding<TData[K]> & FieldMetadata>
87
+ function useField<K extends keyof TData, E extends Record<string, unknown>>(
88
+ key: K,
89
+ extras: () => E
90
+ ): ComputedRef<ModelBinding<TData[K]> & FieldMetadata & E>
91
+ function useField<K extends keyof TData, E extends Record<string, unknown> = Record<string, unknown>>(
92
+ key: K,
93
+ extras?: () => E
94
+ ) {
95
+ if (extras !== undefined) {
96
+ return _useField(key, fieldMetadataWithExtras(key, extras))
97
+ }
98
+
99
+ return _useField(key, fieldMetadata(key))
100
+ }
101
+
102
+ function useFormSelect<
103
+ TSource,
104
+ TCustomProperties extends CollectionItemProperties = EmptyObject,
105
+ TGetValue extends GetOptionValue<TSource, TCustomProperties> = undefined,
106
+ TMultiple extends boolean = false,
107
+ TEmptyValue = never,
108
+ $TValue = ExtractValue<TSource, TGetValue>,
109
+ >(
110
+ key: keyof TData,
111
+ sources: MaybeRefOrGetter<TSource[]>,
112
+ formSelectConfig?: UseFormSelectConfig<TSource, TCustomProperties>
113
+ ): UseFormSelectReturn<TCustomProperties, TSource, $TValue | TEmptyValue, TMultiple> {
114
+ const model = toRef(data, key) as Ref<unknown>
115
+
116
+ const result = (
117
+ _useFormSelect as unknown as (
118
+ sources: MaybeRefOrGetter<TSource[]>,
119
+ config: UseFormSelectConfig<TSource, TCustomProperties> & { model: Ref<unknown> }
120
+ ) => UseFormSelectReturn<TCustomProperties, TSource, $TValue | TEmptyValue, TMultiple>
121
+ )(sources, {
122
+ ...formSelectConfig,
123
+ model,
124
+ })
125
+
126
+ selectRegistry.set(result.id, key)
127
+
128
+ return result
129
+ }
130
+
131
+ function useSelect(id: FormSelectId): ComputedRef<{ id: FormSelectId } & FieldMetadata>
132
+ function useSelect<E extends Record<string, unknown>>(
133
+ id: FormSelectId,
134
+ extras: () => E
135
+ ): ComputedRef<{ id: FormSelectId } & FieldMetadata & E>
136
+ function useSelect<E extends Record<string, unknown>>(
137
+ id: FormSelectId,
138
+ key: keyof TData,
139
+ extras?: () => E
140
+ ): ComputedRef<{ id: FormSelectId } & FieldMetadata & E>
141
+ function useSelect<E extends Record<string, unknown> = Record<string, unknown>>(
142
+ id: FormSelectId,
143
+ keyOrExtras?: keyof TData | (() => E),
144
+ extras?: () => E
145
+ ) {
146
+ if (typeof keyOrExtras === 'string') {
147
+ const key = keyOrExtras as keyof TData
148
+ const metadata = extras !== undefined ? fieldMetadataWithExtras(key, extras) : fieldMetadata(key)
149
+
150
+ return _useSelect(id, metadata)
151
+ }
152
+
153
+ const extrasFromRegistry = keyOrExtras as (() => E) | undefined
154
+ const registryKey = selectRegistry.get(id)
155
+
156
+ if (registryKey !== undefined) {
157
+ const metadata =
158
+ extrasFromRegistry !== undefined
159
+ ? fieldMetadataWithExtras(registryKey, extrasFromRegistry)
160
+ : fieldMetadata(registryKey)
161
+
162
+ return _useSelect(id, metadata)
163
+ }
164
+
165
+ if (extrasFromRegistry !== undefined) {
166
+ return _useSelect(id, extrasFromRegistry)
167
+ }
168
+
169
+ return _useSelect(id)
170
+ }
171
+
172
+ return {
173
+ useField,
174
+ useFormSelect,
175
+ useSelect,
176
+ validate,
177
+ reset,
178
+ handleBlur,
179
+ }
180
+ }
@@ -0,0 +1,22 @@
1
+ export type ParsedTag = {
2
+ term: string
3
+ label: string
4
+ }
5
+
6
+ export function parseTag(value: string): ParsedTag | null {
7
+ const index = value.indexOf('=')
8
+
9
+ if (index === -1) {
10
+ return null
11
+ }
12
+
13
+ const term = value.slice(0, index).trim()
14
+
15
+ const label = value.slice(index + 1).trim()
16
+
17
+ if (term === '' || label === '') {
18
+ return null
19
+ }
20
+
21
+ return { term, label }
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xen-orchestra/web-core",
3
3
  "type": "module",
4
- "version": "0.52.0",
4
+ "version": "0.53.0",
5
5
  "private": false,
6
6
  "exports": {
7
7
  "./*": {
@@ -17,11 +17,14 @@
17
17
  "@fortawesome/free-solid-svg-icons": "^6.7.2",
18
18
  "@fortawesome/vue-fontawesome": "^3.0.8",
19
19
  "@novnc/novnc": "~1.5.0",
20
+ "@regle/core": "^1.20.2",
21
+ "@regle/rules": "^1.20.2",
22
+ "@regle/schemas": "^1.20.2",
20
23
  "@types/d3-time-format": "^4.0.3",
21
- "@vueuse/core": "^13.0.0",
22
- "@vueuse/math": "^13.0.0",
23
- "@vueuse/router": "^13.0.0",
24
- "@vueuse/shared": "^13.0.0",
24
+ "@vueuse/core": "^14.0.0",
25
+ "@vueuse/math": "^14.0.0",
26
+ "@vueuse/router": "^14.0.0",
27
+ "@vueuse/shared": "^14.0.0",
25
28
  "complex-matcher": "^1.1.1",
26
29
  "d3-time-format": "^4.1.0",
27
30
  "echarts": "^5.6.0",
@@ -38,16 +41,16 @@
38
41
  "pinia": "^3.0.1",
39
42
  "vue": "~3.5.13",
40
43
  "vue-i18n": "^11.1.2",
41
- "vue-router": "^4.5.0"
44
+ "vue-router": "^5.0.0"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@types/lodash-es": "^4.17.12",
45
48
  "@types/novnc__novnc": "~1.5.0",
46
- "@vue/tsconfig": "^0.7.0",
49
+ "@vue/tsconfig": "^0.9.0",
47
50
  "pinia": "^3.0.1",
48
51
  "vue": "~3.5.13",
49
52
  "vue-i18n": "^11.1.2",
50
- "vue-router": "^4.5.0"
53
+ "vue-router": "^5.0.0"
51
54
  },
52
55
  "homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
53
56
  "bugs": "https://github.com/vatesfr/xen-orchestra/issues",
package/tsconfig.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "extends": "@vue/tsconfig/tsconfig.dom.json",
3
- "include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
4
- "exclude": ["lib/**/__tests__/*"],
3
+ "include": ["env.d.ts", "./lib/**/*", "./lib/**/*.vue"],
4
+ "exclude": ["./lib/**/__tests__/*"],
5
5
  "compilerOptions": {
6
6
  "noEmit": true,
7
- "baseUrl": ".",
8
7
  "paths": {
9
8
  "@core/*": ["./lib/*"]
10
9
  }
@@ -1,21 +0,0 @@
1
- <template>
2
- <VtsIcon :class="{ muted: disabled }" :name="icon" size="medium" class="chip-icon" />
3
- </template>
4
-
5
- <script lang="ts" setup>
6
- import VtsIcon from '@core/components/icon/VtsIcon.vue'
7
- import type { IconName } from '@core/icons'
8
-
9
- defineProps<{
10
- icon?: IconName
11
- disabled?: boolean
12
- }>()
13
- </script>
14
-
15
- <style lang="postcss" scoped>
16
- .chip-icon {
17
- &.muted {
18
- color: var(--color-neutral-txt-secondary);
19
- }
20
- }
21
- </style>
@@ -1,16 +0,0 @@
1
- <template>
2
- <UiButtonIcon :accent="buttonAccent" icon="fa:xmark" size="small" />
3
- </template>
4
-
5
- <script lang="ts" setup>
6
- import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
7
- import type { ChipAccent } from '@core/components/ui/chip/UiChip.vue'
8
- import { computed } from 'vue'
9
-
10
- const { accent } = defineProps<{
11
- accent: ChipAccent
12
- }>()
13
-
14
- // FIXME: temporary fix for the 'success' accent, since it doesn't exist in the UiButtonIcon component
15
- const buttonAccent = computed(() => (accent === 'info' || accent === 'success' ? 'brand' : accent))
16
- </script>