@xen-orchestra/web-core 0.55.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.
- package/lib/assets/css/base.pcss +5 -0
- package/lib/components/card-object-title/VtsCardObjectTitle.vue +36 -0
- package/lib/components/code-snippet/VtsCodeSnippet.vue +11 -5
- package/lib/components/drawer/VtsDrawer.vue +5 -1
- package/lib/components/drawer/VtsDrawerButton.vue +4 -3
- package/lib/components/drawer/VtsDrawerConfirmButton.vue +4 -2
- package/lib/components/icon/VtsIcon.vue +3 -1
- package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
- package/lib/components/layout/VtsContentSidePanel.vue +41 -0
- package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
- package/lib/components/menu/MenuItem.vue +9 -1
- package/lib/components/menu/MenuList.vue +4 -2
- package/lib/components/menu/VtsActionsMenu.vue +48 -14
- package/lib/components/modal/VtsActionModal.vue +44 -9
- package/lib/components/modal/VtsBlockedModal.vue +1 -1
- package/lib/components/modal/VtsDeleteModal.vue +1 -1
- package/lib/components/operation-card/VtsOperationCard.vue +2 -1
- package/lib/components/overlay/VtsOverlayButton.vue +35 -0
- package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
- package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
- package/lib/components/overlay/VtsOverlayList.vue +82 -0
- package/lib/components/panel/VtsPanel.vue +80 -0
- package/lib/components/panel/VtsSidePanel.vue +140 -0
- package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
- package/lib/components/select/VtsSelect.vue +1 -3
- package/lib/components/space-card/VtsSpaceCard.vue +1 -1
- package/lib/components/state-hero/VtsStateHero.vue +9 -17
- package/lib/components/table/VtsTable.vue +2 -1
- package/lib/components/table/cells/VtsActionCell.vue +7 -0
- package/lib/components/table/cells/VtsLinkCell.vue +3 -1
- package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
- package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
- package/lib/components/task/VtsQuickTaskButton.vue +1 -1
- package/lib/components/task/VtsQuickTaskList.vue +1 -1
- package/lib/components/tree/VtsTreeItem.vue +13 -7
- package/lib/components/tree/VtsTreeList.vue +6 -3
- package/lib/components/tree/VtsTreeLoadingItem.vue +3 -3
- package/lib/components/ui/head-bar/UiHeadBar.vue +2 -2
- package/lib/components/ui/input/UiInput.vue +56 -10
- package/lib/components/ui/link/UiLink.vue +3 -1
- package/lib/components/ui/panel/UiPanel.vue +7 -24
- package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
- package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
- package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
- package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
- package/lib/components/ui/task-list/UiTaskList.vue +1 -1
- package/lib/components/ui/text-area/UiTextarea.vue +12 -14
- package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +1 -1
- package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
- package/lib/composables/table/multi-select.composable.md +33 -0
- package/lib/composables/table-state.composable.ts +1 -1
- package/lib/icons/action-icons.ts +14 -0
- package/lib/layouts/CoreLayout.vue +74 -62
- package/lib/locales/cs.json +56 -1
- package/lib/locales/de.json +3 -2
- package/lib/locales/en.json +85 -4
- package/lib/locales/es.json +11 -1
- package/lib/locales/fa.json +0 -1
- package/lib/locales/fr.json +85 -4
- package/lib/locales/it.json +0 -1
- package/lib/locales/ko.json +2 -0
- package/lib/locales/nl.json +2 -1
- package/lib/locales/pt-BR.json +0 -1
- package/lib/locales/pt.json +0 -1
- package/lib/locales/ru.json +0 -1
- package/lib/locales/sk.json +56 -1
- package/lib/locales/sv.json +3 -2
- package/lib/locales/uk.json +0 -1
- package/lib/locales/zh-Hans.json +79 -4
- package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
- package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
- package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
- package/lib/packages/form-validation/index.ts +2 -0
- package/lib/packages/hide-permanently/README.md +60 -0
- package/lib/packages/hide-permanently/types.ts +3 -0
- package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
- package/lib/packages/overlay/OverlayComponent.vue +18 -0
- package/lib/packages/overlay/README.md +285 -0
- package/lib/packages/overlay/create-event-handler.ts +80 -0
- package/lib/packages/overlay/injection-keys.ts +3 -0
- package/lib/packages/overlay/is-thenable.ts +3 -0
- package/lib/packages/overlay/symbols.ts +5 -0
- package/lib/packages/overlay/types.ts +94 -0
- package/lib/packages/overlay/use-overlay-escape.ts +34 -0
- package/lib/packages/overlay/use-overlay-store.ts +33 -0
- package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
- package/lib/packages/overlay/use-overlay.ts +99 -0
- package/lib/packages/progress/use-progress.ts +12 -2
- package/lib/packages/remote-resource/define-remote-resource.ts +64 -29
- package/lib/packages/sidebar/index.ts +2 -0
- package/lib/packages/sidebar/sidebar.store.ts +55 -0
- package/lib/packages/sidebar/types.ts +21 -0
- package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
- package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
- package/lib/packages/tree/types.ts +6 -0
- package/lib/packages/tree/use-tree.ts +38 -37
- package/lib/stores/panel.store.ts +56 -5
- package/lib/tables/column-definitions/action-column.ts +21 -9
- package/lib/tables/column-definitions/user-name-column.ts +10 -0
- package/lib/tables/column-sets/server-columns.ts +2 -2
- package/lib/tables/column-sets/user-columns.ts +16 -0
- package/lib/tables/column-sets/vm-columns.ts +2 -3
- package/lib/types/object.type.ts +9 -1
- package/lib/types/state-hero.type.ts +19 -0
- package/lib/types/task.type.ts +5 -12
- package/lib/types/vue-virtual-scroller.d.ts +8 -2
- package/lib/utils/injection-keys.util.ts +4 -2
- package/lib/utils/ip-address.utils.ts +7 -0
- package/lib/utils/sr.utils.ts +4 -0
- package/package.json +2 -1
- package/lib/stores/sidebar.store.ts +0 -79
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createEventHandler } from '@core/packages/overlay/create-event-handler.ts'
|
|
2
|
+
import { OVERLAY_ABORT_EVENT } from '@core/packages/overlay/symbols.ts'
|
|
3
|
+
import type {
|
|
4
|
+
ExtractOverlayResponse,
|
|
5
|
+
ForbidExtraEvents,
|
|
6
|
+
MaybeOptionalArgs,
|
|
7
|
+
MaybeOptionalKey,
|
|
8
|
+
OpenEvents,
|
|
9
|
+
Overlay,
|
|
10
|
+
OverlayEventHandler,
|
|
11
|
+
OverlayEvents,
|
|
12
|
+
Prettify,
|
|
13
|
+
} from '@core/packages/overlay/types.ts'
|
|
14
|
+
import { useOverlayStore } from '@core/packages/overlay/use-overlay-store.ts'
|
|
15
|
+
import { reactiveComputed } from '@vueuse/core'
|
|
16
|
+
import { defineAsyncComponent, markRaw, onScopeDispose, reactive, type AsyncComponentLoader, type Component } from 'vue'
|
|
17
|
+
import type { ComponentProps } from 'vue-component-type-helpers'
|
|
18
|
+
|
|
19
|
+
export function useOverlay<TComponent extends Component, const TEvents extends OverlayEvents<TComponent>>({
|
|
20
|
+
component: componentLoader,
|
|
21
|
+
events,
|
|
22
|
+
}: {
|
|
23
|
+
component: AsyncComponentLoader<TComponent>
|
|
24
|
+
events: TEvents & NoInfer<ForbidExtraEvents<TComponent, TEvents>>
|
|
25
|
+
}) {
|
|
26
|
+
const overlayStore = useOverlayStore()
|
|
27
|
+
|
|
28
|
+
const key = Symbol('new-overlay')
|
|
29
|
+
|
|
30
|
+
const component = defineAsyncComponent(componentLoader)
|
|
31
|
+
|
|
32
|
+
let abortCurrent: (() => void) | undefined
|
|
33
|
+
|
|
34
|
+
onScopeDispose(() => abortCurrent?.(), true)
|
|
35
|
+
|
|
36
|
+
function open<TOpenEvents extends OpenEvents<TComponent, TEvents>>(
|
|
37
|
+
...args: MaybeOptionalArgs<
|
|
38
|
+
Prettify<
|
|
39
|
+
{ events?: TOpenEvents & NoInfer<ForbidExtraEvents<TComponent, TOpenEvents>> } & MaybeOptionalKey<
|
|
40
|
+
'props',
|
|
41
|
+
ComponentProps<TComponent>
|
|
42
|
+
>
|
|
43
|
+
>
|
|
44
|
+
>
|
|
45
|
+
) {
|
|
46
|
+
const { events: openEvents, props } = args[0] ?? {}
|
|
47
|
+
|
|
48
|
+
// Reopening while the previous overlay is still open replaces it
|
|
49
|
+
abortCurrent?.()
|
|
50
|
+
|
|
51
|
+
return new Promise<ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>>(resolve => {
|
|
52
|
+
const definitionEvents = events as Record<string, true | OverlayEventHandler>
|
|
53
|
+
const perOpenEvents = (openEvents ?? {}) as Record<string, OverlayEventHandler>
|
|
54
|
+
|
|
55
|
+
// A new object per `open()` call, so that a pending async handler of a
|
|
56
|
+
// replaced overlay cannot interfere with the one currently displayed
|
|
57
|
+
const overlay = reactive<Overlay>({
|
|
58
|
+
key,
|
|
59
|
+
component: markRaw(component),
|
|
60
|
+
props: {},
|
|
61
|
+
status: 'idle',
|
|
62
|
+
lastTrigger: undefined,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
function settle(response: ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>) {
|
|
66
|
+
overlay.status = 'settled'
|
|
67
|
+
abortCurrent = undefined
|
|
68
|
+
resolve(response)
|
|
69
|
+
overlayStore.unregister(overlay)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
abortCurrent = () => settle({ event: OVERLAY_ABORT_EVENT, payload: undefined })
|
|
73
|
+
|
|
74
|
+
const handleEvent = createEventHandler({
|
|
75
|
+
overlay,
|
|
76
|
+
definitionEvents,
|
|
77
|
+
openEvents: perOpenEvents,
|
|
78
|
+
settle: response => settle(response as ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Merge all props + events. If an event is provided in both, the `events`
|
|
82
|
+
// handler runs first, then the `openEvents` one, if needed.
|
|
83
|
+
const eventProps: Record<string, OverlayEventHandler> = {}
|
|
84
|
+
|
|
85
|
+
const eventNames = new Set([...Object.keys(definitionEvents), ...Object.keys(perOpenEvents)])
|
|
86
|
+
|
|
87
|
+
eventNames.forEach(eventName => {
|
|
88
|
+
eventProps[eventName] = (...emitArgs: unknown[]) => handleEvent(eventName, emitArgs)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// `reactiveComputed` keeps the merged props live when a reactive `props` object is passed
|
|
92
|
+
overlay.props = reactiveComputed(() => ({ ...(props as Record<string, unknown> | undefined), ...eventProps }))
|
|
93
|
+
|
|
94
|
+
overlayStore.register(overlay)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { open }
|
|
99
|
+
}
|
|
@@ -2,16 +2,26 @@ import { toComputed } from '@core/utils/to-computed.util.ts'
|
|
|
2
2
|
import type { Progress } from './types.ts'
|
|
3
3
|
import { computed, type MaybeRefOrGetter } from 'vue'
|
|
4
4
|
|
|
5
|
-
export function useProgress(
|
|
5
|
+
export function useProgress(
|
|
6
|
+
rawCurrent: MaybeRefOrGetter<number>,
|
|
7
|
+
rawTotal: MaybeRefOrGetter<number>,
|
|
8
|
+
rawNoRuler: MaybeRefOrGetter<boolean> = false
|
|
9
|
+
): Progress {
|
|
6
10
|
const current = toComputed(rawCurrent)
|
|
7
11
|
|
|
8
12
|
const total = toComputed(rawTotal)
|
|
9
13
|
|
|
14
|
+
const noRuler = toComputed(rawNoRuler)
|
|
15
|
+
|
|
10
16
|
const percentage = computed(() => (total.value === 0 ? 0 : (current.value / total.value) * 100))
|
|
11
17
|
|
|
12
18
|
const percentageCap = computed(() => Math.max(100, Math.ceil(percentage.value / 100) * 100))
|
|
13
19
|
|
|
14
|
-
const fillWidth = computed(() =>
|
|
20
|
+
const fillWidth = computed(() => {
|
|
21
|
+
const max = noRuler.value ? 100 : percentageCap.value
|
|
22
|
+
|
|
23
|
+
return `${(Math.min(percentage.value, max) / max) * 100}%`
|
|
24
|
+
})
|
|
15
25
|
|
|
16
26
|
return {
|
|
17
27
|
current,
|
|
@@ -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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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 (
|
|
171
|
-
data.value
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SIDEBAR_DEFAULTS, type SidebarSide } from '@core/packages/sidebar/types.ts'
|
|
2
|
+
import { useSidebarResize } from '@core/packages/sidebar/use-sidebar-resize.ts'
|
|
3
|
+
import { useSidebarResponsiveExpand } from '@core/packages/sidebar/use-sidebar-responsive-expand.ts'
|
|
4
|
+
import { useUiStore } from '@core/stores/ui.store.ts'
|
|
5
|
+
import { useLocalStorage, useToggle } from '@vueuse/core'
|
|
6
|
+
import { defineStore } from 'pinia'
|
|
7
|
+
import { computed } from 'vue'
|
|
8
|
+
|
|
9
|
+
function createSidebar(side: SidebarSide) {
|
|
10
|
+
const uiStore = useUiStore()
|
|
11
|
+
const prefix = `sidebar.${side}`
|
|
12
|
+
const defaults = SIDEBAR_DEFAULTS[side]
|
|
13
|
+
|
|
14
|
+
const isExpanded = useLocalStorage(`${prefix}.expanded`, defaults.isExpanded)
|
|
15
|
+
const isLocked = useLocalStorage(`${prefix}.locked`, defaults.isLocked)
|
|
16
|
+
const width = useLocalStorage(`${prefix}.width`, defaults.width)
|
|
17
|
+
|
|
18
|
+
const toggleExpand = useToggle(isExpanded)
|
|
19
|
+
const toggleLock = useToggle(isLocked)
|
|
20
|
+
const { startResize, isResizing } = useSidebarResize(side, width)
|
|
21
|
+
|
|
22
|
+
useSidebarResponsiveExpand(isExpanded)
|
|
23
|
+
|
|
24
|
+
const cssWidth = computed(() => (uiStore.isSmall ? '100%' : `${width.value}px`))
|
|
25
|
+
|
|
26
|
+
const cssLockedOffset = computed(() => (isExpanded.value ? '0' : `-${cssWidth.value}`))
|
|
27
|
+
|
|
28
|
+
const cssUnlockedOffset = computed(() => {
|
|
29
|
+
if (isExpanded.value) {
|
|
30
|
+
return '0'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return side === 'left' ? `-${cssWidth.value}` : cssWidth.value
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
isExpanded,
|
|
38
|
+
isLocked,
|
|
39
|
+
width,
|
|
40
|
+
toggleExpand,
|
|
41
|
+
toggleLock,
|
|
42
|
+
startResize,
|
|
43
|
+
isResizing,
|
|
44
|
+
cssWidth,
|
|
45
|
+
cssLockedOffset,
|
|
46
|
+
cssUnlockedOffset,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const useLeftSidebarStore = defineStore('sidebar-left', () => createSidebar('left'))
|
|
51
|
+
export const useRightSidebarStore = defineStore('sidebar-right', () => createSidebar('right'))
|
|
52
|
+
|
|
53
|
+
export function useSidebar(side: SidebarSide = 'left') {
|
|
54
|
+
return side === 'left' ? useLeftSidebarStore() : useRightSidebarStore()
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SIDES = ['left', 'right'] as const
|
|
2
|
+
|
|
3
|
+
export type SidebarSide = (typeof SIDES)[number]
|
|
4
|
+
|
|
5
|
+
export type SidebarState = {
|
|
6
|
+
isExpanded: boolean
|
|
7
|
+
isLocked: boolean
|
|
8
|
+
width: number
|
|
9
|
+
toggleExpand: (value?: boolean) => boolean
|
|
10
|
+
toggleLock: (value?: boolean) => boolean
|
|
11
|
+
startResize: (event: MouseEvent) => void
|
|
12
|
+
isResizing: boolean
|
|
13
|
+
cssWidth: string
|
|
14
|
+
cssLockedOffset: string | number
|
|
15
|
+
cssUnlockedOffset: string | number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SIDEBAR_DEFAULTS = {
|
|
19
|
+
left: { isExpanded: true, isLocked: true, width: 350 },
|
|
20
|
+
right: { isExpanded: false, isLocked: false, width: 350 },
|
|
21
|
+
} satisfies Record<SidebarSide, Pick<SidebarState, 'isExpanded' | 'isLocked' | 'width'>>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SidebarSide } from '@core/packages/sidebar/types.ts'
|
|
2
|
+
import { ifElse } from '@core/utils/if-else.utils.ts'
|
|
3
|
+
import { useRafFn, useStyleTag } from '@vueuse/core'
|
|
4
|
+
import { ref, type Ref } from 'vue'
|
|
5
|
+
|
|
6
|
+
export function useSidebarResize(side: SidebarSide, width: Ref<number>) {
|
|
7
|
+
const isResizing = ref(false)
|
|
8
|
+
|
|
9
|
+
let initialX: number
|
|
10
|
+
let initialWidth: number
|
|
11
|
+
let clientX: number
|
|
12
|
+
|
|
13
|
+
function startResize(event: MouseEvent) {
|
|
14
|
+
clientX = event.clientX
|
|
15
|
+
initialX = clientX
|
|
16
|
+
initialWidth = width.value
|
|
17
|
+
isResizing.value = true
|
|
18
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
19
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleMouseMove(event: MouseEvent) {
|
|
23
|
+
clientX = event.clientX
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleMouseUp() {
|
|
27
|
+
isResizing.value = false
|
|
28
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
29
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
|
|
33
|
+
|
|
34
|
+
const resizeDelta = () => (side === 'left' ? initialWidth + clientX - initialX : initialWidth + initialX - clientX)
|
|
35
|
+
|
|
36
|
+
const { pause, resume } = useRafFn(
|
|
37
|
+
() => (width.value = Math.min(500, window.innerWidth * 0.3, Math.max(200, resizeDelta()))),
|
|
38
|
+
{ immediate: false }
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
ifElse(isResizing, [load, resume], [pause, unload])
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
startResize,
|
|
45
|
+
isResizing,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useUiStore } from '@core/stores/ui.store.ts'
|
|
2
|
+
import { watch, type Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useSidebarResponsiveExpand(isExpanded: Ref<boolean>) {
|
|
5
|
+
const uiStore = useUiStore()
|
|
6
|
+
|
|
7
|
+
let desktopExpanded = isExpanded.value
|
|
8
|
+
|
|
9
|
+
watch(
|
|
10
|
+
() => uiStore.isSmall,
|
|
11
|
+
(isSmall, wasSmall) => {
|
|
12
|
+
if (isSmall && !wasSmall) {
|
|
13
|
+
desktopExpanded = isExpanded.value
|
|
14
|
+
} else if (!isSmall && wasSmall) {
|
|
15
|
+
isExpanded.value = desktopExpanded
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -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 {
|
|
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
|
|
84
|
-
const node = findNodeByObjectId(nodes.value,
|
|
109
|
+
const expandToNode = (objectId: string | number): TreeNode | undefined => {
|
|
110
|
+
const node = findNodeByObjectId(nodes.value, objectId)
|
|
85
111
|
if (!node) {
|
|
86
|
-
|
|
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(
|
|
96
|
-
if (
|
|
97
|
-
|
|
115
|
+
getHierarchy(node).forEach(ancestor => {
|
|
116
|
+
if (ancestor.isBranch) {
|
|
117
|
+
ancestor.toggleCollapse(false)
|
|
98
118
|
}
|
|
99
119
|
})
|
|
100
120
|
|
|
101
|
-
|
|
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
|
-
|
|
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,
|
|
4
|
+
import { computed, watch } from 'vue'
|
|
4
5
|
|
|
5
6
|
export const usePanelStore = defineStore('panel', () => {
|
|
6
7
|
const uiStore = useUiStore()
|
|
7
|
-
const isExpanded =
|
|
8
|
-
const
|
|
9
|
-
const close = () => (isExpanded.value = false)
|
|
8
|
+
const isExpanded = useLocalStorage('panel.expanded', true)
|
|
9
|
+
const isLocked = useLocalStorage('panel.locked', true)
|
|
10
10
|
|
|
11
|
-
|
|
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
|
})
|