@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.
Files changed (41) hide show
  1. package/lib/components/backdrop/VtsBackdrop.vue +1 -1
  2. package/lib/components/column/VtsColumn.vue +21 -0
  3. package/lib/components/columns/VtsColumns.vue +38 -0
  4. package/lib/components/copy-button/VtsCopyButton.vue +29 -0
  5. package/lib/components/enabled-state/VtsEnabledState.vue +23 -0
  6. package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
  7. package/lib/components/quick-info-column/VtsQuickInfoColumn.vue +1 -1
  8. package/lib/components/quick-info-row/VtsQuickInfoRow.vue +26 -7
  9. package/lib/components/relative-time/VtsRelativeTime.vue +18 -0
  10. package/lib/components/select/VtsOption.vue +24 -0
  11. package/lib/components/select/VtsSelect.vue +96 -0
  12. package/lib/components/state-hero/VtsLoadingHero.vue +45 -4
  13. package/lib/components/tree/VtsTreeItem.vue +11 -1
  14. package/lib/components/ui/dropdown/UiDropdownList.vue +10 -2
  15. package/lib/components/ui/head-bar/UiHeadBar.vue +2 -2
  16. package/lib/composables/relative-time.composable.ts +1 -1
  17. package/lib/locales/cs.json +0 -1
  18. package/lib/locales/en.json +65 -1
  19. package/lib/locales/es.json +0 -1
  20. package/lib/locales/fa.json +0 -1
  21. package/lib/locales/fr.json +67 -3
  22. package/lib/locales/nl.json +0 -1
  23. package/lib/locales/sv.json +0 -1
  24. package/lib/packages/collection/README.md +38 -33
  25. package/lib/packages/collection/create-collection.ts +27 -13
  26. package/lib/packages/collection/create-item.ts +39 -0
  27. package/lib/packages/collection/guess-item-id.ts +26 -0
  28. package/lib/packages/collection/index.ts +0 -3
  29. package/lib/packages/collection/types.ts +46 -18
  30. package/lib/packages/collection/use-collection.ts +39 -7
  31. package/lib/packages/collection/use-flag-registry.ts +22 -5
  32. package/lib/packages/form-select/README.md +96 -0
  33. package/lib/packages/form-select/index.ts +2 -0
  34. package/lib/packages/form-select/types.ts +75 -0
  35. package/lib/packages/form-select/use-form-option-controller.ts +50 -0
  36. package/lib/packages/form-select/use-form-select-controller.ts +205 -0
  37. package/lib/packages/form-select/use-form-select-keyboard-navigation.ts +157 -0
  38. package/lib/packages/form-select/use-form-select.ts +193 -0
  39. package/lib/stores/sidebar.store.ts +14 -1
  40. package/package.json +1 -1
  41. 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
- isAvailable: computed(() => source.status === 'available'),
13
- fullName: computed(() => `${source.firstName} ${source.lastName}`),
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**: Computed values derived from the source object
22
+ - **Properties**: Additional custom values
23
23
 
24
24
  ## `useCollection` parameters
25
25
 
26
- | Name | Type | Required | Description |
27
- | --------- | --------------------------------- | :------: | ---------------------------------------------------- |
28
- | `sources` | `MaybeRefOrGetter<TSource[]>` | ✓ | Array of source objects for the collection |
29
- | `options` | `CollectionOptions<TSource, TId>` | | Configuration options for the collection (see below) |
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 | Required | Description |
34
- | ------------ | -------------------------------------------------- | :------: | -------------------------------------------------------- |
35
- | `identifier` | `(source: TSource) => TId` || Function to extract a unique identifier from each source |
36
- | `flags` | `FlagsConfig<TFlag>` | | Flags that can be applied to items in the collection |
37
- | `properties` | `(source: TSource) => Record<string, ComputedRef>` | | Function that returns computed properties for each item |
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 have the same flag.
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 new collection that's a subset of the original |
56
- | `useFlag` | `(flag: TFlag) => UseFlag` | Utilities for working with a specific flag |
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
- ### Return value of `useFlag(flag)`
74
+ ### UseFlagReturn object
70
75
 
71
- | Name | Type | Description |
72
- | ----------- | ------------------------------- | ------------------------------------------------------ |
73
- | `items` | `ComputedRef<CollectionItem[]>` | Array of items that have this flag set |
74
- | `ids` | `ComputedRef<TId[]>` | Array of IDs of items that have this flag set |
75
- | `count` | `ComputedRef<number>` | Number of items that have this flag set |
76
- | `areAllOn` | `ComputedRef<boolean>` | Whether all items in the collection have this flag set |
77
- | `areSomeOn` | `ComputedRef<boolean>` | Whether at least one item has this flag set |
78
- | `areNoneOn` | `ComputedRef<boolean>` | Whether no items have this flag set |
79
- | `toggle` | `(id, forcedValue?) => void` | Toggle this flag on a specific item |
80
- | `toggleAll` | `(forcedValue?) => void` | Toggle this flag on all items in the collection |
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: computed(() => `${user.firstName} ${user.lastName} (${user.group})`),
147
+ fullName: `${user.firstName} ${user.lastName} (${user.group})`,
143
148
  }),
144
149
  })
145
150
 
@@ -1,14 +1,21 @@
1
- import type { CollectionItem, FlagRegistry } from '@core/packages/collection'
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
- TId extends PropertyKey,
8
- TFlag extends string,
9
- TProperties extends Record<string, any>,
10
- >(items: ComputedRef<CollectionItem<TSource, TId, TFlag, TProperties>[]>, flagRegistry: FlagRegistry<TFlag>) {
11
- function useFlag(flag: TFlag) {
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: TId, forcedValue?: boolean) {
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(filter: (item: CollectionItem<TSource, TId, TFlag, TProperties>) => boolean) {
47
- const filteredItems = useArrayFilter(items, item => filter(item))
48
-
49
- return createCollection(filteredItems, flagRegistry)
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,5 +1,2 @@
1
- export * from './build-item.ts'
2
- export * from './create-collection.ts'
3
1
  export * from './types.ts'
4
2
  export * from './use-collection.ts'
5
- export * from './use-flag-registry.ts'
@@ -1,29 +1,57 @@
1
- import type { useFlagRegistry } from '@core/packages/collection'
1
+ import type { ComputedRef, Reactive } from 'vue'
2
2
 
3
- export type FlagsConfig<TFlag extends string> = TFlag[] | { [K in TFlag]: { multiple?: boolean } }
3
+ export type CollectionConfigProperties = Record<string, unknown> & { id?: unknown }
4
4
 
5
- export type CollectionOptions<
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
- TId extends PropertyKey,
19
- TFlag extends string,
20
- TProperties extends Record<string, any>,
9
+ TFlag extends string = never,
10
+ TProperties extends CollectionConfigProperties = Record<string, never>,
21
11
  > = {
22
- id: TId
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> = ReturnType<typeof useFlagRegistry<TFlag>>
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 { buildItem, type CollectionOptions, createCollection, useFlagRegistry } from '@core/packages/collection'
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
- TId extends PropertyKey,
7
- TFlag extends string,
8
- TProperties extends Record<string, unknown>,
9
- >(sources: MaybeRefOrGetter<TSource[]>, options: CollectionOptions<TSource, TId, TFlag, TProperties>) {
10
- const flagRegistry = useFlagRegistry<TFlag>(options.flags)
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 => buildItem(source, options, flagRegistry)))
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 { FlagsConfig } from '@core/packages/collection/types.ts'
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>(_flags: FlagsConfig<TFlag> = []) {
5
- const registry = reactive(new Map()) as Map<TFlag, Set<PropertyKey> | undefined>
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(_flags) ? Object.fromEntries(_flags.map(flag => [flag, { multiple: true }])) : _flags
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[flag] !== undefined
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
+ | &nbsp;⤷&nbsp;`label` | `string` | Human-readable option label (from `getLabel`) |
60
+ | &nbsp;⤷&nbsp;`matching` | `boolean` | Whether the option matches the current search term |
61
+ | &nbsp;⤷&nbsp;`disabled` | `boolean` | Whether the option is disabled |
62
+ | &nbsp;⤷&nbsp;`multiple` | `boolean` | Whether multiple selection is enabled |
63
+ | &nbsp;⤷&nbsp;`...` | `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,2 @@
1
+ export * from './types.ts'
2
+ export * from './use-form-select.ts'
@@ -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>