@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
@@ -8,7 +8,8 @@
8
8
  <script lang="ts" setup>
9
9
  import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
10
10
  import { useUiStore } from '@core/stores/ui.store'
11
- import VncClient from '@novnc/novnc/lib/rfb'
11
+ import type NoVncClient from '@novnc/novnc/lib/rfb'
12
+ import _RFB from '@novnc/novnc/lib/rfb'
12
13
  import { whenever } from '@vueuse/core'
13
14
  import { promiseTimeout } from '@vueuse/shared'
14
15
  import { fibonacci } from 'iterable-backoff'
@@ -30,7 +31,10 @@ const FIBONACCI_MS_ARRAY: number[] = Array.from(fibonacci().toMs().take(N_TOTAL_
30
31
  const consoleContainer = useTemplateRef<HTMLDivElement | null>('console-container')
31
32
  const isReady = ref(false)
32
33
 
33
- let vncClient: VncClient | undefined
34
+ // See https://rolldown.rs/in-depth/bundling-cjs#ambiguous-default-import-from-cjs-modules
35
+ const VncClient = typeof _RFB === 'object' && _RFB !== null && (_RFB as any).__esModule ? (_RFB as any).default : _RFB
36
+
37
+ let vncClient: NoVncClient | undefined
34
38
  let nConnectionAttempts = 0
35
39
 
36
40
  function handleDisconnectionEvent() {
@@ -80,7 +84,7 @@ async function createVncConnection() {
80
84
 
81
85
  vncClient = new VncClient(consoleContainer.value!, url.toString(), {
82
86
  wsProtocols: ['binary'],
83
- })
87
+ }) as NoVncClient
84
88
  vncClient.scaleViewport = true
85
89
  vncClient.background = 'transparent'
86
90
 
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <form novalidate @submit.prevent="emit('submit')">
3
+ <slot />
4
+ </form>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const emit = defineEmits<{
9
+ submit: []
10
+ }>()
11
+
12
+ defineSlots<{
13
+ default(): any
14
+ }>()
15
+ </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <UiInfo v-tooltip="iconOnly ? currentStatus.text : false" class="vts-status" :accent="currentStatus.accent">
2
+ <UiInfo v-tooltip="iconOnly ? currentStatus.text : false" :accent="currentStatus.accent">
3
3
  <template v-if="!iconOnly">{{ currentStatus.text }}</template>
4
4
  </UiInfo>
5
5
  </template>
@@ -62,9 +62,3 @@ const currentStatus = useMapper<Status, { text: string; accent: InfoAccent }>(
62
62
  false
63
63
  )
64
64
  </script>
65
-
66
- <style lang="postcss" scoped>
67
- .vts-status {
68
- align-items: center;
69
- }
70
- </style>
@@ -1,16 +1,14 @@
1
1
  <template>
2
2
  <UiTableCell>
3
3
  <UiTagsList>
4
- <UiTag v-for="tagItem of tags" :key="tagItem" accent="info" variant="secondary">
5
- {{ tagItem }}
6
- </UiTag>
4
+ <VtsTag v-for="tagItem of tags" :key="tagItem" :value="tagItem" />
7
5
  </UiTagsList>
8
6
  </UiTableCell>
9
7
  </template>
10
8
 
11
9
  <script setup lang="ts">
10
+ import VtsTag from '@core/components/tag/VtsTag.vue'
12
11
  import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
13
- import UiTag from '@core/components/ui/tag/UiTag.vue'
14
12
  import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
15
13
  import type { MaybeArray } from '@core/types/utility.type'
16
14
  import { toArray } from '@core/utils/to-array.utils'
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <UiTertiaryTag v-if="parsedTag" :accent>
3
+ <template #term>{{ parsedTag.term }}</template>
4
+ {{ parsedTag.label }}
5
+ </UiTertiaryTag>
6
+ <UiTag v-else :accent :variant>
7
+ {{ value }}
8
+ </UiTag>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import UiTag, { type TagAccent } from '@core/components/ui/tag/UiTag.vue'
13
+ import UiTertiaryTag from '@core/components/ui/tag/UiTertiaryTag.vue'
14
+ import { parseTag } from '@core/utils/parse-tag.util'
15
+ import { computed } from 'vue'
16
+
17
+ type TagVariant = 'primary' | 'secondary'
18
+
19
+ const {
20
+ value,
21
+ accent = 'info',
22
+ variant = 'secondary',
23
+ } = defineProps<{
24
+ value: string
25
+ accent?: TagAccent
26
+ variant?: TagVariant
27
+ }>()
28
+
29
+ const parsedTag = computed(() => parseTag(value))
30
+ </script>
@@ -1,31 +1,28 @@
1
- <!-- v4 -->
1
+ <!-- v8 -->
2
2
  <template>
3
- <span :class="classNames" class="ui-chip typo-body-regular-small" @click="emit('edit')">
4
- <ChipIcon :disabled :icon />
5
- <span class="content text-ellipsis">
3
+ <span :class="classNames" class="ui-chip typo-body-regular-small">
4
+ <span class="text-ellipsis">
6
5
  <slot />
7
6
  </span>
8
- <ChipRemoveIcon v-if="!disabled" :accent @click.stop="emit('remove')" />
7
+ <button v-if="!disabled" class="icon" type="button" @click.stop="emit('remove')">
8
+ <VtsIcon name="action:close-cancel-clear" size="medium" />
9
+ </button>
9
10
  </span>
10
11
  </template>
11
12
 
12
13
  <script lang="ts" setup>
13
- import ChipIcon from '@core/components/ui/chip/ChipIcon.vue'
14
- import ChipRemoveIcon from '@core/components/ui/chip/ChipRemoveIcon.vue'
15
- import type { IconName } from '@core/icons'
14
+ import VtsIcon from '@core/components/icon/VtsIcon.vue'
16
15
  import { toVariants } from '@core/utils/to-variants.util'
17
16
  import { computed } from 'vue'
18
17
 
19
18
  export type ChipAccent = 'info' | 'success' | 'warning' | 'danger'
20
19
 
21
- const props = defineProps<{
20
+ const { accent, disabled } = defineProps<{
22
21
  accent: ChipAccent
23
- icon?: IconName
24
22
  disabled?: boolean
25
23
  }>()
26
24
 
27
25
  const emit = defineEmits<{
28
- edit: []
29
26
  remove: []
30
27
  }>()
31
28
 
@@ -36,8 +33,8 @@ defineSlots<{
36
33
  const classNames = computed(() => {
37
34
  return [
38
35
  toVariants({
39
- accent: props.accent,
40
- muted: props.disabled,
36
+ accent,
37
+ muted: disabled,
41
38
  }),
42
39
  ]
43
40
  })
@@ -45,37 +42,51 @@ const classNames = computed(() => {
45
42
 
46
43
  <style lang="postcss" scoped>
47
44
  .ui-chip {
48
- display: flex;
45
+ display: inline-flex;
49
46
  align-items: center;
50
- gap: 0.8rem;
51
- padding: 0.4rem 0.8rem;
47
+ gap: 0.4rem;
48
+ padding: 0.4rem 1.2rem;
52
49
  border-radius: 10rem;
53
50
  color: var(--color-neutral-txt-primary);
54
- cursor: pointer;
55
51
  min-height: 2.4rem;
56
52
  vertical-align: middle;
57
53
  white-space: nowrap;
58
- min-width: 0;
59
54
 
60
55
  &.muted {
61
56
  color: var(--color-neutral-txt-secondary);
62
57
  pointer-events: none;
63
58
  }
64
59
 
65
- .content {
66
- line-height: 1.6rem;
60
+ .icon {
61
+ border-radius: 0 10rem 10rem 0;
62
+ padding: 0.4rem;
63
+ margin: -0.4rem -1.2rem -0.4rem 0;
64
+ align-self: stretch;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ cursor: pointer;
69
+ background-color: transparent;
70
+ border: none;
67
71
  }
68
72
 
73
+ .icon:focus-visible {
74
+ outline: 0.2rem solid var(--color-brand-txt-base);
75
+ outline-offset: 0.2rem;
76
+ }
69
77
  /* COLOR VARIANTS */
70
-
71
78
  &.accent--info {
72
79
  background-color: var(--color-info-background-selected);
73
80
 
74
- &:is(:hover, :focus-visible) {
81
+ .icon {
82
+ color: var(--color-info-txt-hover);
83
+ }
84
+
85
+ .icon:hover {
75
86
  background-color: var(--color-info-background-hover);
76
87
  }
77
88
 
78
- &:active {
89
+ .icon:active {
79
90
  background-color: var(--color-info-background-active);
80
91
  }
81
92
 
@@ -87,11 +98,15 @@ const classNames = computed(() => {
87
98
  &.accent--success {
88
99
  background-color: var(--color-success-background-selected);
89
100
 
90
- &:is(:hover, :focus-visible) {
101
+ .icon {
102
+ color: var(--color-success-txt-hover);
103
+ }
104
+
105
+ .icon:hover {
91
106
  background-color: var(--color-success-background-hover);
92
107
  }
93
108
 
94
- &:active {
109
+ .icon:active {
95
110
  background-color: var(--color-success-background-active);
96
111
  }
97
112
 
@@ -103,11 +118,15 @@ const classNames = computed(() => {
103
118
  &.accent--warning {
104
119
  background-color: var(--color-warning-background-selected);
105
120
 
106
- &:is(:hover, :focus-visible) {
121
+ .icon {
122
+ color: var(--color-warning-txt-hover);
123
+ }
124
+
125
+ .icon:hover {
107
126
  background-color: var(--color-warning-background-hover);
108
127
  }
109
128
 
110
- &:active {
129
+ .icon:active {
111
130
  background-color: var(--color-warning-background-active);
112
131
  }
113
132
 
@@ -119,11 +138,15 @@ const classNames = computed(() => {
119
138
  &.accent--danger {
120
139
  background-color: var(--color-danger-background-selected);
121
140
 
122
- &:is(:hover, :focus-visible) {
141
+ .icon {
142
+ color: var(--color-danger-txt-hover);
143
+ }
144
+
145
+ .icon:hover {
123
146
  background-color: var(--color-danger-background-hover);
124
147
  }
125
148
 
126
- &:active {
149
+ .icon:active {
127
150
  background-color: var(--color-danger-background-active);
128
151
  }
129
152
 
@@ -1,8 +1,8 @@
1
- <!-- v4 -->
1
+ <!-- v6 -->
2
2
  <template>
3
3
  <div class="ui-info">
4
- <VtsIcon class="icon" :name="icon" size="medium" />
5
- <p v-tooltip="!wrap" class="typo-body-regular-small label" :class="{ 'text-ellipsis': !wrap }">
4
+ <VtsIcon class="icon" :name="icon" :size />
5
+ <p v-tooltip="!wrap" class="label" :class="[textSize, { 'text-ellipsis': !wrap }]">
6
6
  <slot />
7
7
  </p>
8
8
  </div>
@@ -15,10 +15,12 @@ import type { IconName } from '@core/icons'
15
15
  import { useMapper } from '@core/packages/mapper'
16
16
 
17
17
  export type InfoAccent = 'info' | 'success' | 'warning' | 'danger' | 'muted'
18
+ type Size = 'small' | 'medium'
18
19
 
19
- const { accent } = defineProps<{
20
+ const { accent, size = 'medium' } = defineProps<{
20
21
  accent: InfoAccent
21
22
  wrap?: boolean
23
+ size?: Size
22
24
  }>()
23
25
 
24
26
  defineSlots<{
@@ -36,16 +38,25 @@ const icon = useMapper<InfoAccent, IconName>(
36
38
  },
37
39
  'muted'
38
40
  )
41
+
42
+ const textSize = useMapper<Size, any>(
43
+ () => size,
44
+ {
45
+ small: 'typo-body-regular-small',
46
+ medium: 'typo-body-regular',
47
+ },
48
+ 'medium'
49
+ )
39
50
  </script>
40
51
 
41
52
  <style lang="postcss" scoped>
42
53
  .ui-info {
43
- align-items: start;
54
+ align-items: baseline;
44
55
  display: flex;
45
56
  gap: 0.8rem;
46
57
 
47
58
  .icon {
48
- font-size: 1.6rem;
59
+ transform: translateY(0.2ex);
49
60
  }
50
61
 
51
62
  .label:empty {
@@ -1,15 +1,17 @@
1
- <!-- v3 -->
1
+ <!-- v6 -->
2
2
  <template>
3
3
  <component :is="component" :class="classes" class="ui-link" v-bind="attributes">
4
4
  <VtsIcon :name="icon" size="medium" />
5
5
  <slot />
6
- <VtsIcon v-if="attributes.target === '_blank'" name="fa:up-right-from-square" size="medium" class="external-icon" />
6
+ <VtsIcon v-if="attributes.target === '_blank'" name="action:open-in-new-tab" size="medium" class="external-icon" />
7
+ <VtsIcon v-if="isPrimary" v-tooltip="primaryTooltip" name="status:primary-circle" size="medium" />
7
8
  </component>
8
9
  </template>
9
10
 
10
11
  <script lang="ts" setup>
11
12
  import VtsIcon from '@core/components/icon/VtsIcon.vue'
12
13
  import { type LinkOptions, useLinkComponent } from '@core/composables/link-component.composable'
14
+ import { vTooltip } from '@core/directives/tooltip.directive'
13
15
  import type { IconName } from '@core/icons'
14
16
  import { computed } from 'vue'
15
17
 
@@ -17,6 +19,8 @@ const props = defineProps<
17
19
  LinkOptions & {
18
20
  size: 'small' | 'medium'
19
21
  icon?: IconName
22
+ isPrimary?: boolean
23
+ primaryTooltip?: string
20
24
  }
21
25
  >()
22
26
 
@@ -1,34 +1,27 @@
1
- <!-- v3 -->
2
- <!-- TODO: implement tertiary variant to bump to v4 -->
1
+ <!-- v5 -->
3
2
  <template>
4
- <span :class="toVariants({ accent, variant })" class="ui-tag typo-body-regular-small">
5
- <slot name="icon">
6
- <VtsIcon :name="icon" size="medium" />
7
- </slot>
8
- <span>
9
- <slot />
10
- </span>
3
+ <span :class="className" class="ui-tag typo-body-regular-small">
4
+ <slot />
11
5
  </span>
12
6
  </template>
13
7
 
14
8
  <script lang="ts" setup>
15
- import VtsIcon from '@core/components/icon/VtsIcon.vue'
16
- import type { IconName } from '@core/icons'
17
9
  import { toVariants } from '@core/utils/to-variants.util'
10
+ import { computed } from 'vue'
18
11
 
19
- type TagAccent = 'info' | 'neutral' | 'success' | 'warning' | 'danger' | 'muted'
12
+ export type TagAccent = 'info' | 'neutral' | 'success' | 'warning' | 'danger' | 'muted'
20
13
  type TagVariant = 'primary' | 'secondary'
21
14
 
22
- defineProps<{
15
+ const { accent, variant } = defineProps<{
23
16
  accent: TagAccent
24
17
  variant: TagVariant
25
- icon?: IconName
26
18
  }>()
27
19
 
28
20
  defineSlots<{
29
21
  default(): any
30
- icon?(): any
31
22
  }>()
23
+
24
+ const className = computed(() => toVariants({ accent, variant }))
32
25
  </script>
33
26
 
34
27
  <style lang="postcss" scoped>
@@ -39,9 +32,9 @@ defineSlots<{
39
32
  gap: 0.8rem;
40
33
  white-space: normal;
41
34
  word-break: break-word;
42
- padding: 0.2rem 0.8rem;
43
- border-radius: 0.4rem;
35
+ padding: 0.35rem 0.8rem;
44
36
  vertical-align: middle;
37
+ border-radius: 0.4rem;
45
38
 
46
39
  /* COLOR VARIANTS */
47
40
 
@@ -0,0 +1,39 @@
1
+ <!-- v1 -->
2
+ <template>
3
+ <span class="ui-tertiary-tag">
4
+ <UiTag :accent variant="primary" class="radius-left">
5
+ <slot name="term" />
6
+ </UiTag>
7
+ <UiTag :accent variant="secondary" class="radius-right">
8
+ <slot />
9
+ </UiTag>
10
+ </span>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import UiTag, { type TagAccent } from '@core/components/ui/tag/UiTag.vue'
15
+
16
+ defineProps<{
17
+ accent: TagAccent
18
+ }>()
19
+
20
+ defineSlots<{
21
+ default(): any
22
+ term(): any
23
+ }>()
24
+ </script>
25
+
26
+ <style lang="postcss" scoped>
27
+ .ui-tertiary-tag {
28
+ display: inline-flex;
29
+ flex-direction: row;
30
+
31
+ :deep(.radius-left) {
32
+ border-radius: 0.4rem 0 0 0.4rem;
33
+ }
34
+
35
+ :deep(.radius-right) {
36
+ border-radius: 0 0.4rem 0.4rem 0;
37
+ }
38
+ }
39
+ </style>
@@ -39,7 +39,7 @@ import {
39
39
  function constructIcon(icon: IconDefinition): IconSingleConfig {
40
40
  return {
41
41
  icon,
42
- color: 'var(--color-neutral-txt-primary)',
42
+ color: 'currentColor',
43
43
  }
44
44
  }
45
45
 
@@ -326,6 +326,10 @@
326
326
  "for-replication": "For replication",
327
327
  "force-reboot-blocked": "Force reboot Blocked",
328
328
  "force-shutdown-blocked": "Force shutdown Blocked",
329
+ "form:error:field-required": "{field} is required",
330
+ "form:error:integer": "Must be an integer",
331
+ "form:error:required": "This field is required",
332
+ "form:warning:out-of-range": "Should be between {min} and {max}",
329
333
  "format": "Format",
330
334
  "free-space": "Free space",
331
335
  "fullscreen": "Fullscreen",
@@ -326,6 +326,10 @@
326
326
  "for-replication": "Pour la réplication",
327
327
  "force-reboot-blocked": "Redémarrage forcé bloqué",
328
328
  "force-shutdown-blocked": "Arrêt forcé bloqué",
329
+ "form:error:field-required": "Le champ {field} est requis",
330
+ "form:error:integer": "Doit être un entier",
331
+ "form:error:required": "Ce champ est requis",
332
+ "form:warning:out-of-range": "Devrait être compris entre {min} et {max}",
329
333
  "format": "Format",
330
334
  "free-space": "Espace libre",
331
335
  "fullscreen": "Plein écran",