@xen-orchestra/web-core 0.56.0 → 0.57.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 (68) hide show
  1. package/lib/assets/css/base.pcss +5 -0
  2. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  3. package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
  4. package/lib/components/menu/MenuItem.vue +9 -1
  5. package/lib/components/menu/MenuList.vue +4 -2
  6. package/lib/components/menu/VtsActionsMenu.vue +24 -24
  7. package/lib/components/modal/VtsActionModal.vue +44 -9
  8. package/lib/components/modal/VtsBlockedModal.vue +1 -1
  9. package/lib/components/modal/VtsDeleteModal.vue +1 -1
  10. package/lib/components/overlay/VtsOverlayButton.vue +35 -0
  11. package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
  12. package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
  13. package/lib/components/overlay/VtsOverlayList.vue +82 -0
  14. package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
  15. package/lib/components/select/VtsSelect.vue +0 -3
  16. package/lib/components/space-card/VtsSpaceCard.vue +1 -1
  17. package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
  18. package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
  19. package/lib/components/task/VtsQuickTaskButton.vue +1 -1
  20. package/lib/components/task/VtsQuickTaskList.vue +1 -1
  21. package/lib/components/tree/VtsTreeItem.vue +4 -4
  22. package/lib/components/ui/head-bar/UiHeadBar.vue +1 -1
  23. package/lib/components/ui/input/UiInput.vue +53 -9
  24. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
  25. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
  26. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
  27. package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
  28. package/lib/components/ui/task-list/UiTaskList.vue +1 -1
  29. package/lib/components/ui/text-area/UiTextarea.vue +12 -14
  30. package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
  31. package/lib/composables/table/multi-select.composable.md +33 -0
  32. package/lib/layouts/CoreLayout.vue +74 -62
  33. package/lib/locales/en.json +63 -2
  34. package/lib/locales/fr.json +63 -2
  35. package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
  36. package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
  37. package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
  38. package/lib/packages/form-validation/index.ts +2 -0
  39. package/lib/packages/hide-permanently/README.md +60 -0
  40. package/lib/packages/hide-permanently/types.ts +3 -0
  41. package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
  42. package/lib/packages/overlay/OverlayComponent.vue +18 -0
  43. package/lib/packages/overlay/README.md +285 -0
  44. package/lib/packages/overlay/create-event-handler.ts +80 -0
  45. package/lib/packages/overlay/injection-keys.ts +3 -0
  46. package/lib/packages/overlay/is-thenable.ts +3 -0
  47. package/lib/packages/overlay/symbols.ts +5 -0
  48. package/lib/packages/overlay/types.ts +94 -0
  49. package/lib/packages/overlay/use-overlay-escape.ts +34 -0
  50. package/lib/packages/overlay/use-overlay-store.ts +33 -0
  51. package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
  52. package/lib/packages/overlay/use-overlay.ts +99 -0
  53. package/lib/packages/progress/use-progress.ts +12 -2
  54. package/lib/packages/sidebar/index.ts +2 -0
  55. package/lib/packages/sidebar/sidebar.store.ts +55 -0
  56. package/lib/packages/sidebar/types.ts +21 -0
  57. package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
  58. package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
  59. package/lib/tables/column-definitions/user-name-column.ts +10 -0
  60. package/lib/tables/column-sets/server-columns.ts +2 -2
  61. package/lib/tables/column-sets/user-columns.ts +16 -0
  62. package/lib/types/object.type.ts +9 -1
  63. package/lib/types/task.type.ts +5 -12
  64. package/lib/utils/injection-keys.util.ts +3 -1
  65. package/lib/utils/ip-address.utils.ts +7 -0
  66. package/lib/utils/sr.utils.ts +4 -0
  67. package/package.json +2 -1
  68. package/lib/stores/sidebar.store.ts +0 -79
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <UiTableCell>
3
3
  <div class="container">
4
- <VtsProgressBar :current :total noruler class="progress" />
4
+ <VtsProgressBar :current :total no-ruler class="progress" />
5
5
  <span>{{ n(percentage / 100, { maximumFractionDigits: 0, style: 'percent' }) }}</span>
6
6
  </div>
7
7
  </UiTableCell>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <UiTableCell>
3
+ <div class="user-name">
4
+ <UiUserLogo size="small" />
5
+ <UiLink :href size="medium">
6
+ {{ content }}
7
+ </UiLink>
8
+ </div>
9
+ </UiTableCell>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import UiLink from '@core/components/ui/link/UiLink.vue'
14
+ import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
15
+ import UiUserLogo from '@core/components/ui/user-logo/UiUserLogo.vue'
16
+
17
+ defineProps<{
18
+ content: string
19
+ href?: string
20
+ }>()
21
+ </script>
22
+
23
+ <style scoped lang="postcss">
24
+ .user-name {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 0.8rem;
28
+ }
29
+ </style>
@@ -19,8 +19,8 @@
19
19
  import VtsBackdrop from '@core/components/backdrop/VtsBackdrop.vue'
20
20
  import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
21
21
  import UiQuickTaskPanel from '@core/components/ui/quick-task-panel/UiQuickTaskPanel.vue'
22
+ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
22
23
  import { vTooltip } from '@core/directives/tooltip.directive'
23
- import type { Task } from '@core/types/task.type.ts'
24
24
  import { unrefElement, watchArray, whenever } from '@vueuse/core'
25
25
  import placementJs from 'placement.js'
26
26
  import { computed, nextTick, ref } from 'vue'
@@ -27,7 +27,7 @@
27
27
  <script lang="ts" setup>
28
28
  import UiLoader from '@core/components/ui/loader/UiLoader.vue'
29
29
  import UiQuickTaskItem from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
30
- import type { Task } from '@core/types/task.type.ts'
30
+ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
31
31
  import { useI18n } from 'vue-i18n'
32
32
  import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
33
33
 
@@ -6,10 +6,10 @@
6
6
  </template>
7
7
 
8
8
  <script lang="ts" setup>
9
+ import { useLeftSidebarStore } from '@core/packages/sidebar'
9
10
  import type { TreeNodeId } from '@core/packages/tree/types.ts'
10
- import { useSidebarStore } from '@core/stores/sidebar.store'
11
- import { useUiStore } from '@core/stores/ui.store'
12
- import { IK_TREE_ITEM_EXPANDED, IK_TREE_ITEM_HAS_CHILDREN } from '@core/utils/injection-keys.util'
11
+ import { useUiStore } from '@core/stores/ui.store.ts'
12
+ import { IK_TREE_ITEM_EXPANDED, IK_TREE_ITEM_HAS_CHILDREN } from '@core/utils/injection-keys.util.ts'
13
13
  import { onBeforeMount, onBeforeUpdate, provide, ref, toRef, useSlots } from 'vue'
14
14
 
15
15
  const props = defineProps<{
@@ -23,7 +23,7 @@ defineSlots<{
23
23
  sublist?(): any
24
24
  }>()
25
25
 
26
- const sidebar = useSidebarStore()
26
+ const sidebar = useLeftSidebarStore()
27
27
  const uiStore = useUiStore()
28
28
  const hasChildrenState = ref(false)
29
29
 
@@ -3,7 +3,7 @@
3
3
  <div class="ui-head-bar">
4
4
  <div class="label-wrapper">
5
5
  <slot name="icon">
6
- <VtsIcon :name="icon" size="medium" />
6
+ <VtsIcon :name="icon" size="large" />
7
7
  </slot>
8
8
  <h4 v-tooltip class="typo-h4 label text-ellipsis">
9
9
  <slot />
@@ -1,6 +1,11 @@
1
- <!-- v5 -->
1
+ <!-- v9 -->
2
2
  <template>
3
- <div :class="toVariants({ accent, disabled })" class="ui-input" @click.self="focus()">
3
+ <div :class="toVariants({ accent, disabled, selected })" class="ui-input" @click.self="focus()">
4
+ <span v-if="slots.prefix || prefix" class="prefix">
5
+ <slot name="prefix">
6
+ {{ prefix }}
7
+ </slot>
8
+ </span>
4
9
  <span v-if="readonly" class="typo-body-regular input text-ellipsis">
5
10
  <input ref="inputRef" class="readonly-input" readonly type="text" />
6
11
  {{ modelValue }}
@@ -19,14 +24,19 @@
19
24
  />
20
25
  <UiButtonIcon
21
26
  v-if="!disabled && modelValue && clearable"
22
- icon="fa:xmark"
23
- accent="brand"
24
27
  size="small"
28
+ icon="action:close-cancel-clear"
29
+ accent="brand"
25
30
  @click="clear()"
26
31
  />
27
32
  <slot v-if="slots['right-icon'] || rightIcon" name="right-icon">
28
33
  <VtsIcon :name="rightIcon" size="medium" class="right-icon" />
29
34
  </slot>
35
+ <span v-if="slots.suffix || suffix" class="suffix">
36
+ <slot name="suffix">
37
+ {{ suffix }}
38
+ </slot>
39
+ </span>
30
40
  </div>
31
41
  </template>
32
42
 
@@ -58,12 +68,14 @@ const {
58
68
  id?: string
59
69
  required?: boolean
60
70
  disabled?: boolean
71
+ selected?: boolean
61
72
  readonly?: boolean
62
73
  detached?: boolean
63
74
  type?: InputType
64
- icon?: IconName
65
75
  rightIcon?: IconName
66
76
  clearable?: boolean
77
+ prefix?: string
78
+ suffix?: string
67
79
  }>()
68
80
 
69
81
  const emit = defineEmits<{
@@ -74,6 +86,8 @@ const modelValue = defineModel<string | number | undefined>({ required: true })
74
86
 
75
87
  const slots = defineSlots<{
76
88
  'right-icon'?(): any
89
+ prefix?(): any
90
+ suffix?(): any
77
91
  }>()
78
92
 
79
93
  function handleInput(event: Event) {
@@ -135,6 +149,31 @@ defineExpose({ focus })
135
149
  color: var(--color-brand-txt-base);
136
150
  }
137
151
 
152
+ .prefix,
153
+ .suffix {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ align-self: stretch;
158
+ background-color: var(--color-neutral-background-secondary);
159
+ padding: 0.8rem 1.6rem;
160
+ color: var(--color-neutral-txt-secondary);
161
+ }
162
+
163
+ .prefix {
164
+ border-end-start-radius: 0.4rem;
165
+ border-start-start-radius: 0.4rem;
166
+ border-inline-end: 0.1rem solid var(--color-neutral-border);
167
+ margin-inline-start: -1.6rem;
168
+ }
169
+
170
+ .suffix {
171
+ border-end-end-radius: 0.4rem;
172
+ border-start-end-radius: 0.4rem;
173
+ border-inline-start: 0.1rem solid var(--color-neutral-border);
174
+ margin-inline-end: -1.6rem;
175
+ }
176
+
138
177
  &.disabled .right-icon {
139
178
  color: var(--color-neutral-txt-secondary);
140
179
  }
@@ -163,12 +202,9 @@ defineExpose({ focus })
163
202
  /* VARIANT */
164
203
 
165
204
  &.accent--brand {
205
+ --selected-color: var(--color-brand-item-base);
166
206
  border-color: var(--color-neutral-border);
167
207
 
168
- &:focus-within {
169
- border-color: var(--color-brand-item-base);
170
- }
171
-
172
208
  &:hover {
173
209
  border-color: var(--color-brand-item-hover);
174
210
  }
@@ -184,6 +220,7 @@ defineExpose({ focus })
184
220
  }
185
221
 
186
222
  &.accent--warning {
223
+ --selected-color: var(--color-warning-item-base);
187
224
  border-color: var(--color-warning-item-base);
188
225
 
189
226
  &:hover {
@@ -201,6 +238,7 @@ defineExpose({ focus })
201
238
  }
202
239
 
203
240
  &.accent--danger {
241
+ --selected-color: var(--color-danger-item-base);
204
242
  border-color: var(--color-danger-item-base);
205
243
 
206
244
  &:hover {
@@ -216,5 +254,11 @@ defineExpose({ focus })
216
254
  background-color: var(--color-neutral-background-disabled);
217
255
  }
218
256
  }
257
+
258
+ &:focus-within,
259
+ &.selected {
260
+ border-color: var(--selected-color);
261
+ border-width: 0.2rem;
262
+ }
219
263
  }
220
264
  </style>
@@ -10,24 +10,12 @@
10
10
  type="text"
11
11
  accent="brand"
12
12
  :aria-label="uiStore.isSmall ? t('query-search-bar:label') : undefined"
13
- :icon="!uiStore.isSmall ? 'fa:magnifying-glass' : undefined"
14
13
  :placeholder="t('query-search-bar:placeholder')"
15
14
  />
16
15
  <template v-if="!uiStore.isSmall">
17
- <UiButton size="medium" accent="brand" variant="primary" type="submit" class="action-button">
16
+ <UiButton size="medium" accent="brand" variant="secondary" type="submit" class="action-button">
18
17
  {{ t('action:search') }}
19
18
  </UiButton>
20
- <VtsDivider type="stretch" />
21
- <UiButton
22
- v-tooltip="t('coming-soon!')"
23
- size="medium"
24
- accent="brand"
25
- variant="secondary"
26
- disabled
27
- class="action-button"
28
- >
29
- {{ t('query-search-bar:use-query-builder') }}
30
- </UiButton>
31
19
  </template>
32
20
 
33
21
  <!-- Mobile icons: search + filter -->
@@ -39,11 +27,9 @@
39
27
  </template>
40
28
 
41
29
  <script lang="ts" setup>
42
- import VtsDivider from '@core/components/divider/VtsDivider.vue'
43
30
  import UiButton from '@core/components/ui/button/UiButton.vue'
44
31
  import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
45
32
  import UiInput from '@core/components/ui/input/UiInput.vue'
46
- import { vTooltip } from '@core/directives/tooltip.directive'
47
33
  import { useUiStore } from '@core/stores/ui.store'
48
34
  import { uniqueId } from '@core/utils/unique-id.util'
49
35
  import { ref } from 'vue'
@@ -34,7 +34,7 @@ import VtsIcon from '@core/components/icon/VtsIcon.vue'
34
34
  import VtsQuickTaskList from '@core/components/task/VtsQuickTaskList.vue'
35
35
  import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
36
36
  import UiTag from '@core/components/ui/tag/UiTag.vue'
37
- import type { Task } from '@core/types/task.type.ts'
37
+ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
38
38
  import { useToggle } from '@vueuse/core'
39
39
  import { computed } from 'vue'
40
40
  import { useI18n } from 'vue-i18n'
@@ -15,8 +15,8 @@
15
15
  <script lang="ts" setup>
16
16
  import VtsQuickTaskList from '@core/components/task/VtsQuickTaskList.vue'
17
17
  import VtsQuickTaskTabBar, { type TaskTab } from '@core/components/task/VtsQuickTaskTabBar.vue'
18
+ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
18
19
  import { useUiStore } from '@core/stores/ui.store'
19
- import type { Task } from '@core/types/task.type.ts'
20
20
  import { computed, ref } from 'vue'
21
21
 
22
22
  const props = defineProps<{
@@ -21,10 +21,13 @@
21
21
  </div>
22
22
 
23
23
  <div class="main-content">
24
- <div v-if="task.name" class="content-left">
25
- <UiLink size="medium">
26
- {{ task.name }}
27
- </UiLink>
24
+ <div v-if="task.nameParts || task.name" class="content-left">
25
+ <template v-if="task.nameParts">
26
+ <template v-for="(part, index) in task.nameParts" :key="index">
27
+ <UiLink size="medium" :to="part.to">{{ part.text }}</UiLink>
28
+ </template>
29
+ </template>
30
+ <UiLink v-else size="small">{{ task.name }}</UiLink>
28
31
  <div v-if="shouldShowInfos || hasSubTasks" class="infos">
29
32
  <UiCounter v-if="hasSubTasks" :value="subTasksCount" accent="brand" variant="secondary" size="small" />
30
33
  <UiInfo v-if="hasInfos" accent="info" />
@@ -41,7 +44,7 @@
41
44
  <UiCircleProgressBar :accent="progressAccent" size="small" :value="progress" />
42
45
  </div>
43
46
  <div class="actions">
44
- <UiButtonIcon icon="fa:eye" size="medium" accent="brand" @click="emit('select', task.id)" />
47
+ <UiButtonIcon icon="fa:eye" size="small" accent="brand" @click="emit('select', task.id)" />
45
48
  </div>
46
49
  </div>
47
50
  </div>
@@ -61,6 +64,7 @@ import UiLink from '@core/components/ui/link/UiLink.vue'
61
64
  import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
62
65
  import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
63
66
  import { vTooltip } from '@core/directives/tooltip.directive'
67
+ import type { TaskObjectSegment, TaskStatus } from '@core/types/task.type.ts'
64
68
  import { logicOr } from '@vueuse/math'
65
69
  import { computed } from 'vue'
66
70
  import { useI18n } from 'vue-i18n'
@@ -69,9 +73,13 @@ export type Task = {
69
73
  id: string
70
74
  infos?: { data: unknown; message: string }[]
71
75
  name?: string
76
+ nameParts?: TaskObjectSegment[]
72
77
  progress?: number
78
+ tag?: string
79
+ userName?: string
80
+ start?: number
73
81
  end?: number
74
- status: 'failure' | 'interrupted' | 'pending' | 'success'
82
+ status: TaskStatus
75
83
  subtasks?: Task[]
76
84
  warnings?: { data: unknown; message: string }[]
77
85
  }
@@ -16,8 +16,8 @@
16
16
 
17
17
  <script lang="ts" setup>
18
18
  import UiTaskItem from '@core/components/ui/task-item/UiTaskItem.vue'
19
+ import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
19
20
  import { useCollection } from '@core/packages/collection'
20
- import type { Task } from '@core/types/task.type.ts'
21
21
 
22
22
  const { tasks, depth = 0 } = defineProps<{
23
23
  tasks: Task[]
@@ -1,16 +1,19 @@
1
- <!-- v2 -->
1
+ <!-- v5 -->
2
2
  <template>
3
- <div class="ui-textarea" :class="toVariants({ accent: hasMaxCharactersError ? 'danger' : accent })">
3
+ <div class="ui-textarea" :class="toVariants({ accent: isExceedingMaxCharacters ? 'danger' : accent })">
4
4
  <UiLabel v-if="slots.default" :accent="labelAccent" :required :href :for="id">
5
5
  <slot />
6
6
  </UiLabel>
7
7
  <textarea v-bind="attrs" :id ref="textarea" v-model="model" :disabled class="textarea" />
8
8
  <UiCharacterLimit v-if="maxCharacters" :count="model.trim().length" :max="maxCharacters" />
9
+ <UiInfo v-if="slots.info" accent="info">
10
+ <slot name="info" />
11
+ </UiInfo>
9
12
  <UiInfo v-if="isExceedingMaxCharacters" accent="danger">
10
13
  {{ t('field:exceeds-max-characters', { max: maxCharacters }) }}
11
14
  </UiInfo>
12
- <UiInfo v-if="slots.info" :accent="accent === 'brand' ? 'info' : accent">
13
- <slot name="info" />
15
+ <UiInfo v-if="slots.message && labelAccent !== 'neutral'" :accent="labelAccent">
16
+ <slot name="message" />
14
17
  </UiInfo>
15
18
  </div>
16
19
  </template>
@@ -46,6 +49,7 @@ const model = defineModel<string>({ required: true })
46
49
  const slots = defineSlots<{
47
50
  default?(): any
48
51
  info?(): any
52
+ message?(): any
49
53
  }>()
50
54
 
51
55
  const { t } = useI18n()
@@ -58,16 +62,10 @@ const { focused } = useFocus(textAreaElement)
58
62
 
59
63
  // WIP: To update when using VeeValidate and custom validation rules
60
64
  const isExceedingMaxCharacters = computed(() =>
61
- maxCharacters !== undefined ? model.value.trim().length > maxCharacters : false
65
+ maxCharacters !== undefined ? !focused.value && model.value.trim().length > maxCharacters : false
62
66
  )
63
67
 
64
- const hasMaxCharactersError = computed(() => !focused.value && isExceedingMaxCharacters.value)
65
-
66
68
  const labelAccent = computed(() => {
67
- if (hasMaxCharactersError.value) {
68
- return 'danger'
69
- }
70
-
71
69
  return accent === 'brand' ? 'neutral' : accent
72
70
  })
73
71
  </script>
@@ -101,8 +99,8 @@ const labelAccent = computed(() => {
101
99
  }
102
100
 
103
101
  &:focus:not(:active) {
104
- border-width: 0.2rem;
105
102
  border-color: var(--color-brand-item-base);
103
+ box-shadow: inset 0 0 0 0.1rem var(--color-brand-item-base);
106
104
  }
107
105
 
108
106
  &:disabled {
@@ -125,8 +123,8 @@ const labelAccent = computed(() => {
125
123
  }
126
124
 
127
125
  &:focus:not(:active) {
128
- border-width: 0.2rem;
129
126
  border-color: var(--color-warning-item-base);
127
+ box-shadow: inset 0 0 0 0.1rem var(--color-warning-item-base);
130
128
  }
131
129
 
132
130
  &:disabled {
@@ -149,8 +147,8 @@ const labelAccent = computed(() => {
149
147
  }
150
148
 
151
149
  &:focus:not(:active) {
152
- border-width: 0.2rem;
153
150
  border-color: var(--color-danger-item-base);
151
+ box-shadow: inset 0 0 0 0.1rem var(--color-danger-item-base);
154
152
  }
155
153
 
156
154
  &:disabled {
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <VtsModal accent="info" icon="status:info-picto" dismissible>
3
+ <template #title>
4
+ {{ connection.title }}
5
+ </template>
6
+
7
+ <template v-if="action === CONNECTION_ACTION.DISCONNECT" #content>
8
+ {{ t('vif-disconnect-info') }}
9
+ </template>
10
+
11
+ <template #buttons>
12
+ <VtsModalCancelButton>{{ t('action:go-back') }}</VtsModalCancelButton>
13
+ <VtsModalConfirmButton>
14
+ {{ connection.action }}
15
+ </VtsModalConfirmButton>
16
+ </template>
17
+ </VtsModal>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import VtsModal from '@core/components/modal/VtsModal.vue'
22
+ import VtsModalCancelButton from '@core/components/modal/VtsModalCancelButton.vue'
23
+ import VtsModalConfirmButton from '@core/components/modal/VtsModalConfirmButton.vue'
24
+ import { useMapper } from '@core/packages/mapper'
25
+ import { CONNECTION_ACTION, type ConnectionAction } from '@core/types/connection.ts'
26
+ import { useI18n } from 'vue-i18n'
27
+
28
+ const { action, count } = defineProps<{
29
+ action: ConnectionAction
30
+ count: number
31
+ }>()
32
+
33
+ const { t } = useI18n()
34
+
35
+ const connection = useMapper(
36
+ () => action,
37
+ () => ({
38
+ connect: { title: t('vif-connect-title', { n: count }), action: t('action:connect-n-vifs', { n: count }) },
39
+ disconnect: { title: t('vif-disconnect-title', { n: count }), action: t('action:disconnect-n-vifs', { n: count }) },
40
+ }),
41
+ 'connect'
42
+ )
43
+ </script>
@@ -0,0 +1,33 @@
1
+ # useMultiSelect composable
2
+
3
+ ```vue
4
+ <template>
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th>
9
+ <input type="checkbox" v-model="areAllSelected" />
10
+ </th>
11
+ <th>Name</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <tr v-for="item in items">
16
+ <td>
17
+ <input type="checkbox" :value="item.id" v-model="selected" />
18
+ </td>
19
+ <td>{{ item.name }}</td>
20
+ </tr>
21
+ </tbody>
22
+ </table>
23
+
24
+ <!-- You can use something else than a "Select All" checkbox -->
25
+ <button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
26
+ </template>
27
+
28
+ <script lang="ts" setup>
29
+ import useMultiSelect from './multi-select.composable'
30
+
31
+ const { selected, areAllSelected } = useMultiSelect()
32
+ </script>
33
+ ```