@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.
Files changed (75) hide show
  1. package/lib/assets/css/_colors.pcss +8 -0
  2. package/lib/components/button-group/VtsButtonGroup.vue +5 -1
  3. package/lib/components/menu/MenuList.vue +1 -2
  4. package/lib/components/menu/MenuTrigger.vue +5 -11
  5. package/lib/components/modal/VtsModal.vue +82 -0
  6. package/lib/components/modal/VtsModalButton.vue +36 -0
  7. package/lib/components/modal/VtsModalCancelButton.vue +37 -0
  8. package/lib/components/modal/VtsModalConfirmButton.vue +21 -0
  9. package/lib/components/modal/VtsModalList.vue +34 -0
  10. package/lib/components/object-icon/VtsObjectIcon.vue +3 -8
  11. package/lib/components/status/VtsStatus.vue +66 -0
  12. package/lib/components/task/VtsQuickTaskList.vue +17 -5
  13. package/lib/components/tree/VtsTreeItem.vue +2 -2
  14. package/lib/components/ui/breadcrumb/UiBreadcrumb.vue +79 -0
  15. package/lib/components/ui/button/UiButton.vue +13 -67
  16. package/lib/components/ui/modal/UiModal.vue +164 -0
  17. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +2 -2
  18. package/lib/composables/context.composable.ts +3 -5
  19. package/lib/composables/link-component.composable.ts +3 -2
  20. package/lib/composables/pagination.composable.ts +3 -2
  21. package/lib/composables/tree-filter.composable.ts +5 -3
  22. package/lib/icons/fa-icons.ts +13 -1
  23. package/lib/icons/index.ts +17 -0
  24. package/lib/locales/cs.json +60 -2
  25. package/lib/locales/de.json +40 -2
  26. package/lib/locales/en.json +27 -1
  27. package/lib/locales/es.json +51 -5
  28. package/lib/locales/fa.json +10 -10
  29. package/lib/locales/fr.json +28 -2
  30. package/lib/locales/it.json +4 -0
  31. package/lib/locales/nl.json +64 -14
  32. package/lib/locales/pt_BR.json +3 -3
  33. package/lib/locales/ru.json +41 -2
  34. package/lib/locales/sv.json +55 -1
  35. package/lib/locales/uk.json +4 -4
  36. package/lib/packages/collection/use-collection.ts +3 -2
  37. package/lib/packages/form-select/use-form-option-controller.ts +3 -2
  38. package/lib/packages/form-select/use-form-select.ts +8 -7
  39. package/lib/packages/menu/action.ts +4 -3
  40. package/lib/packages/menu/link.ts +5 -4
  41. package/lib/packages/menu/router-link.ts +3 -2
  42. package/lib/packages/menu/toggle-target.ts +3 -2
  43. package/lib/packages/modal/ModalProvider.vue +17 -0
  44. package/lib/packages/modal/README.md +253 -0
  45. package/lib/packages/modal/create-modal-opener.ts +103 -0
  46. package/lib/packages/modal/modal.store.ts +22 -0
  47. package/lib/packages/modal/types.ts +92 -0
  48. package/lib/packages/modal/use-modal.ts +53 -0
  49. package/lib/packages/progress/use-progress.ts +4 -3
  50. package/lib/packages/table/README.md +336 -0
  51. package/lib/packages/table/apply-extensions.ts +26 -0
  52. package/lib/packages/table/define-columns.ts +62 -0
  53. package/lib/packages/table/define-renderer/define-table-cell-renderer.ts +27 -0
  54. package/lib/packages/table/define-renderer/define-table-renderer.ts +47 -0
  55. package/lib/packages/table/define-renderer/define-table-row-renderer.ts +29 -0
  56. package/lib/packages/table/define-renderer/define-table-section-renderer.ts +29 -0
  57. package/lib/packages/table/define-table/define-multi-source-table.ts +39 -0
  58. package/lib/packages/table/define-table/define-table.ts +13 -0
  59. package/lib/packages/table/define-table/define-typed-table.ts +18 -0
  60. package/lib/packages/table/index.ts +11 -0
  61. package/lib/packages/table/transform-sources.ts +13 -0
  62. package/lib/packages/table/types/extensions.ts +16 -0
  63. package/lib/packages/table/types/index.ts +47 -0
  64. package/lib/packages/table/types/table-cell.ts +18 -0
  65. package/lib/packages/table/types/table-row.ts +20 -0
  66. package/lib/packages/table/types/table-section.ts +19 -0
  67. package/lib/packages/table/types/table.ts +28 -0
  68. package/lib/packages/threshold/use-threshold.ts +4 -3
  69. package/lib/types/vue-virtual-scroller.d.ts +101 -0
  70. package/lib/utils/injection-keys.util.ts +3 -0
  71. package/lib/utils/progress.util.ts +2 -1
  72. package/lib/utils/to-computed.util.ts +15 -0
  73. package/package.json +3 -2
  74. package/lib/components/backup-state/VtsBackupState.vue +0 -37
  75. 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, toValue } from 'vue'
3
+ import { computed, type MaybeRefOrGetter } from 'vue'
3
4
 
4
5
  export function useProgress(rawCurrent: MaybeRefOrGetter<number>, rawTotal: MaybeRefOrGetter<number>): Progress {
5
- const current = computed(() => toValue(rawCurrent))
6
+ const current = toComputed(rawCurrent)
6
7
 
7
- const total = computed(() => toValue(rawTotal))
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
+ }