@xen-orchestra/web-core 0.20.1 → 0.22.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 (116) hide show
  1. package/lib/assets/css/_typography.pcss +6 -0
  2. package/lib/assets/no-selection-old.svg +70 -0
  3. package/lib/assets/no-selection.svg +85 -70
  4. package/lib/components/backup-state/VtsBackupState.vue +20 -17
  5. package/lib/components/card/VtsCardRowKeyValue.vue +4 -0
  6. package/lib/components/cell-object/VtsCellObject.vue +4 -1
  7. package/lib/components/console/VtsActionsConsole.vue +7 -4
  8. package/lib/components/console/VtsClipboardConsole.vue +9 -6
  9. package/lib/components/copy-button/VtsCopyButton.vue +9 -15
  10. package/lib/components/dropdown/DropdownTitle.vue +5 -2
  11. package/lib/components/icon/NewVtsIcon.vue +49 -0
  12. package/lib/components/input-group/VtsInputGroup.vue +41 -0
  13. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  14. package/lib/components/layout/VtsLayoutSidebar.vue +6 -3
  15. package/lib/components/linear-chart/VtsLinearChart.vue +4 -0
  16. package/lib/components/object-icon/VtsObjectIcon.vue +22 -0
  17. package/lib/components/quick-info-card/VtsQuickInfoCard.vue +4 -1
  18. package/lib/components/select/VtsOption.vue +10 -6
  19. package/lib/components/select/VtsSelect.vue +74 -50
  20. package/lib/components/state-hero/VtsAllDoneHero.vue +4 -1
  21. package/lib/components/state-hero/VtsAllGoodHero.vue +4 -1
  22. package/lib/components/state-hero/VtsComingSoonHero.vue +4 -1
  23. package/lib/components/state-hero/VtsErrorNoDataHero.vue +4 -1
  24. package/lib/components/state-hero/VtsLoadingHero.vue +4 -1
  25. package/lib/components/state-hero/VtsNoDataHero.vue +4 -1
  26. package/lib/components/state-hero/VtsNoSelectionHero.vue +4 -1
  27. package/lib/components/state-hero/VtsObjectNotFoundHero.vue +4 -1
  28. package/lib/components/state-hero/VtsOfflineHero.vue +4 -1
  29. package/lib/components/state-hero/VtsPageNotFoundHero.vue +4 -1
  30. package/lib/components/table/ColumnTitle.vue +2 -2
  31. package/lib/components/task/VtsQuickTaskButton.vue +4 -1
  32. package/lib/components/task/VtsQuickTaskList.vue +5 -2
  33. package/lib/components/task/VtsQuickTaskTabBar.vue +8 -5
  34. package/lib/components/ui/card-numbers/UiCardNumbers.vue +4 -1
  35. package/lib/components/ui/character-limit/UiCharacterLimit.vue +4 -1
  36. package/lib/components/ui/input/UiInput.vue +2 -2
  37. package/lib/components/ui/label/UiLabel.vue +4 -1
  38. package/lib/components/ui/panel/UiPanel.vue +16 -3
  39. package/lib/components/ui/progress-bar/UiProgressBar.vue +5 -2
  40. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +9 -6
  41. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +6 -3
  42. package/lib/components/ui/quoteCode/UiQuoteCode.vue +104 -0
  43. package/lib/components/ui/stacked-bar/StackedBarSegment.vue +4 -1
  44. package/lib/components/ui/table-pagination/UiTablePagination.vue +6 -3
  45. package/lib/components/ui/text-area/UiTextarea.vue +4 -1
  46. package/lib/components/ui/top-bottom-table/UiTopBottomTable.vue +6 -3
  47. package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +4 -1
  48. package/lib/composables/local-time-ago.composable.ts +53 -0
  49. package/lib/composables/locale-time-ago.composable.ts +53 -0
  50. package/lib/icons/fa-icons.ts +164 -0
  51. package/lib/icons/index.ts +15 -0
  52. package/lib/icons/legacy-icons.ts +80 -0
  53. package/lib/icons/object-icons.ts +187 -0
  54. package/lib/layouts/CoreLayout.vue +7 -3
  55. package/lib/locales/cs.json +0 -1
  56. package/lib/locales/de.json +1 -1
  57. package/lib/locales/en.json +40 -4
  58. package/lib/locales/es.json +1 -1
  59. package/lib/locales/fr.json +39 -3
  60. package/lib/locales/it.json +1 -1
  61. package/lib/locales/nl.json +1 -1
  62. package/lib/locales/ru.json +1 -1
  63. package/lib/locales/sv.json +1 -2
  64. package/lib/packages/collection/README.md +23 -18
  65. package/lib/packages/collection/create-collection.ts +22 -21
  66. package/lib/packages/collection/create-item.ts +21 -20
  67. package/lib/packages/collection/create-use-subset.ts +23 -0
  68. package/lib/packages/collection/guess-item-id.ts +26 -16
  69. package/lib/packages/collection/index.ts +4 -0
  70. package/lib/packages/collection/types.ts +65 -37
  71. package/lib/packages/collection/use-collection.ts +68 -18
  72. package/lib/packages/collection/use-flag-registry.ts +38 -17
  73. package/lib/packages/form-select/guess-label.ts +45 -0
  74. package/lib/packages/form-select/guess-value.ts +23 -0
  75. package/lib/packages/form-select/index.ts +6 -0
  76. package/lib/packages/form-select/normalize-search-term.ts +11 -0
  77. package/lib/packages/form-select/types.ts +90 -42
  78. package/lib/packages/form-select/use-form-option-controller.ts +7 -3
  79. package/lib/packages/form-select/use-form-select-controller.ts +38 -27
  80. package/lib/packages/form-select/use-form-select-keyboard-navigation.ts +1 -1
  81. package/lib/packages/form-select/use-form-select.ts +308 -130
  82. package/lib/packages/icon/DisplayIcon.vue +25 -0
  83. package/lib/packages/icon/DisplayIconAny.vue +16 -0
  84. package/lib/packages/icon/DisplayIconSingle.vue +35 -0
  85. package/lib/packages/icon/DisplayIconStack.vue +34 -0
  86. package/lib/packages/icon/README.md +286 -0
  87. package/lib/packages/icon/create-icon-bindings.ts +27 -0
  88. package/lib/packages/icon/define-icon-pack.ts +23 -0
  89. package/lib/packages/icon/define-icon-single.ts +17 -0
  90. package/lib/packages/icon/define-icon-stack.ts +20 -0
  91. package/lib/packages/icon/define-icon.ts +40 -0
  92. package/lib/packages/icon/generate-icon-variants.ts +17 -0
  93. package/lib/packages/icon/index.ts +8 -0
  94. package/lib/packages/icon/is-icon-stack.ts +5 -0
  95. package/lib/packages/icon/merge-icons.ts +25 -0
  96. package/lib/packages/icon/merge-transforms.ts +12 -0
  97. package/lib/packages/icon/normalize-icon.ts +25 -0
  98. package/lib/packages/icon/to-tuple.ts +7 -0
  99. package/lib/packages/icon/types.ts +72 -0
  100. package/lib/packages/job/README.md +2 -2
  101. package/lib/packages/mapper/README.md +166 -0
  102. package/lib/packages/mapper/convert-to-map.ts +5 -0
  103. package/lib/packages/mapper/create-mapper.ts +30 -0
  104. package/lib/packages/mapper/index.ts +4 -0
  105. package/lib/packages/mapper/types.ts +1 -0
  106. package/lib/packages/mapper/use-mapper.ts +31 -0
  107. package/lib/stores/sidebar.store.ts +1 -1
  108. package/lib/types/chart.ts +2 -2
  109. package/lib/types/utility.type.ts +9 -0
  110. package/lib/utils/object.util.ts +16 -0
  111. package/lib/utils/size.util.ts +14 -3
  112. package/package.json +2 -1
  113. package/lib/assets/zoom.svg +0 -85
  114. package/lib/components/backup-item/VtsBackupItem.vue +0 -47
  115. package/lib/composables/mapper.composable.md +0 -74
  116. package/lib/composables/mapper.composable.ts +0 -18
@@ -1,19 +1,23 @@
1
+ import { createUseSubset } from '@core/packages/collection/create-use-subset.ts'
1
2
  import type {
2
3
  Collection,
3
- CollectionConfigProperties,
4
4
  CollectionItem,
5
+ CollectionItemId,
6
+ CollectionItemProperties,
5
7
  FlagRegistry,
6
- GuessItemId,
7
8
  UseFlagReturn,
8
- } from '@core/packages/collection/types.ts'
9
+ } from './types.ts'
9
10
  import { useArrayFilter, useArrayMap } from '@vueuse/core'
10
11
  import { computed, type ComputedRef } from 'vue'
11
12
 
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> {
13
+ export function createCollection<
14
+ TSource,
15
+ TFlag extends string,
16
+ TProperties extends CollectionItemProperties,
17
+ TId extends CollectionItemId,
18
+ $TItem extends CollectionItem<TSource, TFlag, TProperties, TId>,
19
+ >(items: ComputedRef<$TItem[]>, flagRegistry: FlagRegistry<TId, TFlag>): Collection<TSource, TFlag, TProperties, TId> {
20
+ function useFlag(flag: TFlag): UseFlagReturn<TSource, TFlag, TProperties, TId> {
17
21
  flagRegistry.assertFlag(flag)
18
22
 
19
23
  const flaggedItems = useArrayFilter(items, item => item.flags[flag])
@@ -28,21 +32,17 @@ export function createCollection<TSource, TFlag extends string, TProperties exte
28
32
 
29
33
  const areNoneOn = computed(() => count.value === 0)
30
34
 
31
- function toggle(id: GuessItemId<TSource, TProperties>, forcedValue?: boolean) {
32
- flagRegistry.toggleFlag(id, flag, forcedValue)
35
+ function toggle(id: TId, shouldBeFlagged?: boolean) {
36
+ flagRegistry.toggleFlag(id, flag, shouldBeFlagged)
33
37
  }
34
38
 
35
- function toggleAll(forcedValue = !areAllOn.value) {
39
+ function toggleAll(shouldBeFlagged = !areAllOn.value) {
36
40
  for (const item of items.value) {
37
- flagRegistry.toggleFlag(item.id, flag, forcedValue)
41
+ flagRegistry.toggleFlag(item.id, flag, shouldBeFlagged)
38
42
  }
39
43
  }
40
44
 
41
- function useSubset(
42
- filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
43
- ): Collection<TSource, TFlag, TProperties> {
44
- return createCollection(useArrayFilter(flaggedItems, filter), flagRegistry)
45
- }
45
+ const useSubset = createUseSubset<TSource, TFlag, TProperties, TId>(flaggedItems, flagRegistry)
46
46
 
47
47
  return {
48
48
  items: flaggedItems,
@@ -57,18 +57,19 @@ export function createCollection<TSource, TFlag extends string, TProperties exte
57
57
  }
58
58
  }
59
59
 
60
- function useSubset(
61
- filter: (item: CollectionItem<TSource, TFlag, TProperties>) => boolean
62
- ): Collection<TSource, TFlag, TProperties> {
63
- return createCollection(useArrayFilter(items, filter), flagRegistry)
60
+ function toggleFlag(id: TId, flag: TFlag, shouldBeFlagged?: boolean) {
61
+ flagRegistry.toggleFlag(id, flag, shouldBeFlagged)
64
62
  }
65
63
 
64
+ const useSubset = createUseSubset<TSource, TFlag, TProperties, TId>(items, flagRegistry)
65
+
66
66
  const count = computed(() => items.value.length)
67
67
 
68
68
  return {
69
69
  items,
70
70
  count,
71
71
  useFlag,
72
+ toggleFlag,
72
73
  useSubset,
73
74
  }
74
75
  }
@@ -1,39 +1,40 @@
1
- import { guessItemId } from '@core/packages/collection/guess-item-id.ts'
2
- import type { CollectionConfigProperties, CollectionItem, FlagRegistry } from '@core/packages/collection/types.ts'
1
+ import type { CollectionItem, CollectionItemId, CollectionItemProperties, FlagRegistry } from './types.ts'
3
2
  import { reactive } from 'vue'
4
3
 
5
- export function createItem<TSource, TFlag extends string, TProperties extends CollectionConfigProperties>(
4
+ export function createItem<
5
+ TSource,
6
+ TFlag extends string,
7
+ TProperties extends CollectionItemProperties,
8
+ TId extends CollectionItemId,
9
+ >(
10
+ id: TId,
6
11
  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
-
12
+ properties: TProperties,
13
+ flagRegistry: FlagRegistry<TId, TFlag>
14
+ ): CollectionItem<TSource, TFlag, TProperties, TId> {
14
15
  return {
15
16
  id,
16
17
  source,
17
- toggleFlag(flag: TFlag, forcedValue?: boolean) {
18
- flagRegistry.toggleFlag(id, flag, forcedValue)
18
+ toggleFlag(flag: TFlag, shouldBeFlagged?: boolean) {
19
+ flagRegistry.toggleFlag(id, flag, shouldBeFlagged)
19
20
  },
20
21
  flags: new Proxy({} as Record<TFlag, boolean>, {
21
- has(_, flag: TFlag) {
22
- return flagRegistry.isFlagDefined(flag)
22
+ has(_, flag) {
23
+ return flagRegistry.isFlagDefined(flag as TFlag)
23
24
  },
24
- get(_, flag: TFlag) {
25
- if (!flagRegistry.isFlagDefined(flag)) {
25
+ get(_, flag) {
26
+ if (typeof flag === 'symbol' || flag.startsWith('__v_')) {
26
27
  return undefined
27
28
  }
28
29
 
29
- return flagRegistry.isFlagged(id, flag)
30
+ return flagRegistry.isFlagged(id, flag as TFlag)
30
31
  },
31
- set(_, flag: TFlag, value: boolean) {
32
- flagRegistry.toggleFlag(id, flag, value)
32
+ set(_, flag, value: boolean) {
33
+ flagRegistry.toggleFlag(id, flag as TFlag, value)
33
34
 
34
35
  return true
35
36
  },
36
37
  }),
37
- properties,
38
+ properties: reactive(properties),
38
39
  }
39
40
  }
@@ -0,0 +1,23 @@
1
+ import { createCollection } from '@core/packages/collection/create-collection.ts'
2
+ import type {
3
+ Collection,
4
+ CollectionItem,
5
+ CollectionItemId,
6
+ CollectionItemProperties,
7
+ FlagRegistry,
8
+ } from '@core/packages/collection/types.ts'
9
+ import type { ArrayFilterPredicate } from '@core/types/utility.type.ts'
10
+ import { useArrayFilter } from '@vueuse/core'
11
+ import type { ComputedRef } from 'vue'
12
+
13
+ export function createUseSubset<
14
+ TSource,
15
+ TFlag extends string,
16
+ TProperties extends CollectionItemProperties,
17
+ TId extends CollectionItemId,
18
+ $TItem extends CollectionItem<TSource, TFlag, TProperties, TId> = CollectionItem<TSource, TFlag, TProperties, TId>,
19
+ >(items: ComputedRef<$TItem[]>, flagRegistry: FlagRegistry<TId, TFlag>) {
20
+ return function useSubset(filter: ArrayFilterPredicate<$TItem>): Collection<TSource, TFlag, TProperties, TId> {
21
+ return createCollection(useArrayFilter(items, filter), flagRegistry)
22
+ }
23
+ }
@@ -1,26 +1,36 @@
1
- import type { CollectionConfigProperties, GuessItemId } from '@core/packages/collection/types.ts'
1
+ import { hasObjectProperty } from '@core/utils/object.util.ts'
2
+ import type { CollectionItemId, GetItemId } from './types.ts'
2
3
 
3
- function assertValidId(id: unknown): asserts id is PropertyKey {
4
- const type = typeof id
4
+ export function guessItemId<TSource>(source: TSource, getter: GetItemId<TSource>): CollectionItemId {
5
+ const id = extractItemId(source, getter)
5
6
 
6
- if (!['string', 'number', 'symbol'].includes(type)) {
7
- throw new TypeError(`Invalid ID type: ${type}. Expected string, number, or bigint.`)
7
+ if (isCollectionId(id)) {
8
+ return id
8
9
  }
10
+
11
+ throw new Error(`Unable to guess id from source: ${JSON.stringify(source)}`)
12
+ }
13
+
14
+ function isCollectionId(value: unknown): value is CollectionItemId {
15
+ return typeof value === 'string' || typeof value === 'number'
9
16
  }
10
17
 
11
- export function guessItemId<TSource, TProperties extends CollectionConfigProperties>(
12
- source: TSource,
13
- properties: TProperties | undefined
14
- ) {
15
- let id
18
+ function extractItemId<TSource>(source: TSource, getter: GetItemId<TSource>) {
19
+ if (getter === undefined) {
20
+ if (hasObjectProperty(source, 'id')) {
21
+ return source.id
22
+ }
16
23
 
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
24
+ return source
21
25
  }
22
26
 
23
- assertValidId(id)
27
+ if (typeof getter === 'function') {
28
+ return getter(source)
29
+ }
30
+
31
+ if (hasObjectProperty(source, getter)) {
32
+ return source[getter]
33
+ }
24
34
 
25
- return id as GuessItemId<TSource, TProperties>
35
+ throw new Error(`Property "${String(getter)}" not found in source: ${JSON.stringify(source)}`)
26
36
  }
@@ -1,2 +1,6 @@
1
+ export * from './create-collection.ts'
2
+ export * from './create-item.ts'
3
+ export * from './guess-item-id.ts'
1
4
  export * from './types.ts'
2
5
  export * from './use-collection.ts'
6
+ export * from './use-flag-registry.ts'
@@ -1,57 +1,85 @@
1
- import type { ComputedRef, Reactive } from 'vue'
2
-
3
- export type CollectionConfigProperties = Record<string, unknown> & { id?: unknown }
4
-
5
- export type CollectionConfigFlags<TFlag extends string> = TFlag[] | Record<TFlag, { multiple?: boolean }>
1
+ import type { ArrayFilterPredicate, KeyOfByValue } from '@core/types/utility.type.ts'
2
+ import type { ComputedRef, MaybeRefOrGetter, Reactive } from 'vue'
6
3
 
7
4
  export type CollectionItem<
8
- TSource,
9
- TFlag extends string = never,
10
- TProperties extends CollectionConfigProperties = Record<string, never>,
5
+ TSource = unknown,
6
+ TFlag extends string = string,
7
+ TProperties extends CollectionItemProperties = CollectionItemProperties,
8
+ TId extends CollectionItemId = PickSourceId<TSource, 'id'>,
11
9
  > = {
12
- id: GuessItemId<TSource, Reactive<TProperties>>
10
+ id: TId
13
11
  source: TSource
14
12
  flags: Record<TFlag, boolean>
15
13
  properties: Reactive<TProperties>
16
- toggleFlag: (flag: TFlag, forcedValue?: boolean) => void
14
+ toggleFlag: (flag: TFlag, shouldBeFlagged?: boolean) => void
15
+ }
16
+
17
+ export type Collection<
18
+ TSource = unknown,
19
+ TFlag extends string = string,
20
+ TProperties extends CollectionItemProperties = CollectionItemProperties,
21
+ TId extends CollectionItemId = PickSourceId<TSource, 'id'>,
22
+ $TItem extends CollectionItem<TSource, TFlag, TProperties, TId> = CollectionItem<TSource, TFlag, TProperties, TId>,
23
+ > = {
24
+ items: ComputedRef<$TItem[]>
25
+ count: ComputedRef<number>
26
+ useFlag: (flag: TFlag) => UseFlagReturn<TSource, TFlag, TProperties, TId>
27
+ toggleFlag: (id: TId, flag: TFlag, shouldBeFlagged?: boolean) => void
28
+ useSubset: (filter: ArrayFilterPredicate<$TItem>) => Collection<TSource, TFlag, TProperties, TId>
29
+ }
30
+
31
+ export type CollectionItemProperties = Record<PropertyKey, unknown>
32
+
33
+ export type CollectionItemId = string | number
34
+
35
+ export type PickSourceId<TSource, TKey> = TKey extends keyof TSource
36
+ ? TSource[TKey] extends CollectionItemId
37
+ ? TSource[TKey]
38
+ : never
39
+ : never
40
+
41
+ export type ExtractSourceId<TSource, TGetId extends GetItemId<TSource>> = TGetId extends keyof TSource
42
+ ? PickSourceId<TSource, TGetId>
43
+ : TGetId extends (source: TSource) => infer R
44
+ ? R
45
+ : TSource extends CollectionItemId
46
+ ? TSource
47
+ : PickSourceId<TSource, 'id'>
48
+
49
+ export type FlagConfig = {
50
+ multiple?: MaybeRefOrGetter<boolean>
17
51
  }
18
52
 
19
- export type FlagRegistry<TFlag extends string> = {
20
- isFlagged: (id: PropertyKey, flag: TFlag) => boolean
53
+ export type CollectionConfigFlags<TFlag extends string> = TFlag[] | Record<TFlag, FlagConfig>
54
+
55
+ export type FlagRegistry<TId extends CollectionItemId, TFlag extends string> = {
56
+ isFlagged: (id: TId, flag: TFlag) => boolean
21
57
  isFlagDefined: (flag: TFlag) => boolean
22
- toggleFlag: (id: PropertyKey, flag: TFlag, forcedValue?: boolean) => void
58
+ toggleFlag: (id: TId, flag: TFlag, shouldBeFlagged?: boolean) => void
23
59
  clearFlag: (flag: TFlag) => void
24
60
  isMultipleAllowed: (flag: TFlag) => boolean
25
61
  assertFlag: (flag: TFlag) => void
26
62
  }
27
63
 
28
- export type UseFlagReturn<TSource, TFlag extends string, TProperties extends CollectionConfigProperties> = {
29
- items: ComputedRef<CollectionItem<TSource, TFlag, TProperties>[]>
30
- ids: ComputedRef<GuessItemId<TSource, TProperties>[]>
64
+ export type UseFlagReturn<
65
+ TSource,
66
+ TFlag extends string,
67
+ TProperties extends CollectionItemProperties,
68
+ TId extends CollectionItemId,
69
+ $TItem extends CollectionItem<TSource, TFlag, TProperties, TId> = CollectionItem<TSource, TFlag, TProperties, TId>,
70
+ > = {
71
+ items: ComputedRef<$TItem[]>
72
+ ids: ComputedRef<TId[]>
31
73
  count: ComputedRef<number>
32
74
  areAllOn: ComputedRef<boolean>
33
75
  areSomeOn: ComputedRef<boolean>
34
76
  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>
77
+ toggle: (id: TId, shouldBeFlagged?: boolean) => void
78
+ toggleAll: (shouldBeFlagged?: boolean) => void
79
+ useSubset: (filter: ArrayFilterPredicate<$TItem>) => Collection<TSource, TFlag, TProperties, TId>
40
80
  }
41
81
 
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
82
+ export type GetItemId<TSource> =
83
+ | undefined
84
+ | KeyOfByValue<TSource, CollectionItemId>
85
+ | ((source: TSource) => CollectionItemId)
@@ -1,47 +1,97 @@
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'
1
+ import { guessItemId } from '@core/packages/collection/guess-item-id.ts'
2
+ import type { EmptyObject } from '@core/types/utility.type.ts'
3
+ import type {
4
+ Collection,
5
+ CollectionConfigFlags,
6
+ CollectionItemId,
7
+ CollectionItemProperties,
8
+ ExtractSourceId,
9
+ GetItemId,
10
+ } from './types.ts'
5
11
  import { computed, type MaybeRefOrGetter, toValue } from 'vue'
12
+ import { createCollection } from './create-collection.ts'
13
+ import { createItem } from './create-item.ts'
14
+ import { useFlagRegistry } from './use-flag-registry.ts'
15
+
16
+ // Overload #1: Source is CollectionItemId
17
+
18
+ export function useCollection<
19
+ TSource extends CollectionItemId,
20
+ TFlag extends string = never,
21
+ TProperties extends CollectionItemProperties = EmptyObject,
22
+ TGetId extends ((source: TSource) => CollectionItemId) | undefined = undefined,
23
+ $TId extends CollectionItemId = ExtractSourceId<TSource, TGetId>,
24
+ >(
25
+ source: MaybeRefOrGetter<TSource[]>,
26
+ config?: {
27
+ itemId?: TGetId | ((source: TSource) => CollectionItemId)
28
+ flags?: CollectionConfigFlags<TFlag>
29
+ properties?: (source: TSource) => TProperties
30
+ }
31
+ ): Collection<TSource, TFlag, TProperties, $TId>
32
+
33
+ // Overload #2: Source is an object with id
6
34
 
7
35
  export function useCollection<
8
- TSource extends { id: unknown },
36
+ TSource extends { id: CollectionItemId },
9
37
  TFlag extends string = never,
10
- TProperties extends CollectionConfigProperties = { id?: unknown },
38
+ TProperties extends CollectionItemProperties = EmptyObject,
39
+ TGetId extends GetItemId<TSource> = undefined,
40
+ $TId extends CollectionItemId = ExtractSourceId<TSource, TGetId>,
11
41
  >(
12
- sources: MaybeRefOrGetter<TSource[]>,
42
+ source: MaybeRefOrGetter<TSource[]>,
13
43
  config?: {
44
+ itemId?: TGetId | ((source: TSource) => CollectionItemId)
14
45
  flags?: CollectionConfigFlags<TFlag>
15
46
  properties?: (source: TSource) => TProperties
16
47
  }
17
- ): Collection<TSource, TFlag, TProperties>
48
+ ): Collection<TSource, TFlag, TProperties, $TId>
49
+
50
+ // Overload #3: Any other case
18
51
 
19
52
  export function useCollection<
20
53
  TSource,
21
54
  TFlag extends string = never,
22
- TProperties extends CollectionConfigProperties & { id: unknown } = never,
55
+ TProperties extends CollectionItemProperties = EmptyObject,
56
+ TGetId extends GetItemId<TSource> = never,
57
+ $TId extends CollectionItemId = ExtractSourceId<TSource, TGetId>,
23
58
  >(
24
- sources: MaybeRefOrGetter<TSource[]>,
59
+ source: MaybeRefOrGetter<TSource[]>,
25
60
  config: {
61
+ itemId: TGetId | ((source: TSource) => CollectionItemId)
26
62
  flags?: CollectionConfigFlags<TFlag>
27
- properties: (source: TSource) => TProperties
63
+ properties?: (source: TSource) => TProperties
28
64
  }
29
- ): Collection<TSource, TFlag, TProperties>
65
+ ): Collection<TSource, TFlag, TProperties, $TId>
66
+
67
+ // Implementation
30
68
 
31
69
  export function useCollection<
32
70
  TSource,
33
- TFlag extends string = never,
34
- TProperties extends CollectionConfigProperties = { id?: unknown },
71
+ TFlag extends string,
72
+ TProperties extends CollectionItemProperties,
73
+ TGetId extends GetItemId<TSource>,
74
+ $TId extends CollectionItemId,
35
75
  >(
36
- sources: MaybeRefOrGetter<TSource[]>,
76
+ _sources: MaybeRefOrGetter<TSource[]>,
37
77
  config?: {
78
+ itemId?: TGetId
38
79
  flags?: CollectionConfigFlags<TFlag>
39
80
  properties?: (source: TSource) => TProperties
40
81
  }
41
- ): Collection<TSource, TFlag, TProperties> {
42
- const flagRegistry = useFlagRegistry(config?.flags)
82
+ ): Collection<TSource, TFlag, TProperties, $TId> {
83
+ const flagRegistry = useFlagRegistry<TFlag, $TId>(config?.flags)
84
+
85
+ const sources = computed(() => toValue(_sources))
86
+
87
+ const items = computed(() =>
88
+ sources.value.map(source => {
89
+ const id = guessItemId(source, config?.itemId) as $TId
90
+ const properties = config?.properties?.(source) ?? ({} as TProperties)
43
91
 
44
- const items = computed(() => toValue(sources).map(source => createItem(source, config?.properties, flagRegistry)))
92
+ return createItem<TSource, TFlag, TProperties, $TId>(id, source, properties, flagRegistry)
93
+ })
94
+ )
45
95
 
46
96
  return createCollection(items, flagRegistry)
47
97
  }
@@ -1,12 +1,21 @@
1
- import type { CollectionConfigFlags, FlagRegistry } from '@core/packages/collection/types.ts'
2
- import { reactive } from 'vue'
1
+ import { objectFromEntries, objectEntries } from '@core/utils/object.util.ts'
2
+ import type { CollectionConfigFlags, CollectionItemId, FlagConfig, FlagRegistry } from './types.ts'
3
+ import { isRef, reactive, toValue, watch } from 'vue'
3
4
 
4
- export function useFlagRegistry<TFlag extends string>(
5
+ export function useFlagRegistry<TFlag extends string, TId extends CollectionItemId>(
5
6
  config: CollectionConfigFlags<TFlag> = [] as TFlag[]
6
- ): FlagRegistry<TFlag> {
7
- const registry = reactive(new Map<TFlag, Set<PropertyKey>>())
7
+ ): FlagRegistry<TId, TFlag> {
8
+ const registry = reactive(new Map()) as Map<TFlag, Set<TId>>
8
9
 
9
- const flags = Array.isArray(config) ? Object.fromEntries(config.map(flag => [flag, { multiple: true }])) : config
10
+ const flags = Array.isArray(config)
11
+ ? objectFromEntries(config.map(flag => [flag, { multiple: true } as FlagConfig]))
12
+ : config
13
+
14
+ for (const [flag, { multiple }] of objectEntries(flags)) {
15
+ if (isRef(multiple)) {
16
+ watch(multiple, () => clearFlag(flag))
17
+ }
18
+ }
10
19
 
11
20
  function isFlagDefined(flag: TFlag) {
12
21
  return Object.prototype.hasOwnProperty.call(flags, flag)
@@ -18,39 +27,51 @@ export function useFlagRegistry<TFlag extends string>(
18
27
  }
19
28
  }
20
29
 
21
- function isFlagged(id: PropertyKey, flag: TFlag) {
30
+ function isFlagged(id: TId, flag: TFlag) {
22
31
  assertFlag(flag)
23
32
 
24
33
  return registry.get(flag)?.has(id) ?? false
25
34
  }
26
35
 
27
- function toggleFlag(id: PropertyKey, flag: TFlag, forcedValue = !isFlagged(id, flag)) {
36
+ function toggleFlag(id: TId, flag: TFlag, shouldBeFlagged = !isFlagged(id, flag)) {
28
37
  assertFlag(flag)
29
38
 
30
39
  if (!registry.has(flag)) {
31
40
  registry.set(flag, new Set())
32
41
  }
33
42
 
34
- if (forcedValue) {
35
- if (!isMultipleAllowed(flag)) {
36
- clearFlag(flag)
37
- }
38
- registry.get(flag)!.add(id)
39
- } else {
40
- registry.get(flag)!.delete(id)
43
+ const flagSet = registry.get(flag)!
44
+
45
+ if (shouldBeFlagged === flagSet.has(id)) {
46
+ return
47
+ }
48
+
49
+ if (!shouldBeFlagged) {
50
+ flagSet.delete(id)
51
+ return
41
52
  }
53
+
54
+ if (!isMultipleAllowed(flag) && flagSet.size > 0) {
55
+ clearFlag(flag)
56
+ }
57
+
58
+ flagSet.add(id)
42
59
  }
43
60
 
44
61
  function clearFlag(flag: TFlag) {
45
62
  assertFlag(flag)
46
63
 
47
- registry.set(flag, new Set())
64
+ if (!registry.has(flag) || registry.get(flag)!.size === 0) {
65
+ return
66
+ }
67
+
68
+ registry.get(flag)!.clear()
48
69
  }
49
70
 
50
71
  function isMultipleAllowed(flag: TFlag) {
51
72
  assertFlag(flag)
52
73
 
53
- return flags[flag]?.multiple ?? false
74
+ return toValue(flags[flag]?.multiple) ?? true
54
75
  }
55
76
 
56
77
  return {
@@ -0,0 +1,45 @@
1
+ import type { CollectionItemProperties } from '@core/packages/collection'
2
+ import { hasObjectProperty } from '@core/utils/object.util.ts'
3
+ import type { GetOptionLabel } from './types.ts'
4
+
5
+ export function guessLabel<TSource, TCustomProperties extends CollectionItemProperties>(
6
+ source: TSource,
7
+ extraProperties: TCustomProperties,
8
+ getter: GetOptionLabel<TSource, TCustomProperties>
9
+ ): string {
10
+ const label = extractLabel(source, extraProperties, getter)
11
+
12
+ if (typeof label === 'string' || typeof label === 'number') {
13
+ return label.toString()
14
+ }
15
+
16
+ throw new Error(`Unable to guess label from source: ${JSON.stringify(source)}`)
17
+ }
18
+
19
+ export function extractLabel<TSource, TCustomProperties extends CollectionItemProperties>(
20
+ source: TSource,
21
+ extraProperties: TCustomProperties,
22
+ getter: undefined | keyof TSource | ((source: TSource, properties: TCustomProperties) => string)
23
+ ): unknown {
24
+ if (getter === undefined) {
25
+ if (hasObjectProperty(source, 'label')) {
26
+ return source.label
27
+ }
28
+
29
+ return source
30
+ }
31
+
32
+ if (typeof getter === 'function') {
33
+ return getter(source, extraProperties)
34
+ }
35
+
36
+ if (hasObjectProperty(source, getter)) {
37
+ return source[getter]
38
+ }
39
+
40
+ if (source === undefined) {
41
+ return ''
42
+ }
43
+
44
+ throw new Error(`Property "${String(getter)}" not found in source: ${JSON.stringify(source)}`)
45
+ }
@@ -0,0 +1,23 @@
1
+ import type { CollectionItemProperties } from '@core/packages/collection'
2
+ import { hasObjectProperty } from '@core/utils/object.util.ts'
3
+ import type { GetOptionValue } from './types.ts'
4
+
5
+ export function guessValue<TSource, TValue, TCustomProperties extends CollectionItemProperties>(
6
+ source: TSource,
7
+ customProperties: TCustomProperties,
8
+ getter: GetOptionValue<TSource, TCustomProperties>
9
+ ): TValue {
10
+ if (getter === undefined) {
11
+ return source as unknown as TValue
12
+ }
13
+
14
+ if (typeof getter === 'function') {
15
+ return getter(source, customProperties) as TValue
16
+ }
17
+
18
+ if (hasObjectProperty(source, getter)) {
19
+ return source[getter] as TValue
20
+ }
21
+
22
+ throw new Error(`Property "${String(getter)}" not found in source: ${JSON.stringify(source)}`)
23
+ }
@@ -1,2 +1,8 @@
1
+ export * from './guess-label.ts'
2
+ export * from './guess-value.ts'
3
+ export * from './normalize-search-term.ts'
1
4
  export * from './types.ts'
5
+ export * from './use-form-option-controller.ts'
2
6
  export * from './use-form-select.ts'
7
+ export * from './use-form-select-controller.ts'
8
+ export * from './use-form-select-keyboard-navigation.ts'
@@ -0,0 +1,11 @@
1
+ import { toValue } from 'vue'
2
+
3
+ export function normalizeSearchTerm(value: unknown): string {
4
+ const term = toValue(value)
5
+
6
+ if (term === undefined || term === null) {
7
+ return ''
8
+ }
9
+
10
+ return String(term).toLocaleLowerCase().trim()
11
+ }