@xen-orchestra/web-core 0.19.0 → 0.20.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/components/backdrop/VtsBackdrop.vue +1 -1
- package/lib/components/column/VtsColumn.vue +21 -0
- package/lib/components/columns/VtsColumns.vue +38 -0
- package/lib/components/copy-button/VtsCopyButton.vue +29 -0
- package/lib/components/enabled-state/VtsEnabledState.vue +23 -0
- package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
- package/lib/components/quick-info-column/VtsQuickInfoColumn.vue +1 -1
- package/lib/components/quick-info-row/VtsQuickInfoRow.vue +26 -7
- package/lib/components/relative-time/VtsRelativeTime.vue +18 -0
- package/lib/components/select/VtsOption.vue +24 -0
- package/lib/components/select/VtsSelect.vue +96 -0
- package/lib/components/state-hero/VtsLoadingHero.vue +45 -4
- package/lib/components/tree/VtsTreeItem.vue +11 -1
- package/lib/components/ui/dropdown/UiDropdownList.vue +10 -2
- package/lib/components/ui/head-bar/UiHeadBar.vue +2 -2
- package/lib/composables/relative-time.composable.ts +1 -1
- package/lib/locales/cs.json +0 -1
- package/lib/locales/en.json +65 -1
- package/lib/locales/es.json +0 -1
- package/lib/locales/fa.json +0 -1
- package/lib/locales/fr.json +67 -3
- package/lib/locales/nl.json +0 -1
- package/lib/locales/sv.json +0 -1
- package/lib/packages/collection/README.md +38 -33
- package/lib/packages/collection/create-collection.ts +27 -13
- package/lib/packages/collection/create-item.ts +39 -0
- package/lib/packages/collection/guess-item-id.ts +26 -0
- package/lib/packages/collection/index.ts +0 -3
- package/lib/packages/collection/types.ts +46 -18
- package/lib/packages/collection/use-collection.ts +39 -7
- package/lib/packages/collection/use-flag-registry.ts +22 -5
- package/lib/packages/form-select/README.md +96 -0
- package/lib/packages/form-select/index.ts +2 -0
- package/lib/packages/form-select/types.ts +75 -0
- package/lib/packages/form-select/use-form-option-controller.ts +50 -0
- package/lib/packages/form-select/use-form-select-controller.ts +205 -0
- package/lib/packages/form-select/use-form-select-keyboard-navigation.ts +157 -0
- package/lib/packages/form-select/use-form-select.ts +193 -0
- package/lib/stores/sidebar.store.ts +14 -1
- package/package.json +1 -1
- package/lib/packages/collection/build-item.ts +0 -45
|
@@ -6,11 +6,11 @@ The `useCollection` composable helps you manage a collection of items with flags
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
const { items, useSubset, useFlag } = useCollection(sources, {
|
|
9
|
-
identifier: source => source.id,
|
|
10
9
|
flags: ['selected', 'active', { highlighted: { multiple: false } }],
|
|
11
10
|
properties: source => ({
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
id: source.theId, // Required if TSource doesn't have an `id` property
|
|
12
|
+
isAvailable: source.status === 'available',
|
|
13
|
+
fullName: `${source.firstName} ${source.lastName}`,
|
|
14
14
|
}),
|
|
15
15
|
})
|
|
16
16
|
```
|
|
@@ -19,22 +19,27 @@ const { items, useSubset, useFlag } = useCollection(sources, {
|
|
|
19
19
|
|
|
20
20
|
- **Collection Item**: An object with a unique identifier, a reference to its source object, flags, computed properties, and methods to manipulate flags
|
|
21
21
|
- **Flags**: Boolean states attached to items (like 'selected', 'active', 'highlighted')
|
|
22
|
-
- **Properties**:
|
|
22
|
+
- **Properties**: Additional custom values
|
|
23
23
|
|
|
24
24
|
## `useCollection` parameters
|
|
25
25
|
|
|
26
|
-
| Name | Type
|
|
27
|
-
| --------- |
|
|
28
|
-
| `sources` | `MaybeRefOrGetter<TSource[]>`
|
|
29
|
-
| `options` | `CollectionOptions<TSource,
|
|
26
|
+
| Name | Type | Required | Description |
|
|
27
|
+
| --------- | ------------------------------------------------ | :------: | ---------------------------------------------------- |
|
|
28
|
+
| `sources` | `MaybeRefOrGetter<TSource[]>` | ✓ | Array of source objects for the collection |
|
|
29
|
+
| `options` | `CollectionOptions<TSource, TFlag, TProperties>` | ~ | Configuration options for the collection (see below) |
|
|
30
30
|
|
|
31
31
|
### `options` object
|
|
32
32
|
|
|
33
|
-
| Name | Type
|
|
34
|
-
| ------------ |
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
|
|
33
|
+
| Name | Type | Required | Description |
|
|
34
|
+
| ------------ | ---------------------------------------------- | :------: | --------------------------------------------------------------------- |
|
|
35
|
+
| `flags` | `FlagsConfig<TFlag>` | | Flags that can be applied to items in the collection |
|
|
36
|
+
| `properties` | `(source: TSource) => Record<string, unknown>` | ~ | Function that returns additional properties for each item (see below) |
|
|
37
|
+
|
|
38
|
+
### Item ID
|
|
39
|
+
|
|
40
|
+
The item ID will be retrieved automatically from `TSource.id`
|
|
41
|
+
|
|
42
|
+
If `TSource` doesn't provide an `id`, then `options.properties` will be required and must return at least an `id`
|
|
38
43
|
|
|
39
44
|
### `FlagsConfig` type
|
|
40
45
|
|
|
@@ -44,17 +49,17 @@ type FlagsConfig<TFlag extends string> = TFlag[] | { [K in TFlag]: { multiple?:
|
|
|
44
49
|
|
|
45
50
|
Values for `multiple`:
|
|
46
51
|
|
|
47
|
-
- `true` (default): multiple items can
|
|
52
|
+
- `true` (default): multiple items can share the same flag.
|
|
48
53
|
- `false`: only one item can have the flag at a time (exclusive selection). Setting the flag on one item will automatically unset it on all other items.
|
|
49
54
|
|
|
50
55
|
## Return Value
|
|
51
56
|
|
|
52
|
-
| Name | Type | Description
|
|
53
|
-
| ----------- | ------------------------------------------- |
|
|
54
|
-
| `items` | `ComputedRef<CollectionItem[]>` | Array of collection items with flags and properties
|
|
55
|
-
| `useSubset` | `(filter: (item) => boolean) => Collection` | Creates a
|
|
56
|
-
| `useFlag` | `(flag: TFlag) =>
|
|
57
|
-
| `count` | `ComputedRef<number>` | Number of items in the collection
|
|
57
|
+
| Name | Type | Description |
|
|
58
|
+
| ----------- | ------------------------------------------- | --------------------------------------------------- |
|
|
59
|
+
| `items` | `ComputedRef<CollectionItem[]>` | Array of collection items with flags and properties |
|
|
60
|
+
| `useSubset` | `(filter: (item) => boolean) => Collection` | Creates a sub collection matching the filter |
|
|
61
|
+
| `useFlag` | `(flag: TFlag) => UseFlagReturn` | Utilities for working with a specific flag |
|
|
62
|
+
| `count` | `ComputedRef<number>` | Number of items in the collection |
|
|
58
63
|
|
|
59
64
|
### `CollectionItem` object
|
|
60
65
|
|
|
@@ -66,18 +71,19 @@ Values for `multiple`:
|
|
|
66
71
|
| `properties` | `TProperties` | Object containing all computed properties for this item |
|
|
67
72
|
| `toggleFlag` | `(flag, forcedValue?) => void` | Method to toggle a flag on this item |
|
|
68
73
|
|
|
69
|
-
###
|
|
74
|
+
### UseFlagReturn object
|
|
70
75
|
|
|
71
|
-
| Name | Type
|
|
72
|
-
| ----------- |
|
|
73
|
-
| `items` | `ComputedRef<CollectionItem[]>`
|
|
74
|
-
| `ids` | `ComputedRef<TId[]>`
|
|
75
|
-
| `count` | `ComputedRef<number>`
|
|
76
|
-
| `areAllOn` | `ComputedRef<boolean>`
|
|
77
|
-
| `areSomeOn` | `ComputedRef<boolean>`
|
|
78
|
-
| `areNoneOn` | `ComputedRef<boolean>`
|
|
79
|
-
| `toggle` | `(id, forcedValue?) => void`
|
|
80
|
-
| `toggleAll` | `(forcedValue?) => void`
|
|
76
|
+
| Name | Type | Description |
|
|
77
|
+
| ----------- | ------------------------------------------- | ------------------------------------------------------ |
|
|
78
|
+
| `items` | `ComputedRef<CollectionItem[]>` | Array of items that have this flag set |
|
|
79
|
+
| `ids` | `ComputedRef<TId[]>` | Array of IDs of items that have this flag set |
|
|
80
|
+
| `count` | `ComputedRef<number>` | Number of items that have this flag set |
|
|
81
|
+
| `areAllOn` | `ComputedRef<boolean>` | Whether all items in the collection have this flag set |
|
|
82
|
+
| `areSomeOn` | `ComputedRef<boolean>` | Whether at least one item has this flag set |
|
|
83
|
+
| `areNoneOn` | `ComputedRef<boolean>` | Whether no items have this flag set |
|
|
84
|
+
| `toggle` | `(id, forcedValue?) => void` | Toggle this flag on a specific item |
|
|
85
|
+
| `toggleAll` | `(forcedValue?) => void` | Toggle this flag on all items in the collection |
|
|
86
|
+
| `useSubset` | `(filter: (item) => boolean) => Collection` | Creates a sub collection matching the filter |
|
|
81
87
|
|
|
82
88
|
## Flag Operations
|
|
83
89
|
|
|
@@ -136,10 +142,9 @@ const {
|
|
|
136
142
|
useSubset,
|
|
137
143
|
count,
|
|
138
144
|
} = useCollection(rawUsers, {
|
|
139
|
-
identifier: user => user.id,
|
|
140
145
|
flags: ['selected'],
|
|
141
146
|
properties: user => ({
|
|
142
|
-
fullName:
|
|
147
|
+
fullName: `${user.firstName} ${user.lastName} (${user.group})`,
|
|
143
148
|
}),
|
|
144
149
|
})
|
|
145
150
|
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Collection,
|
|
3
|
+
CollectionConfigProperties,
|
|
4
|
+
CollectionItem,
|
|
5
|
+
FlagRegistry,
|
|
6
|
+
GuessItemId,
|
|
7
|
+
UseFlagReturn,
|
|
8
|
+
} from '@core/packages/collection/types.ts'
|
|
2
9
|
import { useArrayFilter, useArrayMap } from '@vueuse/core'
|
|
3
10
|
import { computed, type ComputedRef } from 'vue'
|
|
4
11
|
|
|
5
|
-
export function createCollection<
|
|
6
|
-
TSource,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
export function createCollection<TSource, TFlag extends string, TProperties extends CollectionConfigProperties>(
|
|
13
|
+
items: ComputedRef<CollectionItem<TSource, TFlag, TProperties>[]>,
|
|
14
|
+
flagRegistry: FlagRegistry<TFlag>
|
|
15
|
+
): Collection<TSource, TFlag, TProperties> {
|
|
16
|
+
function useFlag(flag: TFlag): UseFlagReturn<TSource, TFlag, TProperties> {
|
|
17
|
+
flagRegistry.assertFlag(flag)
|
|
18
|
+
|
|
12
19
|
const flaggedItems = useArrayFilter(items, item => item.flags[flag])
|
|
13
20
|
|
|
14
21
|
const ids = useArrayMap(flaggedItems, item => item.id)
|
|
@@ -21,7 +28,7 @@ export function createCollection<
|
|
|
21
28
|
|
|
22
29
|
const areNoneOn = computed(() => count.value === 0)
|
|
23
30
|
|
|
24
|
-
function toggle(id:
|
|
31
|
+
function toggle(id: GuessItemId<TSource, TProperties>, forcedValue?: boolean) {
|
|
25
32
|
flagRegistry.toggleFlag(id, flag, forcedValue)
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -31,6 +38,12 @@ export function createCollection<
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
function useSubset(
|
|
42
|
+
filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
|
|
43
|
+
): Collection<TSource, TFlag, TProperties> {
|
|
44
|
+
return createCollection(useArrayFilter(flaggedItems, filter), flagRegistry)
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
return {
|
|
35
48
|
items: flaggedItems,
|
|
36
49
|
ids,
|
|
@@ -40,13 +53,14 @@ export function createCollection<
|
|
|
40
53
|
areNoneOn,
|
|
41
54
|
toggle,
|
|
42
55
|
toggleAll,
|
|
56
|
+
useSubset,
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
function useSubset(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return createCollection(
|
|
60
|
+
function useSubset(
|
|
61
|
+
filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
|
|
62
|
+
): Collection<TSource, TFlag, TProperties> {
|
|
63
|
+
return createCollection(useArrayFilter(items, filter), flagRegistry)
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
const count = computed(() => items.value.length)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { guessItemId } from '@core/packages/collection/guess-item-id.ts'
|
|
2
|
+
import type { CollectionConfigProperties, CollectionItem, FlagRegistry } from '@core/packages/collection/types.ts'
|
|
3
|
+
import { reactive } from 'vue'
|
|
4
|
+
|
|
5
|
+
export function createItem<TSource, TFlag extends string, TProperties extends CollectionConfigProperties>(
|
|
6
|
+
source: TSource,
|
|
7
|
+
getProperties: undefined | ((source: TSource) => TProperties),
|
|
8
|
+
flagRegistry: FlagRegistry<TFlag>
|
|
9
|
+
): CollectionItem<TSource, TFlag, TProperties> {
|
|
10
|
+
const properties = reactive(getProperties?.(source) ?? ({} as TProperties))
|
|
11
|
+
|
|
12
|
+
const id = guessItemId(source, properties)
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
source,
|
|
17
|
+
toggleFlag(flag: TFlag, forcedValue?: boolean) {
|
|
18
|
+
flagRegistry.toggleFlag(id, flag, forcedValue)
|
|
19
|
+
},
|
|
20
|
+
flags: new Proxy({} as Record<TFlag, boolean>, {
|
|
21
|
+
has(_, flag: TFlag) {
|
|
22
|
+
return flagRegistry.isFlagDefined(flag)
|
|
23
|
+
},
|
|
24
|
+
get(_, flag: TFlag) {
|
|
25
|
+
if (!flagRegistry.isFlagDefined(flag)) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return flagRegistry.isFlagged(id, flag)
|
|
30
|
+
},
|
|
31
|
+
set(_, flag: TFlag, value: boolean) {
|
|
32
|
+
flagRegistry.toggleFlag(id, flag, value)
|
|
33
|
+
|
|
34
|
+
return true
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
properties,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CollectionConfigProperties, GuessItemId } from '@core/packages/collection/types.ts'
|
|
2
|
+
|
|
3
|
+
function assertValidId(id: unknown): asserts id is PropertyKey {
|
|
4
|
+
const type = typeof id
|
|
5
|
+
|
|
6
|
+
if (!['string', 'number', 'symbol'].includes(type)) {
|
|
7
|
+
throw new TypeError(`Invalid ID type: ${type}. Expected string, number, or bigint.`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function guessItemId<TSource, TProperties extends CollectionConfigProperties>(
|
|
12
|
+
source: TSource,
|
|
13
|
+
properties: TProperties | undefined
|
|
14
|
+
) {
|
|
15
|
+
let id
|
|
16
|
+
|
|
17
|
+
if (typeof properties === 'object' && properties !== null && 'id' in properties) {
|
|
18
|
+
id = properties.id
|
|
19
|
+
} else if (typeof source === 'object' && source !== null && 'id' in source) {
|
|
20
|
+
id = source.id
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assertValidId(id)
|
|
24
|
+
|
|
25
|
+
return id as GuessItemId<TSource, TProperties>
|
|
26
|
+
}
|
|
@@ -1,29 +1,57 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComputedRef, Reactive } from 'vue'
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type CollectionConfigProperties = Record<string, unknown> & { id?: unknown }
|
|
4
4
|
|
|
5
|
-
export type
|
|
6
|
-
TSource,
|
|
7
|
-
TId extends PropertyKey,
|
|
8
|
-
TFlag extends string,
|
|
9
|
-
TProperties extends Record<string, unknown>,
|
|
10
|
-
> = {
|
|
11
|
-
identifier: (source: TSource) => TId
|
|
12
|
-
properties?: (source: TSource) => TProperties
|
|
13
|
-
flags?: FlagsConfig<TFlag>
|
|
14
|
-
}
|
|
5
|
+
export type CollectionConfigFlags<TFlag extends string> = TFlag[] | Record<TFlag, { multiple?: boolean }>
|
|
15
6
|
|
|
16
7
|
export type CollectionItem<
|
|
17
8
|
TSource,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
TProperties extends Record<string, any>,
|
|
9
|
+
TFlag extends string = never,
|
|
10
|
+
TProperties extends CollectionConfigProperties = Record<string, never>,
|
|
21
11
|
> = {
|
|
22
|
-
id:
|
|
12
|
+
id: GuessItemId<TSource, Reactive<TProperties>>
|
|
23
13
|
source: TSource
|
|
24
14
|
flags: Record<TFlag, boolean>
|
|
25
|
-
properties: TProperties
|
|
15
|
+
properties: Reactive<TProperties>
|
|
26
16
|
toggleFlag: (flag: TFlag, forcedValue?: boolean) => void
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
export type FlagRegistry<TFlag extends string> =
|
|
19
|
+
export type FlagRegistry<TFlag extends string> = {
|
|
20
|
+
isFlagged: (id: PropertyKey, flag: TFlag) => boolean
|
|
21
|
+
isFlagDefined: (flag: TFlag) => boolean
|
|
22
|
+
toggleFlag: (id: PropertyKey, flag: TFlag, forcedValue?: boolean) => void
|
|
23
|
+
clearFlag: (flag: TFlag) => void
|
|
24
|
+
isMultipleAllowed: (flag: TFlag) => boolean
|
|
25
|
+
assertFlag: (flag: TFlag) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type UseFlagReturn<TSource, TFlag extends string, TProperties extends CollectionConfigProperties> = {
|
|
29
|
+
items: ComputedRef<CollectionItem<TSource, TFlag, TProperties>[]>
|
|
30
|
+
ids: ComputedRef<GuessItemId<TSource, TProperties>[]>
|
|
31
|
+
count: ComputedRef<number>
|
|
32
|
+
areAllOn: ComputedRef<boolean>
|
|
33
|
+
areSomeOn: ComputedRef<boolean>
|
|
34
|
+
areNoneOn: ComputedRef<boolean>
|
|
35
|
+
toggle: (id: GuessItemId<TSource, TProperties>, forcedValue?: boolean) => void
|
|
36
|
+
toggleAll: (forcedValue?: boolean) => void
|
|
37
|
+
useSubset: (
|
|
38
|
+
filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
|
|
39
|
+
) => Collection<TSource, TFlag, TProperties>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Collection<TSource, TFlag extends string, TProperties extends CollectionConfigProperties> = {
|
|
43
|
+
items: ComputedRef<CollectionItem<TSource, TFlag, TProperties>[]>
|
|
44
|
+
count: ComputedRef<number>
|
|
45
|
+
useFlag: (flag: TFlag) => UseFlagReturn<TSource, TFlag, TProperties>
|
|
46
|
+
useSubset: (
|
|
47
|
+
filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
|
|
48
|
+
) => Collection<TSource, TFlag, TProperties>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type AssertId<TId> = TId extends PropertyKey ? TId : never
|
|
52
|
+
|
|
53
|
+
export type GuessItemId<TSource, TProperties> = TProperties extends { id: infer TId }
|
|
54
|
+
? AssertId<TId>
|
|
55
|
+
: TSource extends { id: infer TId }
|
|
56
|
+
? AssertId<TId>
|
|
57
|
+
: never
|
|
@@ -1,15 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCollection } from '@core/packages/collection/create-collection.ts'
|
|
2
|
+
import { createItem } from '@core/packages/collection/create-item.ts'
|
|
3
|
+
import type { Collection, CollectionConfigFlags, CollectionConfigProperties } from '@core/packages/collection/types.ts'
|
|
4
|
+
import { useFlagRegistry } from '@core/packages/collection/use-flag-registry.ts'
|
|
2
5
|
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
|
3
6
|
|
|
7
|
+
export function useCollection<
|
|
8
|
+
TSource extends { id: unknown },
|
|
9
|
+
TFlag extends string = never,
|
|
10
|
+
TProperties extends CollectionConfigProperties = { id?: unknown },
|
|
11
|
+
>(
|
|
12
|
+
sources: MaybeRefOrGetter<TSource[]>,
|
|
13
|
+
config?: {
|
|
14
|
+
flags?: CollectionConfigFlags<TFlag>
|
|
15
|
+
properties?: (source: TSource) => TProperties
|
|
16
|
+
}
|
|
17
|
+
): Collection<TSource, TFlag, TProperties>
|
|
18
|
+
|
|
19
|
+
export function useCollection<
|
|
20
|
+
TSource,
|
|
21
|
+
TFlag extends string = never,
|
|
22
|
+
TProperties extends CollectionConfigProperties & { id: unknown } = never,
|
|
23
|
+
>(
|
|
24
|
+
sources: MaybeRefOrGetter<TSource[]>,
|
|
25
|
+
config: {
|
|
26
|
+
flags?: CollectionConfigFlags<TFlag>
|
|
27
|
+
properties: (source: TSource) => TProperties
|
|
28
|
+
}
|
|
29
|
+
): Collection<TSource, TFlag, TProperties>
|
|
30
|
+
|
|
4
31
|
export function useCollection<
|
|
5
32
|
TSource,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
TFlag extends string = never,
|
|
34
|
+
TProperties extends CollectionConfigProperties = { id?: unknown },
|
|
35
|
+
>(
|
|
36
|
+
sources: MaybeRefOrGetter<TSource[]>,
|
|
37
|
+
config?: {
|
|
38
|
+
flags?: CollectionConfigFlags<TFlag>
|
|
39
|
+
properties?: (source: TSource) => TProperties
|
|
40
|
+
}
|
|
41
|
+
): Collection<TSource, TFlag, TProperties> {
|
|
42
|
+
const flagRegistry = useFlagRegistry(config?.flags)
|
|
11
43
|
|
|
12
|
-
const items = computed(() => toValue(sources).map(source =>
|
|
44
|
+
const items = computed(() => toValue(sources).map(source => createItem(source, config?.properties, flagRegistry)))
|
|
13
45
|
|
|
14
46
|
return createCollection(items, flagRegistry)
|
|
15
47
|
}
|
|
@@ -1,20 +1,32 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CollectionConfigFlags, FlagRegistry } from '@core/packages/collection/types.ts'
|
|
2
2
|
import { reactive } from 'vue'
|
|
3
3
|
|
|
4
|
-
export function useFlagRegistry<TFlag extends string>(
|
|
5
|
-
|
|
4
|
+
export function useFlagRegistry<TFlag extends string>(
|
|
5
|
+
config: CollectionConfigFlags<TFlag> = [] as TFlag[]
|
|
6
|
+
): FlagRegistry<TFlag> {
|
|
7
|
+
const registry = reactive(new Map<TFlag, Set<PropertyKey>>())
|
|
6
8
|
|
|
7
|
-
const flags = Array.isArray(
|
|
9
|
+
const flags = Array.isArray(config) ? Object.fromEntries(config.map(flag => [flag, { multiple: true }])) : config
|
|
8
10
|
|
|
9
11
|
function isFlagDefined(flag: TFlag) {
|
|
10
|
-
return flags
|
|
12
|
+
return Object.prototype.hasOwnProperty.call(flags, flag)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertFlag(flag: TFlag) {
|
|
16
|
+
if (!isFlagDefined(flag)) {
|
|
17
|
+
throw new Error(`Flag "${flag}" is not defined.`)
|
|
18
|
+
}
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
function isFlagged(id: PropertyKey, flag: TFlag) {
|
|
22
|
+
assertFlag(flag)
|
|
23
|
+
|
|
14
24
|
return registry.get(flag)?.has(id) ?? false
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
function toggleFlag(id: PropertyKey, flag: TFlag, forcedValue = !isFlagged(id, flag)) {
|
|
28
|
+
assertFlag(flag)
|
|
29
|
+
|
|
18
30
|
if (!registry.has(flag)) {
|
|
19
31
|
registry.set(flag, new Set())
|
|
20
32
|
}
|
|
@@ -30,10 +42,14 @@ export function useFlagRegistry<TFlag extends string>(_flags: FlagsConfig<TFlag>
|
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
function clearFlag(flag: TFlag) {
|
|
45
|
+
assertFlag(flag)
|
|
46
|
+
|
|
33
47
|
registry.set(flag, new Set())
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
function isMultipleAllowed(flag: TFlag) {
|
|
51
|
+
assertFlag(flag)
|
|
52
|
+
|
|
37
53
|
return flags[flag]?.multiple ?? false
|
|
38
54
|
}
|
|
39
55
|
|
|
@@ -43,5 +59,6 @@ export function useFlagRegistry<TFlag extends string>(_flags: FlagsConfig<TFlag>
|
|
|
43
59
|
toggleFlag,
|
|
44
60
|
clearFlag,
|
|
45
61
|
isMultipleAllowed,
|
|
62
|
+
assertFlag,
|
|
46
63
|
}
|
|
47
64
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# `useFormSelect` composable
|
|
2
|
+
|
|
3
|
+
This composable manages a collection of filterable form options to be used with `VtsSelect` component.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const { searchTerm, options, selectedOptions, selectedValues } = useFormSelect(sources, {
|
|
9
|
+
properties: source => ({
|
|
10
|
+
value: source.id, // optional if source has a `value` or `ìd` property
|
|
11
|
+
label: source.name, // optional if source has a `label` property
|
|
12
|
+
searchableTerm: [source.name, source.code], // optional, defaults to the label
|
|
13
|
+
disabled: source.state == 'offline', // optional, defaults to false
|
|
14
|
+
}),
|
|
15
|
+
multiple: false,
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Parameters
|
|
20
|
+
|
|
21
|
+
| | Required | Type | Default | Description |
|
|
22
|
+
| --------- | :------: | ----------------------------- | ------- | --------------------------------------------------------- |
|
|
23
|
+
| `sources` | ✓ | `MaybeRefOrGetter<TSource[]>` | | Array of source objects to be converted into form options |
|
|
24
|
+
| `config` | ✓ | (see below) | | Configuration |
|
|
25
|
+
|
|
26
|
+
### `config` object
|
|
27
|
+
|
|
28
|
+
| | Required | Type | Default | Description |
|
|
29
|
+
| --------------------------- | :------: | ---------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------- |
|
|
30
|
+
| `properties` | ~ | Record<string, unknown> | | Object containing custom properties for each option |
|
|
31
|
+
| `properties.value` | ~ | `TValue` | | A unique value for each option. Required if `TSource` doesn't have an `id` or `value` |
|
|
32
|
+
| `properties.label` | ~ | `string` | | A human-readable label for each option. Required if `TSource` doesn't have a `label` |
|
|
33
|
+
| `properties.searchableTerm` | | `MaybeArray<string>` | Same as `label` | Searchable term(s) for each option for filtering |
|
|
34
|
+
| `properties.disabled` | | `boolean` | `false` | Determines if an option should be disabled |
|
|
35
|
+
| `multiple` | | `boolean` | `false` | Whether multiple options can be selected simultaneously |
|
|
36
|
+
| `selectedLabel` | | `(count: number, labels: string[]) => string \| undefined` | | Function to format the label for selected options. Default label is `labels.join(', ')` |
|
|
37
|
+
|
|
38
|
+
## Return Value
|
|
39
|
+
|
|
40
|
+
| | Type | Description |
|
|
41
|
+
| ----------------- | --------------------------- | ---------------------------------------------------- |
|
|
42
|
+
| `searchTerm` | `Ref<string>` | Reactive reference to control the search/filter term |
|
|
43
|
+
| `allOptions` | `ComputedRef<FormOption[]>` | All options, regardless of search filtering |
|
|
44
|
+
| `options` | `ComputedRef<FormOption[]>` | Options filtered by the current search term |
|
|
45
|
+
| `selectedOptions` | `ComputedRef<FormOption[]>` | Options that are currently selected |
|
|
46
|
+
| `selectedValues` | `ComputedRef<TValue[]>` | Values of options that are currently selected |
|
|
47
|
+
| `selectedLabel` | `ComputedRef<string>` | Label for the selected options |
|
|
48
|
+
|
|
49
|
+
## `FormOption` object
|
|
50
|
+
|
|
51
|
+
Each item in the `options` array is a `FormOption` object with these properties:
|
|
52
|
+
|
|
53
|
+
| | Type | Description |
|
|
54
|
+
| ----------------------- | ---------------------------------------- | ------------------------------------------------------ |
|
|
55
|
+
| `id` | `TValue` | Unique identifier for the option (from `getValue`) |
|
|
56
|
+
| `source` | `TSource` | The original source object |
|
|
57
|
+
| `flags` | `{ selected: boolean, active: boolean }` | State flags for selection and keyboard navigation |
|
|
58
|
+
| `properties` | `object` | Computed properties for the option |
|
|
59
|
+
| ⤷ `label` | `string` | Human-readable option label (from `getLabel`) |
|
|
60
|
+
| ⤷ `matching` | `boolean` | Whether the option matches the current search term |
|
|
61
|
+
| ⤷ `disabled` | `boolean` | Whether the option is disabled |
|
|
62
|
+
| ⤷ `multiple` | `boolean` | Whether multiple selection is enabled |
|
|
63
|
+
| ⤷ `...` | `any` | Any other custom property defined in the config |
|
|
64
|
+
| `toggleFlag` | `(flag, forcedValue?) => void` | Method to toggle a flag (like 'selected') on this item |
|
|
65
|
+
|
|
66
|
+
## Example: Basic usage with `VtsSelect`
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<template>
|
|
70
|
+
<VtsSelect :options :selected-label />
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script lang="ts" setup>
|
|
74
|
+
const { options, selectedLabel, selectedValues } = useFormOptions(vms, {
|
|
75
|
+
properties: vm => ({
|
|
76
|
+
label: vm.name_label,
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Example: Searchable + multi-select + custom selected label
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<template>
|
|
86
|
+
<VtsSelect v-model:search="searchTerm" :options :selected-label />
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script lang="ts" setup>
|
|
90
|
+
const { options, searchTerm, selectedValues, selectedLabel } = useFormOptions(vms, {
|
|
91
|
+
properties: vm => ({ label: vm.name_label }),
|
|
92
|
+
multiple: true,
|
|
93
|
+
selectedlabel: count => (count > 3 ? `${count} VMs selected` : undefined), // Keep the default label if less than 3
|
|
94
|
+
})
|
|
95
|
+
</script>
|
|
96
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CollectionConfigProperties, CollectionItem } from '@core/packages/collection'
|
|
2
|
+
import type { MaybeArray } from '@core/types/utility.type.ts'
|
|
3
|
+
import type { ComputedRef, InjectionKey, Reactive, WritableComputedRef } from 'vue'
|
|
4
|
+
|
|
5
|
+
export type FormSelectBaseConfig = {
|
|
6
|
+
multiple?: boolean
|
|
7
|
+
selectedLabel?: (count: number, labels: string[]) => string | undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type FormSelectBaseProperties = Record<string, unknown> & {
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
searchableTerm?: MaybeArray<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FormOptionValue = string | number
|
|
16
|
+
|
|
17
|
+
export type FormOptionProperties<TValue extends FormOptionValue> = {
|
|
18
|
+
id: TValue
|
|
19
|
+
label: string
|
|
20
|
+
multiple: boolean
|
|
21
|
+
disabled: boolean
|
|
22
|
+
matching: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FormOption<
|
|
26
|
+
TSource = unknown,
|
|
27
|
+
TValue extends FormOptionValue = FormOptionValue,
|
|
28
|
+
TProperties extends CollectionConfigProperties = CollectionConfigProperties,
|
|
29
|
+
> = CollectionItem<TSource, 'active' | 'selected', TProperties & FormOptionProperties<TValue>>
|
|
30
|
+
|
|
31
|
+
export type UseFormSelectReturn<
|
|
32
|
+
TSource,
|
|
33
|
+
TValue extends FormOptionValue,
|
|
34
|
+
TProperties extends CollectionConfigProperties,
|
|
35
|
+
> = {
|
|
36
|
+
searchTerm: WritableComputedRef<string>
|
|
37
|
+
allOptions: ComputedRef<FormOption<TSource, TValue, TProperties>[]>
|
|
38
|
+
options: ComputedRef<FormOption<TSource, TValue, TProperties>[]>
|
|
39
|
+
selectedOptions: ComputedRef<FormOption<TSource, TValue, TProperties>[]>
|
|
40
|
+
selectedValues: ComputedRef<TValue[]>
|
|
41
|
+
selectedLabel: ComputedRef<string>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type FormOptionIndex =
|
|
45
|
+
| number
|
|
46
|
+
| 'previous'
|
|
47
|
+
| 'next'
|
|
48
|
+
| 'previous-page'
|
|
49
|
+
| 'next-page'
|
|
50
|
+
| 'first'
|
|
51
|
+
| 'last'
|
|
52
|
+
| 'selected'
|
|
53
|
+
|
|
54
|
+
export enum FORM_SELECT_HANDLED_KEY {
|
|
55
|
+
DOWN = 'ArrowDown',
|
|
56
|
+
UP = 'ArrowUp',
|
|
57
|
+
LEFT = 'ArrowLeft',
|
|
58
|
+
RIGHT = 'ArrowRight',
|
|
59
|
+
ENTER = 'Enter',
|
|
60
|
+
SPACE = ' ',
|
|
61
|
+
ESCAPE = 'Escape',
|
|
62
|
+
HOME = 'Home',
|
|
63
|
+
END = 'End',
|
|
64
|
+
TAB = 'Tab',
|
|
65
|
+
PAGE_DOWN = 'PageDown',
|
|
66
|
+
PAGE_UP = 'PageUp',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type FormSelectController = Reactive<{
|
|
70
|
+
isNavigatingWithKeyboard: boolean
|
|
71
|
+
closeDropdown(keepFocus: boolean): void
|
|
72
|
+
focusSearchOrTrigger(): void
|
|
73
|
+
}>
|
|
74
|
+
|
|
75
|
+
export const IK_FORM_SELECT_CONTROLLER = Symbol('IK_FORM_SELECT_CONTROLLER') as InjectionKey<FormSelectController>
|