@xen-orchestra/web-core 0.54.0 → 0.56.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 (107) hide show
  1. package/lib/components/button-group/VtsButtonGroup.vue +11 -1
  2. package/lib/components/card-object-title/VtsCardObjectTitle.vue +36 -0
  3. package/lib/components/code-snippet/VtsCodeSnippet.vue +11 -5
  4. package/lib/components/drawer/VtsDrawer.vue +89 -0
  5. package/lib/components/drawer/VtsDrawerButton.vue +22 -0
  6. package/lib/components/drawer/VtsDrawerCancelButton.vue +33 -0
  7. package/lib/components/drawer/VtsDrawerConfirmButton.vue +32 -0
  8. package/lib/components/drawer/VtsDrawerList.vue +34 -0
  9. package/lib/components/icon/VtsIcon.vue +3 -1
  10. package/lib/components/layout/VtsContentSidePanel.vue +41 -0
  11. package/lib/components/layout/VtsLayoutSidebar.vue +5 -2
  12. package/lib/components/menu/VtsActionsMenu.vue +37 -3
  13. package/lib/components/modal/VtsModalButton.vue +0 -1
  14. package/lib/components/operation-card/VtsOperationCard.vue +2 -1
  15. package/lib/components/panel/VtsPanel.vue +80 -0
  16. package/lib/components/panel/VtsSidePanel.vue +140 -0
  17. package/lib/components/query-builder/VtsQueryBuilderModal.vue +0 -1
  18. package/lib/components/select/VtsSelect.vue +1 -0
  19. package/lib/components/state-hero/VtsStateHero.vue +26 -24
  20. package/lib/components/table/VtsTable.vue +2 -1
  21. package/lib/components/table/cells/VtsActionCell.vue +7 -0
  22. package/lib/components/table/cells/VtsHeaderCell.vue +1 -17
  23. package/lib/components/table/cells/VtsLinkCell.vue +3 -1
  24. package/lib/components/table/cells/VtsTagCell.vue +4 -2
  25. package/lib/components/tree/VtsTreeItem.vue +9 -3
  26. package/lib/components/tree/VtsTreeList.vue +6 -3
  27. package/lib/components/tree/VtsTreeLoadingItem.vue +3 -3
  28. package/lib/components/ui/drawer/UiDrawer.vue +123 -0
  29. package/lib/components/ui/head-bar/UiHeadBar.vue +1 -1
  30. package/lib/components/ui/input/UiInput.vue +3 -1
  31. package/lib/components/ui/link/UiLink.vue +3 -1
  32. package/lib/components/ui/modal/UiModal.vue +10 -18
  33. package/lib/components/ui/panel/UiPanel.vue +7 -24
  34. package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +1 -1
  35. package/lib/composables/default-tab.composable.ts +4 -0
  36. package/lib/composables/table-state.composable.ts +1 -1
  37. package/lib/icons/action-icons.ts +20 -2
  38. package/lib/icons/custom-icons.ts +16 -0
  39. package/lib/icons/object-icons.ts +33 -18
  40. package/lib/locales/cs.json +110 -11
  41. package/lib/locales/da.json +1 -3
  42. package/lib/locales/de.json +1 -12
  43. package/lib/locales/en.json +76 -12
  44. package/lib/locales/es.json +15 -6
  45. package/lib/locales/fa.json +0 -1
  46. package/lib/locales/fr.json +76 -12
  47. package/lib/locales/it.json +0 -1
  48. package/lib/locales/ko.json +0 -2
  49. package/lib/locales/nb-NO.json +18 -20
  50. package/lib/locales/nl.json +0 -11
  51. package/lib/locales/pt-BR.json +0 -4
  52. package/lib/locales/pt.json +0 -4
  53. package/lib/locales/ru.json +0 -1
  54. package/lib/locales/sk.json +110 -11
  55. package/lib/locales/sv.json +2 -13
  56. package/lib/locales/uk.json +0 -1
  57. package/lib/locales/zh-Hans.json +79 -16
  58. package/lib/packages/drawer/DrawerProvider.vue +17 -0
  59. package/lib/packages/drawer/README.md +189 -0
  60. package/lib/packages/drawer/create-drawer-opener.ts +119 -0
  61. package/lib/packages/drawer/drawer.store.ts +22 -0
  62. package/lib/packages/drawer/types.ts +93 -0
  63. package/lib/packages/drawer/use-drawer.ts +53 -0
  64. package/lib/packages/modal/create-modal-opener.ts +12 -10
  65. package/lib/packages/remote-resource/define-remote-resource.ts +64 -29
  66. package/lib/packages/tree/types.ts +6 -0
  67. package/lib/packages/tree/use-tree.ts +38 -37
  68. package/lib/stores/panel.store.ts +56 -5
  69. package/lib/tables/column-definitions/action-column.ts +22 -10
  70. package/lib/tables/column-definitions/address-column.ts +1 -1
  71. package/lib/tables/column-definitions/button-column.ts +1 -1
  72. package/lib/tables/column-definitions/button-icon-column.ts +1 -1
  73. package/lib/tables/column-definitions/collapsed-list-column.ts +1 -1
  74. package/lib/tables/column-definitions/date-column.ts +1 -1
  75. package/lib/tables/column-definitions/double-link-column.ts +1 -1
  76. package/lib/tables/column-definitions/info-column.ts +1 -1
  77. package/lib/tables/column-definitions/input-column.ts +1 -5
  78. package/lib/tables/column-definitions/link-column.ts +1 -1
  79. package/lib/tables/column-definitions/link-or-text-column.ts +1 -1
  80. package/lib/tables/column-definitions/literal-column.ts +1 -1
  81. package/lib/tables/column-definitions/number-column.ts +1 -1
  82. package/lib/tables/column-definitions/percent-column.ts +1 -1
  83. package/lib/tables/column-definitions/progress-bar-column.ts +1 -1
  84. package/lib/tables/column-definitions/select-column.ts +1 -1
  85. package/lib/tables/column-definitions/status-column.ts +2 -2
  86. package/lib/tables/column-definitions/tag-column.ts +3 -2
  87. package/lib/tables/column-definitions/text-column.ts +1 -1
  88. package/lib/tables/column-definitions/truncated-text-column.ts +1 -1
  89. package/lib/tables/column-sets/backup-job-schedule-columns.ts +2 -2
  90. package/lib/tables/column-sets/backup-log-columns.ts +1 -1
  91. package/lib/tables/column-sets/new-vm-network-columns.ts +0 -2
  92. package/lib/tables/column-sets/new-vm-sr-columns.ts +0 -1
  93. package/lib/tables/column-sets/server-columns.ts +1 -1
  94. package/lib/tables/column-sets/snapshot-columns.ts +1 -1
  95. package/lib/tables/column-sets/sr-columns.ts +2 -2
  96. package/lib/tables/column-sets/traffic-rules-columns.ts +5 -7
  97. package/lib/tables/column-sets/vif-network-columns.ts +24 -0
  98. package/lib/tables/column-sets/vm-columns.ts +2 -3
  99. package/lib/tables/helpers/render-head-cell.ts +2 -4
  100. package/lib/tables/types.ts +0 -2
  101. package/lib/types/connection.ts +14 -0
  102. package/lib/types/state-hero.type.ts +19 -0
  103. package/lib/types/storage-repository.type.ts +14 -0
  104. package/lib/types/vue-virtual-scroller.d.ts +8 -2
  105. package/lib/utils/injection-keys.util.ts +1 -1
  106. package/lib/utils/sr.utils.ts +34 -0
  107. package/package.json +4 -4
@@ -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
+ }
@@ -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,
@@ -14,19 +14,19 @@ import {
14
14
  computed,
15
15
  type ComputedRef,
16
16
  type EffectScope,
17
+ effectScope,
17
18
  getCurrentScope,
19
+ type MaybeRef,
18
20
  type MaybeRefOrGetter,
19
21
  onScopeDispose,
20
22
  reactive,
21
23
  type Ref,
22
24
  ref,
23
25
  shallowRef,
24
- triggerRef,
25
26
  toRef,
26
27
  toValue,
28
+ triggerRef,
27
29
  watch,
28
- effectScope,
29
- type MaybeRef,
30
30
  } from 'vue'
31
31
 
32
32
  const DEFAULT_CACHE_EXPIRATION_MS = 10_000
@@ -41,7 +41,12 @@ export function defineRemoteResource<
41
41
  url: string | ((...args: TArgs) => string)
42
42
  initialData: () => TData
43
43
  state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
44
- onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
44
+ onDataReceived?: (
45
+ data: Ref<NoInfer<TData>>,
46
+ receivedData: any,
47
+ calledFrom: 'execute' | 'update',
48
+ context?: ResourceContext<TArgs>
49
+ ) => void
45
50
  cacheExpirationMs?: number | false
46
51
  pollingIntervalMs?: number | false
47
52
  stream?: boolean
@@ -50,7 +55,12 @@ export function defineRemoteResource<
50
55
  export function defineRemoteResource<TData, TState extends object, TArgs extends any[] = []>(config: {
51
56
  url: string | ((...args: TArgs) => string)
52
57
  state?: (data: Ref<TData | undefined>, context: ResourceContext<TArgs>) => TState
53
- onDataReceived?: (data: Ref<TData | undefined>, receivedData: any) => void
58
+ onDataReceived?: (
59
+ data: Ref<TData | undefined>,
60
+ receivedData: any,
61
+ calledFrom: 'execute' | 'update',
62
+ context?: ResourceContext<TArgs>
63
+ ) => void
54
64
  cacheExpirationMs?: number | false
55
65
  pollingIntervalMs?: number | false
56
66
  stream?: boolean
@@ -64,7 +74,12 @@ export function defineRemoteResource<
64
74
  url: string | ((...args: TArgs) => string)
65
75
  initialData: () => TData
66
76
  state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
67
- onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
77
+ onDataReceived?: (
78
+ data: Ref<NoInfer<TData>>,
79
+ receivedData: any,
80
+ calledFrom: 'execute' | 'update',
81
+ context?: ResourceContext<TArgs>
82
+ ) => void
68
83
  onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
69
84
  stream?: boolean
70
85
  initWatchCollection: () => {
@@ -86,7 +101,12 @@ export function defineRemoteResource<
86
101
  url: string | ((...args: TArgs) => string)
87
102
  initialData?: () => TData
88
103
  state?: (data: Ref<TData>, context: ResourceContext<TArgs>) => TState
89
- onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
104
+ onDataReceived?: (
105
+ data: Ref<NoInfer<TData>>,
106
+ receivedData: any,
107
+ calledFrom: 'execute' | 'update',
108
+ context?: ResourceContext<TArgs>
109
+ ) => void
90
110
  onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
91
111
  cacheExpirationMs?: number | false
92
112
  pollingIntervalMs?: number | false
@@ -161,33 +181,38 @@ export function defineRemoteResource<
161
181
 
162
182
  const onDataReceived =
163
183
  config.onDataReceived ??
164
- ((data: Ref<TData>, receivedData: any, context?: ResourceContext<TArgs>) => {
184
+ ((data: Ref<TData>, receivedData: any, calledFrom: 'execute' | 'update', context?: ResourceContext<TArgs>) => {
165
185
  // allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
166
186
  if (watchCollection?.predicate?.(receivedData, context) === false) {
167
187
  return
168
188
  }
169
189
 
170
- if (data.value === undefined || (Array.isArray(data.value) && Array.isArray(receivedData))) {
171
- data.value = receivedData
172
-
173
- if (watchCollection !== undefined && Array.isArray(data.value)) {
174
- handleBuffer(data as Ref<TData[]>)
175
- isBufferEventsProcessed = true
190
+ if (!Array.isArray(data.value)) {
191
+ if (data.value === undefined) {
192
+ data.value = receivedData
193
+ } else {
194
+ merge(data.value, receivedData)
176
195
  }
196
+
177
197
  return
178
198
  }
179
199
 
180
- if (Array.isArray(data.value)) {
181
- if (!isBufferEventsProcessed) {
182
- bufferedEvents.push(['update', receivedData])
183
- } else {
184
- removeData(data.value, receivedData)
185
- data.value.push(receivedData)
186
- }
200
+ const store = data.value as TData[]
201
+
202
+ if (Array.isArray(receivedData)) {
203
+ data.value = receivedData as TData
204
+
187
205
  return
188
206
  }
189
207
 
190
- merge(data.value, receivedData)
208
+ if (calledFrom === 'execute') {
209
+ store.push(receivedData)
210
+ } else if (!isBufferEventsProcessed) {
211
+ bufferedEvents.push(['update', receivedData])
212
+ } else {
213
+ removeData(store, receivedData)
214
+ store.push(receivedData)
215
+ }
191
216
  })
192
217
 
193
218
  const onDataRemoved =
@@ -292,13 +317,23 @@ export function defineRemoteResource<
292
317
  }
293
318
 
294
319
  if (config.stream) {
320
+ if (Array.isArray(data.value)) {
321
+ data.value = [] as TData
322
+ }
323
+
295
324
  for await (const event of readNDJSONStream(response.body)) {
296
- onDataReceived(data, event)
297
- flushData()
325
+ onDataReceived(data, event, 'execute', context)
326
+ void flushData()
327
+ }
328
+
329
+ if (watchCollection !== undefined && Array.isArray(data.value)) {
330
+ handleBuffer(data as Ref<TData[]>)
331
+ isBufferEventsProcessed = true
332
+ void flushData()
298
333
  }
299
334
  } else {
300
- onDataReceived(data, await response.json())
301
- flushData()
335
+ onDataReceived(data, await response.json(), 'execute', context)
336
+ void flushData()
302
337
  }
303
338
 
304
339
  isReady.value = true
@@ -324,12 +359,12 @@ export function defineRemoteResource<
324
359
  handlePost,
325
360
  resource,
326
361
  onDataReceived: receivedData => {
327
- onDataReceived(data, receivedData, context)
328
- flushData()
362
+ onDataReceived(data, receivedData, 'update', context)
363
+ void flushData()
329
364
  },
330
365
  onDataRemoved: receivedData => {
331
366
  onDataRemoved(data, receivedData, context)
332
- flushData()
367
+ void flushData()
333
368
  },
334
369
  })
335
370
  await execute()
@@ -24,6 +24,12 @@ export type TreeNode<
24
24
  TDiscriminator = any,
25
25
  > = Leaf<TData, TDiscriminator> | Branch<TData, TChildNode, TDiscriminator>
26
26
 
27
+ export type FlatTreeNode<TNode extends TreeNode = TreeNode> = {
28
+ id: TreeNodeId
29
+ node: TNode
30
+ depth: number
31
+ }
32
+
27
33
  export type BaseTreeNodeOptions<TData extends object, TDiscriminator> = {
28
34
  discriminator?: TDiscriminator
29
35
  predicate?: (node: TreeNodeBase<TData, TDiscriminator>) => boolean | undefined
@@ -1,14 +1,14 @@
1
1
  import { buildNodes } from '@core/packages/tree/build-nodes'
2
2
  import type {
3
3
  DefinitionToTreeNode,
4
+ FlatTreeNode,
4
5
  TreeContext,
5
6
  TreeNode,
6
7
  TreeNodeDefinition,
7
8
  TreeNodeId,
8
9
  UseTreeOptions,
9
10
  } from '@core/packages/tree/types'
10
- import { useTimeoutFn } from '@vueuse/core'
11
- import { computed, type MaybeRefOrGetter, nextTick, reactive, ref, toValue } from 'vue'
11
+ import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
12
12
 
13
13
  export function useTree<
14
14
  TDefinition extends TreeNodeDefinition,
@@ -53,6 +53,32 @@ export function useTree<
53
53
 
54
54
  const visibleNodes = computed(() => nodes.value.filter(node => !node.isExcluded))
55
55
 
56
+ const flatNodes = computed<FlatTreeNode[]>(() => {
57
+ const result: FlatTreeNode[] = []
58
+
59
+ const flatten = (list: TreeNode[]) => {
60
+ for (const node of list) {
61
+ result.push({ id: node.id, node, depth: node.depth })
62
+ if (node.isBranch && !node.isCollapsed) {
63
+ flatten(node.children)
64
+ }
65
+ }
66
+ }
67
+
68
+ flatten(visibleNodes.value)
69
+
70
+ return result
71
+ })
72
+
73
+ const flatNodeIndexById = computed(() => {
74
+ const index = new Map<TreeNodeId, number>()
75
+ const list = flatNodes.value
76
+ for (let i = 0; i < list.length; i++) {
77
+ index.set(list[i].id, i)
78
+ }
79
+ return index
80
+ })
81
+
56
82
  const getNode = (id: TreeNodeId | undefined) => (id !== undefined ? nodesMap.value.get(id) : undefined)
57
83
  const getNodes = (ids: TreeNodeId[]) => ids.map(getNode).filter(node => node !== undefined) as TreeNode[]
58
84
 
@@ -80,46 +106,19 @@ export function useTree<
80
106
  return undefined
81
107
  }
82
108
 
83
- const scrollToNodeElement = async (id: string | number, options?: ScrollIntoViewOptions) => {
84
- const node = findNodeByObjectId(nodes.value, id)
109
+ const expandToNode = (objectId: string | number): TreeNode | undefined => {
110
+ const node = findNodeByObjectId(nodes.value, objectId)
85
111
  if (!node) {
86
- if (id) {
87
- useTimeoutFn(async () => {
88
- await nextTick()
89
- await scrollToNodeElement(id, options)
90
- }, 200)
91
- }
92
- return
112
+ return undefined
93
113
  }
94
114
 
95
- getHierarchy(node).forEach(node => {
96
- if (node.isBranch) {
97
- node.toggleCollapse(false)
115
+ getHierarchy(node).forEach(ancestor => {
116
+ if (ancestor.isBranch) {
117
+ ancestor.toggleCollapse(false)
98
118
  }
99
119
  })
100
120
 
101
- const nodeElement = document.querySelector<HTMLElement>(`[data-node-id="${id}"]`)
102
-
103
- if (!nodeElement) {
104
- useTimeoutFn(async () => {
105
- await nextTick()
106
- await scrollToNodeElement(id, options)
107
- }, 200)
108
- return
109
- }
110
-
111
- const hasChildren = node.isBranch && node.hasChildren
112
- const cfg: ScrollIntoViewOptions = options ?? { block: hasChildren ? 'start' : 'center', behavior: 'smooth' }
113
-
114
- if (hasChildren) {
115
- nodeElement.style.scrollMarginTop = '0.8rem'
116
-
117
- useTimeoutFn(async () => {
118
- nodeElement.style.scrollMarginTop = ''
119
- }, 1000)
120
- }
121
-
122
- nodeElement.scrollIntoView(cfg)
121
+ return node
123
122
  }
124
123
 
125
124
  const selectedNodes = computed(() => getNodes(Array.from(selectedIds.value.values())))
@@ -140,6 +139,8 @@ export function useTree<
140
139
 
141
140
  return {
142
141
  nodes: visibleNodes,
142
+ flatNodes,
143
+ flatNodeIndexById,
143
144
  activeId,
144
145
  activeNode,
145
146
  selectedIds,
@@ -148,6 +149,6 @@ export function useTree<
148
149
  collapsedIds,
149
150
  expandedIds,
150
151
  options,
151
- scrollToNodeElement,
152
+ expandToNode,
152
153
  }
153
154
  }
@@ -1,12 +1,63 @@
1
1
  import { useUiStore } from '@core/stores/ui.store'
2
+ import { useLocalStorage } from '@vueuse/core'
2
3
  import { defineStore } from 'pinia'
3
- import { computed, ref } from 'vue'
4
+ import { computed, watch } from 'vue'
4
5
 
5
6
  export const usePanelStore = defineStore('panel', () => {
6
7
  const uiStore = useUiStore()
7
- const isExpanded = ref(false)
8
- const open = () => (isExpanded.value = true)
9
- const close = () => (isExpanded.value = false)
8
+ const isExpanded = useLocalStorage('panel.expanded', true)
9
+ const isLocked = useLocalStorage('panel.locked', true)
10
10
 
11
- return { open, close, isExpanded: computed(() => !uiStore.isSmall || isExpanded.value) }
11
+ const cssHorizontalOffset = computed(() => (isExpanded.value ? 0 : '100%'))
12
+ /* Panel closes automatically when it has no content (true on small UI or when not locked) */
13
+ const syncsOpenStateWithSelection = computed(() => uiStore.isSmall || !isLocked.value)
14
+
15
+ function expand() {
16
+ isExpanded.value = true
17
+ }
18
+
19
+ function collapse() {
20
+ isExpanded.value = false
21
+ }
22
+
23
+ function syncWithSelection(hasSelection: boolean) {
24
+ if (!syncsOpenStateWithSelection.value) {
25
+ return
26
+ }
27
+ if (!hasSelection && isExpanded.value) {
28
+ collapse()
29
+ } else if (hasSelection && !isExpanded.value) {
30
+ expand()
31
+ }
32
+ }
33
+
34
+ function toggleLock() {
35
+ const next = !isLocked.value
36
+ isLocked.value = next
37
+ if (next && !uiStore.isSmall && !isExpanded.value) {
38
+ expand()
39
+ }
40
+ }
41
+
42
+ watch(
43
+ () => uiStore.isSmall,
44
+ isSmall => {
45
+ /* Expand when coming back from small to large UI, if locked and not expanded */
46
+ if (!isSmall && isLocked.value && !isExpanded.value) {
47
+ expand()
48
+ }
49
+ },
50
+ { immediate: true }
51
+ )
52
+
53
+ return {
54
+ isExpanded,
55
+ isLocked,
56
+ syncsOpenStateWithSelection,
57
+ expand,
58
+ collapse,
59
+ toggleLock,
60
+ syncWithSelection,
61
+ cssHorizontalOffset,
62
+ }
12
63
  })