@xen-orchestra/web-core 0.31.1 → 0.33.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/_colors.pcss +8 -0
- package/lib/components/button-group/VtsButtonGroup.vue +5 -1
- package/lib/components/menu/MenuList.vue +1 -2
- package/lib/components/menu/MenuTrigger.vue +5 -11
- package/lib/components/modal/VtsModal.vue +82 -0
- package/lib/components/modal/VtsModalButton.vue +36 -0
- package/lib/components/modal/VtsModalCancelButton.vue +37 -0
- package/lib/components/modal/VtsModalConfirmButton.vue +21 -0
- package/lib/components/modal/VtsModalList.vue +34 -0
- package/lib/components/object-icon/VtsObjectIcon.vue +3 -8
- package/lib/components/status/VtsStatus.vue +66 -0
- package/lib/components/task/VtsQuickTaskList.vue +17 -5
- package/lib/components/tree/VtsTreeItem.vue +2 -2
- package/lib/components/ui/breadcrumb/UiBreadcrumb.vue +79 -0
- package/lib/components/ui/button/UiButton.vue +13 -67
- package/lib/components/ui/modal/UiModal.vue +164 -0
- package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +2 -2
- package/lib/composables/context.composable.ts +3 -5
- package/lib/composables/link-component.composable.ts +3 -2
- package/lib/composables/pagination.composable.ts +3 -2
- package/lib/composables/tree-filter.composable.ts +5 -3
- package/lib/icons/fa-icons.ts +13 -1
- package/lib/icons/index.ts +17 -0
- package/lib/locales/cs.json +60 -2
- package/lib/locales/de.json +40 -2
- package/lib/locales/en.json +27 -1
- package/lib/locales/es.json +51 -5
- package/lib/locales/fa.json +10 -10
- package/lib/locales/fr.json +28 -2
- package/lib/locales/it.json +4 -0
- package/lib/locales/nl.json +64 -14
- package/lib/locales/pt_BR.json +3 -3
- package/lib/locales/ru.json +41 -2
- package/lib/locales/sv.json +55 -1
- package/lib/locales/uk.json +4 -4
- package/lib/packages/collection/use-collection.ts +3 -2
- package/lib/packages/form-select/use-form-option-controller.ts +3 -2
- package/lib/packages/form-select/use-form-select.ts +8 -7
- package/lib/packages/menu/action.ts +4 -3
- package/lib/packages/menu/link.ts +5 -4
- package/lib/packages/menu/router-link.ts +3 -2
- package/lib/packages/menu/toggle-target.ts +3 -2
- package/lib/packages/modal/ModalProvider.vue +17 -0
- package/lib/packages/modal/README.md +253 -0
- package/lib/packages/modal/create-modal-opener.ts +103 -0
- package/lib/packages/modal/modal.store.ts +22 -0
- package/lib/packages/modal/types.ts +92 -0
- package/lib/packages/modal/use-modal.ts +53 -0
- package/lib/packages/progress/use-progress.ts +4 -3
- package/lib/packages/table/README.md +336 -0
- package/lib/packages/table/apply-extensions.ts +26 -0
- package/lib/packages/table/define-columns.ts +62 -0
- package/lib/packages/table/define-renderer/define-table-cell-renderer.ts +27 -0
- package/lib/packages/table/define-renderer/define-table-renderer.ts +47 -0
- package/lib/packages/table/define-renderer/define-table-row-renderer.ts +29 -0
- package/lib/packages/table/define-renderer/define-table-section-renderer.ts +29 -0
- package/lib/packages/table/define-table/define-multi-source-table.ts +39 -0
- package/lib/packages/table/define-table/define-table.ts +13 -0
- package/lib/packages/table/define-table/define-typed-table.ts +18 -0
- package/lib/packages/table/index.ts +11 -0
- package/lib/packages/table/transform-sources.ts +13 -0
- package/lib/packages/table/types/extensions.ts +16 -0
- package/lib/packages/table/types/index.ts +47 -0
- package/lib/packages/table/types/table-cell.ts +18 -0
- package/lib/packages/table/types/table-row.ts +20 -0
- package/lib/packages/table/types/table-section.ts +19 -0
- package/lib/packages/table/types/table.ts +28 -0
- package/lib/packages/threshold/use-threshold.ts +4 -3
- package/lib/types/vue-virtual-scroller.d.ts +101 -0
- package/lib/utils/injection-keys.util.ts +3 -0
- package/lib/utils/progress.util.ts +2 -1
- package/lib/utils/to-computed.util.ts +15 -0
- package/package.json +3 -2
- package/lib/components/backup-state/VtsBackupState.vue +0 -37
- package/lib/components/connection-status/VtsConnectionStatus.vue +0 -36
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useModalStore } from '@core/packages/modal/modal.store.ts'
|
|
2
|
+
import {
|
|
3
|
+
ABORT_MODAL,
|
|
4
|
+
ModalCancelResponse,
|
|
5
|
+
type ModalConfig,
|
|
6
|
+
ModalConfirmResponse,
|
|
7
|
+
type ModalHandlerArgs,
|
|
8
|
+
type ModalResponse,
|
|
9
|
+
} from '@core/packages/modal/types.ts'
|
|
10
|
+
import { computed, defineAsyncComponent, reactive, ref, watch } from 'vue'
|
|
11
|
+
import { useRoute } from 'vue-router'
|
|
12
|
+
|
|
13
|
+
export function createModalOpener() {
|
|
14
|
+
const modalStore = useModalStore()
|
|
15
|
+
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
|
|
18
|
+
const ids = new Set<symbol | string>()
|
|
19
|
+
|
|
20
|
+
function closeById(id: string | symbol) {
|
|
21
|
+
modalStore.removeModal(id)
|
|
22
|
+
ids.delete(id)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
watch(
|
|
26
|
+
() => route.path,
|
|
27
|
+
() => {
|
|
28
|
+
ids.forEach(id => {
|
|
29
|
+
if (!modalStore.getModal(id)?.keepOpenOnRouteChange) {
|
|
30
|
+
closeById(id)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return function openModal<
|
|
37
|
+
TProps,
|
|
38
|
+
TConfirmArgs extends ModalHandlerArgs<TProps, 'onConfirm'>,
|
|
39
|
+
TCancelArgs extends ModalHandlerArgs<TProps, 'onCancel'>,
|
|
40
|
+
TConfirmPayload = undefined,
|
|
41
|
+
TCancelPayload = undefined,
|
|
42
|
+
>(id: string | symbol, config: ModalConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>) {
|
|
43
|
+
const close = () => closeById(id)
|
|
44
|
+
|
|
45
|
+
const promise = new Promise<ModalResponse<TConfirmPayload, TCancelPayload>>(resolve => {
|
|
46
|
+
const isBusy = ref(false)
|
|
47
|
+
|
|
48
|
+
modalStore.addModal({
|
|
49
|
+
id,
|
|
50
|
+
component: defineAsyncComponent(() => config.component),
|
|
51
|
+
isBusy: computed(() => isBusy.value),
|
|
52
|
+
props: reactive(config.props ?? {}),
|
|
53
|
+
keepOpenOnRouteChange: config.keepOpenOnRouteChange ?? false,
|
|
54
|
+
onConfirm: async (...args: TConfirmArgs) => {
|
|
55
|
+
try {
|
|
56
|
+
isBusy.value = true
|
|
57
|
+
|
|
58
|
+
const result = config.onConfirm ? await config.onConfirm(...args) : (undefined as TConfirmPayload)
|
|
59
|
+
|
|
60
|
+
if (result === ABORT_MODAL || result instanceof ModalCancelResponse) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result instanceof ModalConfirmResponse) {
|
|
65
|
+
resolve(result)
|
|
66
|
+
} else {
|
|
67
|
+
resolve(new ModalConfirmResponse(result))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
close()
|
|
71
|
+
} finally {
|
|
72
|
+
isBusy.value = false
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
onCancel: async (...args: TCancelArgs) => {
|
|
76
|
+
try {
|
|
77
|
+
isBusy.value = true
|
|
78
|
+
|
|
79
|
+
const result = config.onCancel ? await config.onCancel(...args) : (undefined as TCancelPayload)
|
|
80
|
+
|
|
81
|
+
if (result === ABORT_MODAL || result instanceof ModalCancelResponse) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result instanceof ModalConfirmResponse) {
|
|
86
|
+
resolve(new ModalCancelResponse(result.payload))
|
|
87
|
+
} else {
|
|
88
|
+
resolve(new ModalCancelResponse(result))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
close()
|
|
92
|
+
} finally {
|
|
93
|
+
isBusy.value = false
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return Object.assign(promise, {
|
|
100
|
+
close,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RegisteredModal } from '@core/packages/modal/types.ts'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { computed, shallowReactive } from 'vue'
|
|
4
|
+
|
|
5
|
+
export const useModalStore = defineStore('new-modal', () => {
|
|
6
|
+
const modals = shallowReactive(new Map<symbol | string, RegisteredModal>())
|
|
7
|
+
|
|
8
|
+
const addModal = (modal: RegisteredModal) => {
|
|
9
|
+
modals.set(modal.id, modal)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const removeModal = (id: symbol | string) => {
|
|
13
|
+
modals.delete(id)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
modals: computed(() => Array.from(modals.values())),
|
|
18
|
+
getModal: (id: symbol | string) => modals.get(id),
|
|
19
|
+
addModal,
|
|
20
|
+
removeModal,
|
|
21
|
+
}
|
|
22
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { MaybeRef } from '@vueuse/core'
|
|
2
|
+
import type { Component, ComputedRef, InjectionKey } from 'vue'
|
|
3
|
+
|
|
4
|
+
export type ModalPropsOption<TProps> = {
|
|
5
|
+
[K in keyof TProps]: MaybeRef<TProps[K]>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type MaybePromise<T> = T | Promise<T>
|
|
9
|
+
|
|
10
|
+
export type AreAllPropsOptional<TProps> = Record<string, never> extends TProps ? true : false
|
|
11
|
+
|
|
12
|
+
export type RegisteredModal = {
|
|
13
|
+
id: symbol | string
|
|
14
|
+
component: Component
|
|
15
|
+
keepOpenOnRouteChange: boolean
|
|
16
|
+
props: object
|
|
17
|
+
isBusy: ComputedRef<boolean>
|
|
18
|
+
onConfirm: (...args: any[]) => void | Promise<void>
|
|
19
|
+
onCancel: (...args: any[]) => void | Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ModalHandlerArgs<TProps, THandler extends 'onConfirm' | 'onCancel'> = TProps extends {
|
|
23
|
+
[K in THandler]?: (...args: infer TArgs) => any
|
|
24
|
+
}
|
|
25
|
+
? TArgs
|
|
26
|
+
: never
|
|
27
|
+
|
|
28
|
+
export abstract class AbstractModalResponse<TPayload> {
|
|
29
|
+
constructor(public readonly payload: TPayload) {}
|
|
30
|
+
|
|
31
|
+
abstract get confirmed(): boolean
|
|
32
|
+
abstract get canceled(): boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ModalConfirmResponse<TPayload> extends AbstractModalResponse<TPayload> {
|
|
36
|
+
public readonly confirmed = true
|
|
37
|
+
public readonly canceled = false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ModalCancelResponse<TPayload> extends AbstractModalResponse<TPayload> {
|
|
41
|
+
public readonly confirmed = false
|
|
42
|
+
public readonly canceled = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ModalResponse<TConfirmPayload, TCancelPayload> =
|
|
46
|
+
| ModalConfirmResponse<TConfirmPayload>
|
|
47
|
+
| ModalCancelResponse<TCancelPayload>
|
|
48
|
+
|
|
49
|
+
export const ABORT_MODAL = Symbol('abort modal')
|
|
50
|
+
|
|
51
|
+
export type OpenModalReturn<TConfirmPayload, TCancelPayload> = Promise<
|
|
52
|
+
ModalResponse<TConfirmPayload, TCancelPayload>
|
|
53
|
+
> & {
|
|
54
|
+
close: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const IK_MODAL = Symbol('modal') as InjectionKey<ComputedRef<RegisteredModal>>
|
|
58
|
+
|
|
59
|
+
export type ModalPropsConfigEntry<TProps> =
|
|
60
|
+
AreAllPropsOptional<TProps> extends true ? { props?: ModalPropsOption<TProps> } : { props: ModalPropsOption<TProps> }
|
|
61
|
+
|
|
62
|
+
export type ModalConfig<
|
|
63
|
+
TProps,
|
|
64
|
+
TConfirmArgs extends any[],
|
|
65
|
+
TCancelArgs extends any[],
|
|
66
|
+
TConfirmPayload,
|
|
67
|
+
TCancelPayload,
|
|
68
|
+
> = {
|
|
69
|
+
component: Promise<{
|
|
70
|
+
default: abstract new () => {
|
|
71
|
+
$props: TProps
|
|
72
|
+
}
|
|
73
|
+
}>
|
|
74
|
+
onConfirm?: (...args: TConfirmArgs) => MaybePromise<ModalResponse<TConfirmPayload, any> | TConfirmPayload>
|
|
75
|
+
onCancel?: (...args: TCancelArgs) => MaybePromise<ModalResponse<TCancelPayload, any> | TCancelPayload>
|
|
76
|
+
keepOpenOnRouteChange?: boolean
|
|
77
|
+
} & ModalPropsConfigEntry<TProps>
|
|
78
|
+
|
|
79
|
+
export type UseModalReturn<TArgs extends any[] = any[], TConfirmPayload = any, TCancelPayload = any> = (
|
|
80
|
+
...args: TArgs
|
|
81
|
+
) => OpenModalReturn<TConfirmPayload, TCancelPayload>
|
|
82
|
+
|
|
83
|
+
export type OpenModal = <
|
|
84
|
+
TProps,
|
|
85
|
+
TConfirmArgs extends ModalHandlerArgs<TProps, 'onConfirm'>,
|
|
86
|
+
TCancelArgs extends ModalHandlerArgs<TProps, 'onCancel'>,
|
|
87
|
+
TConfirmPayload = undefined,
|
|
88
|
+
TCancelPayload = undefined,
|
|
89
|
+
>(
|
|
90
|
+
id: string,
|
|
91
|
+
config: ModalConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
|
|
92
|
+
) => OpenModalReturn<TConfirmPayload, TCancelPayload>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createModalOpener } from '@core/packages/modal/create-modal-opener.ts'
|
|
2
|
+
import { type ModalConfig, type ModalHandlerArgs, type UseModalReturn } from '@core/packages/modal/types.ts'
|
|
3
|
+
|
|
4
|
+
export function useModal(): ReturnType<typeof createModalOpener>
|
|
5
|
+
|
|
6
|
+
export function useModal<
|
|
7
|
+
TProps,
|
|
8
|
+
TConfirmArgs extends ModalHandlerArgs<TProps, 'onConfirm'>,
|
|
9
|
+
TCancelArgs extends ModalHandlerArgs<TProps, 'onCancel'>,
|
|
10
|
+
TConfirmPayload = undefined,
|
|
11
|
+
TCancelPayload = undefined,
|
|
12
|
+
>(
|
|
13
|
+
config: ModalConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
|
|
14
|
+
): UseModalReturn<[], TConfirmPayload, TCancelPayload>
|
|
15
|
+
|
|
16
|
+
export function useModal<
|
|
17
|
+
TConfigBuilderArgs extends any[],
|
|
18
|
+
TProps,
|
|
19
|
+
TConfirmArgs extends ModalHandlerArgs<TProps, 'onConfirm'>,
|
|
20
|
+
TCancelArgs extends ModalHandlerArgs<TProps, 'onCancel'>,
|
|
21
|
+
TConfirmPayload = undefined,
|
|
22
|
+
TCancelPayload = undefined,
|
|
23
|
+
>(
|
|
24
|
+
configBuilder: (
|
|
25
|
+
...args: TConfigBuilderArgs
|
|
26
|
+
) => ModalConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>
|
|
27
|
+
): UseModalReturn<TConfigBuilderArgs, TConfirmPayload, TCancelPayload>
|
|
28
|
+
|
|
29
|
+
export function useModal<
|
|
30
|
+
TConfigBuilderArgs extends any[],
|
|
31
|
+
TProps,
|
|
32
|
+
TConfirmArgs extends ModalHandlerArgs<TProps, 'onConfirm'>,
|
|
33
|
+
TCancelArgs extends ModalHandlerArgs<TProps, 'onCancel'>,
|
|
34
|
+
TConfig extends ModalConfig<TProps, TConfirmArgs, TCancelArgs, TConfirmPayload, TCancelPayload>,
|
|
35
|
+
TConfirmPayload = undefined,
|
|
36
|
+
TCancelPayload = undefined,
|
|
37
|
+
>(
|
|
38
|
+
configOrBuilder?: TConfig | ((...args: TConfigBuilderArgs) => TConfig)
|
|
39
|
+
): UseModalReturn<TConfigBuilderArgs, TConfirmPayload, TCancelPayload> | ReturnType<typeof createModalOpener> {
|
|
40
|
+
const openModal = createModalOpener()
|
|
41
|
+
|
|
42
|
+
if (configOrBuilder === undefined) {
|
|
43
|
+
return openModal
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const id = Symbol('modal')
|
|
47
|
+
|
|
48
|
+
return (...args: TConfigBuilderArgs) => {
|
|
49
|
+
const config = typeof configOrBuilder === 'function' ? configOrBuilder(...args) : configOrBuilder
|
|
50
|
+
|
|
51
|
+
return openModal(id, config)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { toComputed } from '@core/utils/to-computed.util.ts'
|
|
1
2
|
import type { Progress } from './types.ts'
|
|
2
|
-
import { computed, type MaybeRefOrGetter
|
|
3
|
+
import { computed, type MaybeRefOrGetter } from 'vue'
|
|
3
4
|
|
|
4
5
|
export function useProgress(rawCurrent: MaybeRefOrGetter<number>, rawTotal: MaybeRefOrGetter<number>): Progress {
|
|
5
|
-
const current =
|
|
6
|
+
const current = toComputed(rawCurrent)
|
|
6
7
|
|
|
7
|
-
const total =
|
|
8
|
+
const total = toComputed(rawTotal)
|
|
8
9
|
|
|
9
10
|
const percentage = computed(() => (total.value === 0 ? 0 : (current.value / total.value) * 100))
|
|
10
11
|
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Table System
|
|
2
|
+
|
|
3
|
+
Table system that separates data logic from presentation through reusable renderers.
|
|
4
|
+
|
|
5
|
+
## Understanding Renderers
|
|
6
|
+
|
|
7
|
+
A **renderer** is a function that creates a VNode for a specific part of the table (cell, row, or table). When you define a renderer, you specify:
|
|
8
|
+
|
|
9
|
+
1. **Component**: The Vue component to render (loaded asynchronously)
|
|
10
|
+
2. **Props function** (optional): Default props based on configuration
|
|
11
|
+
3. **Extensions** (optional): Named categories of additional functionality
|
|
12
|
+
|
|
13
|
+
### Props System
|
|
14
|
+
|
|
15
|
+
The `props` parameter is a function that receives an optional typed config and returns default props:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const TextBody = defineTableCellRenderer({
|
|
19
|
+
component: () => import('./TextCell.vue'),
|
|
20
|
+
props: (config: { text: string }) => ({ data: config.text }),
|
|
21
|
+
// ^ This config type will be enforced when using the renderer
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Usage - TypeScript knows you need to provide `text`
|
|
25
|
+
TextBody({ text: 'Hello' })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
When you use the renderer, you can:
|
|
29
|
+
|
|
30
|
+
- Provide the expected config to satisfy the `props` function
|
|
31
|
+
- Add additional props that will be merged
|
|
32
|
+
- Override default props
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// The renderer merges:
|
|
36
|
+
// 1. Props from the props function: { text: 'Hello' }
|
|
37
|
+
// 2. Additional/override props: { class: 'custom' }
|
|
38
|
+
TextBody({
|
|
39
|
+
text: 'Hello', // Used by props function
|
|
40
|
+
props: {
|
|
41
|
+
// Additional or override props
|
|
42
|
+
class: 'custom',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Extensions System
|
|
48
|
+
|
|
49
|
+
Extensions work like the `props` function but are optional and named. Each extension:
|
|
50
|
+
|
|
51
|
+
- Has a unique name (like `selectable`, `highlightable`)
|
|
52
|
+
- Receives typed configuration
|
|
53
|
+
- Returns the extension arguments (only `props` for now) to merge into the component
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const MyRow = defineTableRowRenderer({
|
|
57
|
+
component: () => import('./Row.vue'),
|
|
58
|
+
extensions: {
|
|
59
|
+
selectable: (config: { id: string; selectedId: Ref<string | null> }) => ({
|
|
60
|
+
props: {
|
|
61
|
+
selected: config.id === config.selectedId.value,
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
highlightable: (config: { isHighlighted: boolean }) => ({
|
|
65
|
+
props: {
|
|
66
|
+
highlighted: config.isHighlighted,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Usage - provide config for the extensions you want to use
|
|
73
|
+
MyRow({
|
|
74
|
+
cells: () => [...],
|
|
75
|
+
extensions: {
|
|
76
|
+
selectable: { id: user.id, selectedId },
|
|
77
|
+
highlightable: { isHighlighted: true },
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Extensions are optional when using the renderer - you only provide the ones you need.
|
|
83
|
+
|
|
84
|
+
## Defining Renderers
|
|
85
|
+
|
|
86
|
+
### Cell Renderers
|
|
87
|
+
|
|
88
|
+
Create header and body cell renderers:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { defineTableCellRenderer } from '@core/packages/table'
|
|
92
|
+
|
|
93
|
+
const TextHeader = defineTableCellRenderer({
|
|
94
|
+
component: () => import('./VtsHeaderCell.vue'),
|
|
95
|
+
props: (config: { label: string }) => ({
|
|
96
|
+
label: config.label,
|
|
97
|
+
icon: icon('fa:align-left'),
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const TextBody = defineTableCellRenderer({
|
|
102
|
+
component: () => import('./body-cells/VtsTextCell.vue'),
|
|
103
|
+
props: (config: { text: string | number }) => ({ text: config.text }),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Usage
|
|
107
|
+
TextHeader({ label: 'Name' })
|
|
108
|
+
TextBody({ text: user.name })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Row Renderers
|
|
112
|
+
|
|
113
|
+
Create row renderer:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { defineTableRowRenderer } from '@core/packages/table'
|
|
117
|
+
|
|
118
|
+
const DefaultRow = defineTableRowRenderer({
|
|
119
|
+
component: () => import('./VtsRow.vue'),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
DefaultRow({
|
|
123
|
+
cells: () => [...]
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Table Renderers
|
|
128
|
+
|
|
129
|
+
Create table renderer:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { defineTableRenderer } from '@core/packages/table'
|
|
133
|
+
|
|
134
|
+
const DefaultTable = defineTableRenderer({
|
|
135
|
+
component: () => import('./VtsTableNew.vue'),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Usage
|
|
139
|
+
DefaultTable({
|
|
140
|
+
thead: MyThead(...),
|
|
141
|
+
// thead: { rows: () => [...] }, // to use native "thead"
|
|
142
|
+
// thead: { cells: () => [...] }, // to use native "thead" + "tr",
|
|
143
|
+
tbody: MyTBody(...),
|
|
144
|
+
// tbody: { rows: () => [...] }, // to use native "tbody"
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Building Tables
|
|
149
|
+
|
|
150
|
+
### Column Definition
|
|
151
|
+
|
|
152
|
+
Use `defineColumns` to create columns configuration.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const columns = defineColumns({
|
|
156
|
+
name: {
|
|
157
|
+
header: () => TextHeader({ label: 'Name' }),
|
|
158
|
+
body: user => TextBody({ text: user.name }),
|
|
159
|
+
},
|
|
160
|
+
email: {
|
|
161
|
+
header: () => TextHeader({ label: 'Email' }),
|
|
162
|
+
body: user => TextBody({ text: user.email }),
|
|
163
|
+
},
|
|
164
|
+
// Conditional column
|
|
165
|
+
role: isAdmin
|
|
166
|
+
? {
|
|
167
|
+
header: () => TextHeader({ label: 'Role' }),
|
|
168
|
+
body: user => TextBody({ text: user.role }),
|
|
169
|
+
}
|
|
170
|
+
: undefined,
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`header` and `body` can also take a config parameter if needed:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const columns = defineColumns({
|
|
178
|
+
name: {
|
|
179
|
+
header: (config) => TextHeader(...),
|
|
180
|
+
body: (user, config) => TextBody(...),
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// API
|
|
187
|
+
columns.getHeaderCells(config?) // Array of header cell VNodes
|
|
188
|
+
columns.getBodyCells(user, config?) // Array of body cell VNodes for a row
|
|
189
|
+
columns.toggleColumn('name') // Toggle column visibility
|
|
190
|
+
columns.toggleColumn('name', true) // Force column visibility
|
|
191
|
+
columns.visibleColumnsCount // ComputedRef<number>, useful for colspan
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Table Definition
|
|
195
|
+
|
|
196
|
+
Use one of three table definition functions:
|
|
197
|
+
|
|
198
|
+
#### Define basic single-source table: `defineTable`
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const { getHeaderCells, getBodyCells } = defineColumns(...)
|
|
202
|
+
|
|
203
|
+
const useUserTable = defineTable((sources: ComputedRef<User[]>) =>
|
|
204
|
+
() => DefaultTable({
|
|
205
|
+
thead: {
|
|
206
|
+
cells: () => getHeaderCells()
|
|
207
|
+
},
|
|
208
|
+
tbody: {
|
|
209
|
+
rows: () => sources.value.map(user =>
|
|
210
|
+
DefaultRow({
|
|
211
|
+
cells: () => getBodyCells(user)
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Usage
|
|
221
|
+
const users = ref<User[]>([...])
|
|
222
|
+
|
|
223
|
+
const table = useUserTable(users, {})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`defineTable` setup function can also define a config parameter as second argument:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const useUserTable = defineTable((sources: ComputedRef<User[]>, config: { needThis: string }) => ...)
|
|
230
|
+
|
|
231
|
+
const table = useUserTable(users, { needThis: 'value' })
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Define type-discriminated table: `defineTypedTable`
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
type Source = { type: 'user'; sources: ComputedRef<User[]> } | { type: 'admin'; sources: ComputedRef<Admin[]> }
|
|
238
|
+
|
|
239
|
+
const useItemTable = defineTypedTable(({ type, sources }: Source) => {
|
|
240
|
+
// If type === 'user', sources is ComputedRef<User[]>
|
|
241
|
+
// If type === 'admin', sources is ComputedRef<Admin[]>
|
|
242
|
+
|
|
243
|
+
return () => DefaultTable({...})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Usage
|
|
247
|
+
useItemTable('admin', admins, {})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Define multiple sources table: `defineMultiSourceTable`
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
type Sources = {
|
|
254
|
+
users: ComputedRef<User[]>
|
|
255
|
+
admins: ComputedRef<Admin[]>
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const useDashboard = defineMultiSourceTable((sources: Sources) => {
|
|
259
|
+
// sources.users: ComputedRef<User[]>
|
|
260
|
+
// sources.admins: ComputedRef<Admin[]>
|
|
261
|
+
|
|
262
|
+
return () => DefaultTable({...})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Usage
|
|
266
|
+
useDashboard({ users, admins }, {})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Source Transformation
|
|
270
|
+
|
|
271
|
+
When using a defined table, if passed sources doesn't match expected sources, then a `transform` config will be required to add missing or incorrectly typed properties:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
type User = {
|
|
275
|
+
id: string
|
|
276
|
+
fullName: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const useUserTable = defineTable((sources: ComputedRef<User[]>) => {})
|
|
280
|
+
|
|
281
|
+
// Raw data has different shape
|
|
282
|
+
interface RawUser {
|
|
283
|
+
uuid: string
|
|
284
|
+
firstName: string
|
|
285
|
+
lastName: string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Transform is required when types don't match
|
|
289
|
+
useUserTable(rawUsers, {
|
|
290
|
+
transform: user => ({
|
|
291
|
+
id: user.uuid,
|
|
292
|
+
fullName: `${user.firstName} ${user.lastName}`,
|
|
293
|
+
}),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Transform is optional when types already match
|
|
297
|
+
useUserTable(users, {})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Rendering the table
|
|
301
|
+
|
|
302
|
+
```vue
|
|
303
|
+
<template>
|
|
304
|
+
<MyUsersTable />
|
|
305
|
+
</template>
|
|
306
|
+
|
|
307
|
+
<script setup lang="ts">
|
|
308
|
+
const MyUsersTable = useUsersTable(users, {})
|
|
309
|
+
</script>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Props
|
|
313
|
+
|
|
314
|
+
When a table is rendered, each element's props will be merged together in the following order:
|
|
315
|
+
|
|
316
|
+
1. Props from the renderer `props` function
|
|
317
|
+
2. Props from extensions `props` functions
|
|
318
|
+
3. Props provided when using the renderer
|
|
319
|
+
|
|
320
|
+
They will be merged with Vue's default merging strategy (for example, `class` and `style` will be concatenated).
|
|
321
|
+
|
|
322
|
+
## API Reference
|
|
323
|
+
|
|
324
|
+
### Renderer Functions
|
|
325
|
+
|
|
326
|
+
- `defineTableRenderer` - Define table wrapper (`table`)
|
|
327
|
+
- `defineTableSectionRenderer` - Define table sections (`thead` / `tbody`)
|
|
328
|
+
- `defineTableRowRenderer` - Define table rows (`tr`)
|
|
329
|
+
- `defineTableCellRenderer` - Define table cells (`th` / `td`)
|
|
330
|
+
|
|
331
|
+
### Table Functions
|
|
332
|
+
|
|
333
|
+
- `defineTable` - Single source table
|
|
334
|
+
- `defineTypedTable` - Type-discriminated table
|
|
335
|
+
- `defineMultiSourceTable` - Multiple sources table
|
|
336
|
+
- `defineColumns` - Column definitions
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Extensions } from '.'
|
|
2
|
+
import { mergeProps } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function applyExtensions(
|
|
5
|
+
config: {
|
|
6
|
+
props?: (config: any) => Record<string, any>
|
|
7
|
+
extensions?: Extensions<any>
|
|
8
|
+
},
|
|
9
|
+
renderConfig: {
|
|
10
|
+
props?: Record<string, any>
|
|
11
|
+
extensions?: Record<string, any>
|
|
12
|
+
}
|
|
13
|
+
): { props: Record<string, any> } {
|
|
14
|
+
const baseProps = mergeProps(config.props?.(renderConfig) ?? {}, renderConfig.props ?? {})
|
|
15
|
+
|
|
16
|
+
if (!renderConfig.extensions) {
|
|
17
|
+
return { props: baseProps }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = Object.entries(renderConfig.extensions).reduce(
|
|
21
|
+
(props, [extName, extData]) => mergeProps(props, config.extensions?.[extName!](extData).props ?? {}),
|
|
22
|
+
baseProps
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return { props }
|
|
26
|
+
}
|