@xen-orchestra/web-core 0.53.0 → 0.55.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 (80) hide show
  1. package/lib/components/button-group/VtsButtonGroup.vue +11 -1
  2. package/lib/components/drawer/VtsDrawer.vue +85 -0
  3. package/lib/components/drawer/VtsDrawerButton.vue +21 -0
  4. package/lib/components/drawer/VtsDrawerCancelButton.vue +33 -0
  5. package/lib/components/drawer/VtsDrawerConfirmButton.vue +30 -0
  6. package/lib/components/drawer/VtsDrawerList.vue +34 -0
  7. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -1
  8. package/lib/components/layout/VtsLayoutSidebar.vue +5 -2
  9. package/lib/components/modal/VtsModalButton.vue +0 -1
  10. package/lib/components/query-builder/VtsQueryBuilderModal.vue +0 -1
  11. package/lib/components/state-hero/VtsStateHero.vue +17 -7
  12. package/lib/components/table/cells/VtsDoubleLinkCell.vue +1 -1
  13. package/lib/components/table/cells/VtsHeaderCell.vue +1 -17
  14. package/lib/components/table/cells/VtsTagCell.vue +4 -2
  15. package/lib/components/ui/drawer/UiDrawer.vue +123 -0
  16. package/lib/components/ui/input/UiInput.vue +7 -3
  17. package/lib/components/ui/modal/UiModal.vue +10 -18
  18. package/lib/composables/default-tab.composable.ts +4 -0
  19. package/lib/icons/action-icons.ts +6 -2
  20. package/lib/icons/custom-icons.ts +16 -0
  21. package/lib/icons/object-icons.ts +33 -18
  22. package/lib/icons/status-icons.ts +1 -1
  23. package/lib/locales/cs.json +135 -4
  24. package/lib/locales/da.json +1 -3
  25. package/lib/locales/de.json +141 -26
  26. package/lib/locales/en.json +124 -10
  27. package/lib/locales/es.json +32 -5
  28. package/lib/locales/fr.json +124 -10
  29. package/lib/locales/ko.json +132 -0
  30. package/lib/locales/nb-NO.json +18 -20
  31. package/lib/locales/nl.json +111 -13
  32. package/lib/locales/pt-BR.json +0 -3
  33. package/lib/locales/pt.json +0 -3
  34. package/lib/locales/sk.json +134 -3
  35. package/lib/locales/sv.json +171 -5
  36. package/lib/locales/zh-Hans.json +705 -635
  37. package/lib/packages/drawer/DrawerProvider.vue +17 -0
  38. package/lib/packages/drawer/README.md +189 -0
  39. package/lib/packages/drawer/create-drawer-opener.ts +119 -0
  40. package/lib/packages/drawer/drawer.store.ts +22 -0
  41. package/lib/packages/drawer/types.ts +93 -0
  42. package/lib/packages/drawer/use-drawer.ts +53 -0
  43. package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +17 -0
  44. package/lib/packages/form-validation/index.ts +1 -0
  45. package/lib/packages/modal/create-modal-opener.ts +12 -10
  46. package/lib/tables/column-definitions/action-column.ts +1 -1
  47. package/lib/tables/column-definitions/address-column.ts +1 -1
  48. package/lib/tables/column-definitions/button-column.ts +1 -1
  49. package/lib/tables/column-definitions/button-icon-column.ts +1 -1
  50. package/lib/tables/column-definitions/collapsed-list-column.ts +1 -1
  51. package/lib/tables/column-definitions/date-column.ts +1 -1
  52. package/lib/tables/column-definitions/double-link-column.ts +1 -1
  53. package/lib/tables/column-definitions/info-column.ts +1 -1
  54. package/lib/tables/column-definitions/input-column.ts +1 -5
  55. package/lib/tables/column-definitions/link-column.ts +1 -1
  56. package/lib/tables/column-definitions/link-or-text-column.ts +1 -1
  57. package/lib/tables/column-definitions/literal-column.ts +1 -1
  58. package/lib/tables/column-definitions/number-column.ts +1 -1
  59. package/lib/tables/column-definitions/percent-column.ts +1 -1
  60. package/lib/tables/column-definitions/progress-bar-column.ts +1 -1
  61. package/lib/tables/column-definitions/select-column.ts +1 -1
  62. package/lib/tables/column-definitions/status-column.ts +2 -2
  63. package/lib/tables/column-definitions/tag-column.ts +3 -2
  64. package/lib/tables/column-definitions/text-column.ts +1 -1
  65. package/lib/tables/column-definitions/truncated-text-column.ts +1 -1
  66. package/lib/tables/column-sets/backup-job-schedule-columns.ts +2 -2
  67. package/lib/tables/column-sets/backup-log-columns.ts +1 -1
  68. package/lib/tables/column-sets/new-vm-network-columns.ts +0 -2
  69. package/lib/tables/column-sets/new-vm-sr-columns.ts +0 -1
  70. package/lib/tables/column-sets/server-columns.ts +1 -1
  71. package/lib/tables/column-sets/snapshot-columns.ts +1 -1
  72. package/lib/tables/column-sets/sr-columns.ts +2 -2
  73. package/lib/tables/column-sets/traffic-rules-columns.ts +5 -7
  74. package/lib/tables/column-sets/vif-network-columns.ts +24 -0
  75. package/lib/tables/helpers/render-head-cell.ts +2 -4
  76. package/lib/tables/types.ts +0 -2
  77. package/lib/types/connection.ts +14 -0
  78. package/lib/types/storage-repository.type.ts +14 -0
  79. package/lib/utils/sr.utils.ts +34 -0
  80. package/package.json +4 -4
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <slot />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import { IK_DRAWER, type RegisteredDrawer } from '@core/packages/drawer/types.ts'
7
+ import { computed, provide } from 'vue'
8
+
9
+ const { drawer } = defineProps<{
10
+ drawer: RegisteredDrawer
11
+ }>()
12
+
13
+ provide(
14
+ IK_DRAWER,
15
+ computed(() => drawer)
16
+ )
17
+ </script>
@@ -0,0 +1,189 @@
1
+ # Drawer System
2
+
3
+ ### Drawers list component
4
+
5
+ Add `VtsDrawerList` once in your app layout. It renders all active drawers from the store.
6
+
7
+ ```vue
8
+ <!-- App.vue -->
9
+ <template>
10
+ ...
11
+ <VtsDrawerList />
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import VtsDrawerList from '@core/components/drawer/VtsDrawerList.vue'
16
+ </script>
17
+ ```
18
+
19
+ ### Opening a drawer (`useDrawer` composable)
20
+
21
+ ```ts
22
+ const openMyDrawer = useDrawer({
23
+ component: import('path/to/MyDrawer.vue'),
24
+ props: {
25
+ foo: 'Foo',
26
+ },
27
+ onConfirm: () => console.log('Confirmed!'),
28
+ onCancel: () => console.log('Cancelled!'),
29
+ })
30
+ ```
31
+
32
+ ```html
33
+ <button @click="openMyDrawer()">Open drawer</button>
34
+ ```
35
+
36
+ | Property | Type | Required | Default | Description |
37
+ | --------------------- | ----------------------------------------------------------------- | :------: | ----------- | ------------------------------------------ |
38
+ | component | `Promise<Component>` | ✓ | | `import('path/to/drawer.vue')` |
39
+ | props | `Record<string, MaybeRef<any>>` | | `{}` | Props passed to the drawer component |
40
+ | onConfirm | `(...args: any[]) => TConfirmPayload \| Promise<TConfirmPayload>` | | `undefined` | Called when the drawer is confirmed |
41
+ | onCancel | `(...args: any[]) => TCancelPayload \| Promise<TCancelPayload>` | | `undefined` | Called when the drawer is cancelled |
42
+ | keepOpenOnRouteChange | `boolean` | | `false` | By default the drawer closes on navigation |
43
+
44
+ `useDrawer()` with no arguments returns the raw `openDrawer(id, config)` function for advanced use cases where you manage the drawer ID yourself.
45
+
46
+ ### Drawer component
47
+
48
+ A drawer component uses `VtsDrawer` and its sub-components:
49
+
50
+ ```vue
51
+ <!-- MyDrawer.vue -->
52
+ <template>
53
+ <VtsDrawer :dismissible>
54
+ <template #title>My Drawer</template>
55
+ <template #content>
56
+ <!-- your content here -->
57
+ </template>
58
+ <template #buttons>
59
+ <VtsDrawerCancelButton />
60
+ <VtsDrawerConfirmButton>Save</VtsDrawerConfirmButton>
61
+ </template>
62
+ </VtsDrawer>
63
+ </template>
64
+
65
+ <script lang="ts" setup>
66
+ import VtsDrawer from '@core/components/drawer/VtsDrawer.vue'
67
+ import VtsDrawerCancelButton from '@core/components/drawer/VtsDrawerCancelButton.vue'
68
+ import VtsDrawerConfirmButton from '@core/components/drawer/VtsDrawerConfirmButton.vue'
69
+
70
+ defineProps<{
71
+ dismissible?: boolean
72
+ // Injected by VtsDrawerList: true when this drawer is the topmost in the stack.
73
+ // Pass it through to VtsDrawer so only the current drawer reacts to Escape.
74
+ current?: boolean
75
+ }>()
76
+ </script>
77
+ ```
78
+
79
+ `VtsDrawer` props:
80
+
81
+ | Prop | Type | Default | Description |
82
+ | ------------- | --------- | ----------- | --------------------------------------------------------- |
83
+ | `dismissible` | `boolean` | `false` | Allows closing via backdrop click, X button, and Escape |
84
+ | `isOpen` | `boolean` | `undefined` | Controls visibility externally (uncontrolled by default) |
85
+ | `current` | `boolean` | `undefined` | Pass through from parent props — controls Escape handling |
86
+
87
+ When `isOpen` is omitted, `VtsDrawer` manages its own visibility internally (uncontrolled mode). In controlled mode (`:is-open` provided), the `dismiss` event is emitted instead of closing automatically.
88
+
89
+ `VtsDrawerCancelButton` and `VtsDrawerConfirmButton` accept an optional `onClick` prop. When provided, the button emits a `click` event instead of triggering `onCancel`/`onConfirm`:
90
+
91
+ ```vue
92
+ <VtsDrawerCancelButton :on-click="handleCustomCancel" />
93
+ ```
94
+
95
+ ### Opening with arguments
96
+
97
+ ```ts
98
+ const openMyDrawer = useDrawer((name: string) => ({
99
+ component: import('path/to/MyDrawer.vue'),
100
+ props: { name },
101
+ }))
102
+
103
+ openMyDrawer('John')
104
+ ```
105
+
106
+ ### Payload and response
107
+
108
+ Payloads are fully typed based on the return type of `onConfirm` and `onCancel`:
109
+
110
+ ```ts
111
+ const openMyDrawer = useDrawer({
112
+ component: import('path/to/MyDrawer.vue'),
113
+ onConfirm: () => 'saved',
114
+ onCancel: () => 'aborted',
115
+ })
116
+
117
+ const result = await openMyDrawer()
118
+
119
+ if (result.confirmed) {
120
+ result.payload // string
121
+ } else {
122
+ result.payload // string
123
+ }
124
+ ```
125
+
126
+ If `onConfirm` or `onCancel` throws, the promise rejects with the thrown error.
127
+
128
+ ### `onConfirm` and `onCancel` event args
129
+
130
+ If your drawer component defines args for `confirm` and `cancel` events, you'll get them as arguments of the `onConfirm` and `onCancel` callbacks:
131
+
132
+ ```vue
133
+ <!-- MyDrawer.vue -->
134
+ <script lang="ts" setup>
135
+ defineEmits<{
136
+ confirm: [value: string]
137
+ cancel: []
138
+ }>()
139
+ </script>
140
+ ```
141
+
142
+ ```ts
143
+ const openMyDrawer = useDrawer({
144
+ component: import('path/to/MyDrawer.vue'),
145
+ onConfirm: (value: string) => console.log('Confirmed with', value),
146
+ })
147
+ ```
148
+
149
+ ### Busy state
150
+
151
+ If `onConfirm` or `onCancel` returns a `Promise`, the buttons show a busy indicator while the promise is pending.
152
+
153
+ **Important asymmetry:**
154
+
155
+ - `onConfirm` closes the drawer **optimistically** before the async work runs. The drawer is gone before `await` settles.
156
+ - `onCancel` waits for the async work to complete **before** closing the drawer.
157
+
158
+ ```ts
159
+ const openMyDrawer = useDrawer({
160
+ component: import('path/to/MyDrawer.vue'),
161
+ onConfirm: async () => {
162
+ await saveToServer() // drawer is already closed while this runs
163
+ },
164
+ onCancel: async () => {
165
+ await cleanup() // drawer stays open while this runs, then closes
166
+ },
167
+ })
168
+ ```
169
+
170
+ ### Aborting close
171
+
172
+ Return `ABORT_DRAWER` from `onCancel` to keep the drawer open (e.g. to show a confirmation prompt):
173
+
174
+ ```ts
175
+ const openMyDrawer = useDrawer({
176
+ component: import('path/to/MyDrawer.vue'),
177
+ onCancel: async () => {
178
+ if (!hasUnsavedChanges()) {
179
+ return
180
+ }
181
+ const confirmed = await confirmDiscard()
182
+ if (!confirmed) {
183
+ return ABORT_DRAWER // drawer stays open
184
+ }
185
+ },
186
+ })
187
+ ```
188
+
189
+ > **Note:** Returning `ABORT_DRAWER` from `onConfirm` does **not** keep the drawer open. The drawer is closed optimistically before `onConfirm` runs. Returning `ABORT_DRAWER` permanently prevents the promise from resolving — avoid it in `onConfirm`.
@@ -0,0 +1,119 @@
1
+ import { useDrawerStore } from '@core/packages/drawer/drawer.store.ts'
2
+ import {
3
+ ABORT_DRAWER,
4
+ DrawerCancelResponse,
5
+ type DrawerConfig,
6
+ DrawerConfirmResponse,
7
+ type DrawerHandlerArgs,
8
+ type DrawerResponse,
9
+ } from '@core/packages/drawer/types.ts'
10
+ import { computed, defineAsyncComponent, reactive, ref, watch } from 'vue'
11
+ import { useRoute } from 'vue-router'
12
+
13
+ export function createDrawerOpener() {
14
+ const drawerStore = useDrawerStore()
15
+
16
+ const route = useRoute()
17
+
18
+ const ids = new Set<symbol | string>()
19
+ const cancelers = new Map<string | symbol, () => void>()
20
+
21
+ function closeById(id: string | symbol) {
22
+ drawerStore.removeDrawer(id)
23
+ ids.delete(id)
24
+ cancelers.delete(id)
25
+ }
26
+
27
+ if (route !== undefined) {
28
+ watch(
29
+ () => route.path,
30
+ () => {
31
+ ids.forEach(id => {
32
+ if (!drawerStore.getDrawer(id)?.keepOpenOnRouteChange) {
33
+ cancelers.get(id)?.()
34
+ closeById(id)
35
+ }
36
+ })
37
+ }
38
+ )
39
+ }
40
+
41
+ return function openDrawer<
42
+ TProps,
43
+ TConfirmArgs extends DrawerHandlerArgs<TProps, 'onConfirm'>,
44
+ TCancelArgs extends DrawerHandlerArgs<TProps, 'onCancel'>,
45
+ TConfirmPayload = undefined,
46
+ TCancelPayload = undefined,
47
+ >(id: string | symbol, config: DrawerConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>) {
48
+ const close = () => closeById(id)
49
+
50
+ const promise = new Promise<DrawerResponse<TConfirmPayload, TCancelPayload>>((resolve, reject) => {
51
+ const isBusy = ref(false)
52
+
53
+ cancelers.set(id, () => resolve(new DrawerCancelResponse(undefined as TCancelPayload)))
54
+
55
+ drawerStore.addDrawer({
56
+ id,
57
+ component: defineAsyncComponent({
58
+ loader: () => config.component,
59
+ onError(_error, _retry, fail) {
60
+ fail()
61
+ },
62
+ }),
63
+ isBusy: computed(() => isBusy.value),
64
+ props: reactive(config.props ?? {}),
65
+ keepOpenOnRouteChange: config.keepOpenOnRouteChange ?? false,
66
+ onConfirm: async (...args: TConfirmArgs) => {
67
+ try {
68
+ isBusy.value = true
69
+
70
+ close()
71
+
72
+ const result = config.onConfirm ? await config.onConfirm(...args) : (undefined as TConfirmPayload)
73
+
74
+ if (result === ABORT_DRAWER || result instanceof DrawerCancelResponse) {
75
+ return
76
+ }
77
+
78
+ if (result instanceof DrawerConfirmResponse) {
79
+ resolve(result)
80
+ } else {
81
+ resolve(new DrawerConfirmResponse(result))
82
+ }
83
+ } catch (error) {
84
+ reject(error)
85
+ } finally {
86
+ isBusy.value = false
87
+ }
88
+ },
89
+ onCancel: async (...args: TCancelArgs) => {
90
+ try {
91
+ isBusy.value = true
92
+
93
+ const result = config.onCancel ? await config.onCancel(...args) : (undefined as TCancelPayload)
94
+
95
+ if (result === ABORT_DRAWER || result instanceof DrawerCancelResponse) {
96
+ return
97
+ }
98
+
99
+ if (result instanceof DrawerConfirmResponse) {
100
+ resolve(new DrawerCancelResponse(result.payload))
101
+ } else {
102
+ resolve(new DrawerCancelResponse(result))
103
+ }
104
+
105
+ close()
106
+ } catch (error) {
107
+ reject(error)
108
+ } finally {
109
+ isBusy.value = false
110
+ }
111
+ },
112
+ })
113
+
114
+ ids.add(id)
115
+ })
116
+
117
+ return Object.assign(promise, { close })
118
+ }
119
+ }
@@ -0,0 +1,22 @@
1
+ import type { RegisteredDrawer } from '@core/packages/drawer/types.ts'
2
+ import { defineStore } from 'pinia'
3
+ import { computed, shallowReactive } from 'vue'
4
+
5
+ export const useDrawerStore = defineStore('drawer', () => {
6
+ const drawers = shallowReactive(new Map<symbol | string, RegisteredDrawer>())
7
+
8
+ const addDrawer = (drawer: RegisteredDrawer) => {
9
+ drawers.set(drawer.id, drawer)
10
+ }
11
+
12
+ const removeDrawer = (id: symbol | string) => {
13
+ drawers.delete(id)
14
+ }
15
+
16
+ return {
17
+ drawers: computed(() => Array.from(drawers.values())),
18
+ getDrawer: (id: symbol | string) => drawers.get(id),
19
+ addDrawer,
20
+ removeDrawer,
21
+ }
22
+ })
@@ -0,0 +1,93 @@
1
+ import type { Component, ComputedRef, InjectionKey, MaybeRef } from 'vue'
2
+
3
+ export type DrawerPropsOption<TProps> = {
4
+ [K in keyof TProps]: MaybeRef<TProps[K]>
5
+ }
6
+
7
+ export type MaybePromise<T> = T | Promise<T>
8
+
9
+ export type AreAllPropsOptional<TProps> = Record<string, never> extends TProps ? true : false
10
+
11
+ export type RegisteredDrawer = {
12
+ id: symbol | string
13
+ component: Component
14
+ keepOpenOnRouteChange: boolean
15
+ props: object
16
+ isBusy: ComputedRef<boolean>
17
+ onConfirm: (...args: any[]) => void | Promise<void>
18
+ onCancel: (...args: any[]) => void | Promise<void>
19
+ }
20
+
21
+ export type DrawerHandlerArgs<TProps, THandler extends 'onConfirm' | 'onCancel'> = TProps extends {
22
+ [K in THandler]?: (...args: infer TArgs) => any
23
+ }
24
+ ? TArgs
25
+ : never
26
+
27
+ export abstract class AbstractDrawerResponse<TPayload> {
28
+ constructor(public readonly payload: TPayload) {}
29
+
30
+ abstract get confirmed(): boolean
31
+ abstract get canceled(): boolean
32
+ }
33
+
34
+ export class DrawerConfirmResponse<TPayload> extends AbstractDrawerResponse<TPayload> {
35
+ public readonly confirmed = true
36
+ public readonly canceled = false
37
+ }
38
+
39
+ export class DrawerCancelResponse<TPayload> extends AbstractDrawerResponse<TPayload> {
40
+ public readonly confirmed = false
41
+ public readonly canceled = true
42
+ }
43
+
44
+ export type DrawerResponse<TConfirmPayload, TCancelPayload> =
45
+ | DrawerConfirmResponse<TConfirmPayload>
46
+ | DrawerCancelResponse<TCancelPayload>
47
+
48
+ export const ABORT_DRAWER = Symbol('abort drawer')
49
+
50
+ export type OpenDrawerReturn<TConfirmPayload, TCancelPayload> = Promise<
51
+ DrawerResponse<TConfirmPayload, TCancelPayload>
52
+ > & {
53
+ close: () => void
54
+ }
55
+
56
+ export const IK_DRAWER = Symbol('drawer') as InjectionKey<ComputedRef<RegisteredDrawer>>
57
+
58
+ export type DrawerPropsConfigEntry<TProps> =
59
+ AreAllPropsOptional<TProps> extends true
60
+ ? { props?: DrawerPropsOption<TProps> }
61
+ : { props: DrawerPropsOption<TProps> }
62
+
63
+ export type DrawerConfig<
64
+ TProps,
65
+ TConfirmArgs extends any[],
66
+ TCancelArgs extends any[],
67
+ TConfirmPayload,
68
+ TCancelPayload,
69
+ > = {
70
+ component: Promise<{
71
+ default: abstract new () => {
72
+ $props: TProps
73
+ }
74
+ }>
75
+ onConfirm?: (...args: TConfirmArgs) => MaybePromise<DrawerResponse<TConfirmPayload, any> | TConfirmPayload>
76
+ onCancel?: (...args: TCancelArgs) => MaybePromise<DrawerResponse<TCancelPayload, any> | TCancelPayload>
77
+ keepOpenOnRouteChange?: boolean
78
+ } & DrawerPropsConfigEntry<TProps>
79
+
80
+ export type UseDrawerReturn<TArgs extends any[] = any[], TConfirmPayload = any, TCancelPayload = any> = (
81
+ ...args: TArgs
82
+ ) => OpenDrawerReturn<TConfirmPayload, TCancelPayload>
83
+
84
+ export type OpenDrawer = <
85
+ TProps,
86
+ TConfirmArgs extends DrawerHandlerArgs<TProps, 'onConfirm'>,
87
+ TCancelArgs extends DrawerHandlerArgs<TProps, 'onCancel'>,
88
+ TConfirmPayload = undefined,
89
+ TCancelPayload = undefined,
90
+ >(
91
+ id: string,
92
+ config: DrawerConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
93
+ ) => OpenDrawerReturn<TConfirmPayload, TCancelPayload>
@@ -0,0 +1,53 @@
1
+ import { createDrawerOpener } from '@core/packages/drawer/create-drawer-opener.ts'
2
+ import { type DrawerConfig, type DrawerHandlerArgs, type UseDrawerReturn } from '@core/packages/drawer/types.ts'
3
+
4
+ export function useDrawer(): ReturnType<typeof createDrawerOpener>
5
+
6
+ export function useDrawer<
7
+ TProps,
8
+ TConfirmArgs extends DrawerHandlerArgs<TProps, 'onConfirm'>,
9
+ TCancelArgs extends DrawerHandlerArgs<TProps, 'onCancel'>,
10
+ TConfirmPayload = undefined,
11
+ TCancelPayload = undefined,
12
+ >(
13
+ config: DrawerConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
14
+ ): UseDrawerReturn<[], TConfirmPayload, TCancelPayload>
15
+
16
+ export function useDrawer<
17
+ TConfigBuilderArgs extends any[],
18
+ TProps,
19
+ TConfirmArgs extends DrawerHandlerArgs<TProps, 'onConfirm'>,
20
+ TCancelArgs extends DrawerHandlerArgs<TProps, 'onCancel'>,
21
+ TConfirmPayload = undefined,
22
+ TCancelPayload = undefined,
23
+ >(
24
+ configBuilder: (
25
+ ...args: TConfigBuilderArgs
26
+ ) => DrawerConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
27
+ ): UseDrawerReturn<TConfigBuilderArgs, TConfirmPayload, TCancelPayload>
28
+
29
+ export function useDrawer<
30
+ TConfigBuilderArgs extends any[],
31
+ TProps,
32
+ TConfirmArgs extends DrawerHandlerArgs<TProps, 'onConfirm'>,
33
+ TCancelArgs extends DrawerHandlerArgs<TProps, 'onCancel'>,
34
+ TConfig extends DrawerConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>,
35
+ TConfirmPayload = undefined,
36
+ TCancelPayload = undefined,
37
+ >(
38
+ configOrBuilder?: TConfig | ((...args: TConfigBuilderArgs) => TConfig)
39
+ ): UseDrawerReturn<TConfigBuilderArgs, TConfirmPayload, TCancelPayload> | ReturnType<typeof createDrawerOpener> {
40
+ const openDrawer = createDrawerOpener()
41
+
42
+ if (configOrBuilder === undefined) {
43
+ return openDrawer
44
+ }
45
+
46
+ const id = Symbol('drawer')
47
+
48
+ return (...args: TConfigBuilderArgs) => {
49
+ const config = typeof configOrBuilder === 'function' ? configOrBuilder(...args) : configOrBuilder
50
+
51
+ return openDrawer(id, config)
52
+ }
53
+ }
@@ -0,0 +1,17 @@
1
+ import { createRule, type Maybe } from '@regle/core'
2
+ import { isFilled } from '@regle/rules'
3
+
4
+ const IPV4_OCTET = '(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)'
5
+ const IPV4_CIDR_PREFIX = '(3[0-2]|[12]?\\d)'
6
+ const IP_OR_CIDR_REGEX = new RegExp(`^${IPV4_OCTET}(\\.${IPV4_OCTET}){3}(/${IPV4_CIDR_PREFIX})?$`)
7
+
8
+ export const ipv4OrCidr = createRule({
9
+ validator(value: Maybe<string>) {
10
+ if (!isFilled(value)) {
11
+ return true
12
+ }
13
+
14
+ return IP_OR_CIDR_REGEX.test(value.trim())
15
+ },
16
+ message: 'Invalid IP address',
17
+ })
@@ -1,4 +1,5 @@
1
1
  export * from './custom-rules/out-of-range.rule.ts'
2
+ export * from './custom-rules/ipv4-or-cidr.rule.ts'
2
3
  export * from './merge-validation-configs.ts'
3
4
  export * from './types.ts'
4
5
  export * from './use-form-validation.ts'
@@ -22,16 +22,18 @@ export function createModalOpener() {
22
22
  ids.delete(id)
23
23
  }
24
24
 
25
- watch(
26
- () => route.path,
27
- () => {
28
- ids.forEach(id => {
29
- if (!modalStore.getModal(id)?.keepOpenOnRouteChange) {
30
- closeById(id)
31
- }
32
- })
33
- }
34
- )
25
+ if (route !== undefined) {
26
+ watch(
27
+ () => route.path,
28
+ () => {
29
+ ids.forEach(id => {
30
+ if (!modalStore.getModal(id)?.keepOpenOnRouteChange) {
31
+ closeById(id)
32
+ }
33
+ })
34
+ }
35
+ )
36
+ }
35
37
 
36
38
  return function openModal<
37
39
  TProps,
@@ -9,7 +9,7 @@ import { h, toValue } from 'vue'
9
9
  export type { ActionItem }
10
10
 
11
11
  export const useActionColumn = defineColumn((config: HeaderConfig & Partial<ButtonIconConfig>) => ({
12
- renderHead: () => renderHeadCell(config.headerIcon, config.headerLabel),
12
+ renderHead: () => renderHeadCell(config.headerLabel),
13
13
  renderBody: (params: { onClick: () => void; actions?: ActionItem[] }) =>
14
14
  h(VtsActionCell, {
15
15
  buttonIcon: toValue(config.buttonIcon ?? 'fa:eye'),
@@ -1,4 +1,4 @@
1
1
  import { useCollapsedListColumn } from '@core/tables/column-definitions/collapsed-list-column.ts'
2
2
  import type { HeaderConfig } from '@core/tables/types.ts'
3
3
 
4
- export const useAddressColumn = (config?: HeaderConfig) => useCollapsedListColumn({ headerIcon: 'fa:at', ...config })
4
+ export const useAddressColumn = (config?: HeaderConfig) => useCollapsedListColumn(config)
@@ -17,7 +17,7 @@ type ButtonConfig = {
17
17
  }
18
18
 
19
19
  export const useButtonColumn = defineColumn((config?: HeaderConfig & ButtonConfig) => ({
20
- renderHead: () => renderHeadCell(config?.headerIcon, config?.headerLabel),
20
+ renderHead: () => renderHeadCell(config?.headerLabel),
21
21
  renderBody: (label: string, onClick: () => void, props?: Partial<ButtonProps>) =>
22
22
  renderBodyCell(() =>
23
23
  h(
@@ -16,7 +16,7 @@ export type ButtonIconConfig = {
16
16
  }
17
17
 
18
18
  export const useButtonIconColumn = defineColumn((config: HeaderConfig & ButtonIconConfig) => ({
19
- renderHead: () => renderHeadCell(config.headerIcon, config.headerLabel),
19
+ renderHead: () => renderHeadCell(config.headerLabel),
20
20
  renderBody: (onClick: () => void) =>
21
21
  h(UiTableCell, { align: 'center', style: 'width: 6rem' }, () =>
22
22
  h(UiButtonIcon, {
@@ -7,6 +7,6 @@ import { toArray } from '@core/utils/to-array.utils.ts'
7
7
  import { h } from 'vue'
8
8
 
9
9
  export const useCollapsedListColumn = defineColumn((config?: HeaderConfig) => ({
10
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:square-caret-down', config?.headerLabel),
10
+ renderHead: () => renderHeadCell(config?.headerLabel),
11
11
  renderBody: (items: MaybeArray<string>) => h(VtsCollapsedListCell, { items: toArray(items) }),
12
12
  }))
@@ -13,7 +13,7 @@ export const useDateColumn = defineColumn((config?: HeaderConfig & DateConfig) =
13
13
  const { d } = useI18n()
14
14
 
15
15
  return {
16
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:calendar', config?.headerLabel),
16
+ renderHead: () => renderHeadCell(config?.headerLabel),
17
17
  renderBody: (date?: DateLike, options?: { relative?: boolean }) => {
18
18
  return renderBodyCell(() => {
19
19
  if (date === undefined) {
@@ -5,7 +5,7 @@ import type { HeaderConfig } from '@core/tables/types.ts'
5
5
  import { h } from 'vue'
6
6
 
7
7
  export const useDoubleLinkColumn = defineColumn((config?: HeaderConfig) => ({
8
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:a', config?.headerLabel),
8
+ renderHead: () => renderHeadCell(config?.headerLabel),
9
9
  renderBody: (link: { label: string } & VtsDoubleLinkCellProps) => {
10
10
  const { label, ...doubleLinkCellProps } = link
11
11
 
@@ -7,6 +7,6 @@ import type { HeaderConfig } from '@core/tables/types.ts'
7
7
  import { h } from 'vue'
8
8
 
9
9
  export const useInfoColumn = defineColumn((config?: HeaderConfig) => ({
10
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:square-caret-down', config?.headerLabel),
10
+ renderHead: () => renderHeadCell(config?.headerLabel),
11
11
  renderBody: (label: string, accent: InfoAccent) => renderBodyCell(() => h(UiInfo, { accent }, () => label)),
12
12
  }))
@@ -11,11 +11,7 @@ type InputConfig = {
11
11
  }
12
12
 
13
13
  export const useInputColumn = defineColumn((config?: HeaderConfig & InputConfig) => ({
14
- renderHead: () =>
15
- renderHeadCell(
16
- config?.headerIcon ?? (config?.type === 'number' ? 'fa:hashtag' : 'fa:align-left'),
17
- config?.headerLabel
18
- ),
14
+ renderHead: () => renderHeadCell(config?.headerLabel),
19
15
  renderBody: (model: Ref<string | number | undefined>, inputProps?: { disabled?: boolean }) =>
20
16
  renderBodyCell(() =>
21
17
  h(UiInput, {
@@ -5,7 +5,7 @@ import type { HeaderConfig } from '@core/tables/types.ts'
5
5
  import { h } from 'vue'
6
6
 
7
7
  export const useLinkColumn = defineColumn((config?: HeaderConfig) => ({
8
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:a', config?.headerLabel),
8
+ renderHead: () => renderHeadCell(config?.headerLabel),
9
9
  renderBody: (link: { label: string } & VtsLinkCellProps) => {
10
10
  const { label, ...linkCellProps } = link
11
11
 
@@ -6,7 +6,7 @@ import type { HeaderConfig } from '@core/tables/types.ts'
6
6
  import { h } from 'vue'
7
7
 
8
8
  export const useLinkOrTextColumn = defineColumn((config?: HeaderConfig) => ({
9
- renderHead: () => renderHeadCell(config?.headerIcon, config?.headerLabel),
9
+ renderHead: () => renderHeadCell(config?.headerLabel),
10
10
  renderBody: (link: { label: string; suffix?: string } & VtsLinkCellProps) => {
11
11
  const { label, suffix, ...linkCellProps } = link
12
12
 
@@ -4,6 +4,6 @@ import { renderHeadCell } from '@core/tables/helpers/render-head-cell'
4
4
  import type { HeaderConfig } from '@core/tables/types.ts'
5
5
 
6
6
  export const useLiteralColumn = defineColumn((config?: HeaderConfig) => ({
7
- renderHead: () => renderHeadCell(config?.headerIcon ?? 'fa:square-caret-down', config?.headerLabel),
7
+ renderHead: () => renderHeadCell(config?.headerLabel),
8
8
  renderBody: (value: any) => renderBodyCell(() => value),
9
9
  }))