@xen-orchestra/web-core 0.20.0 → 0.21.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 (118) hide show
  1. package/lib/assets/all-done.svg +62 -0
  2. package/lib/assets/all-good.svg +113 -0
  3. package/lib/assets/error.svg +57 -372
  4. package/lib/assets/no-data.svg +190 -65
  5. package/lib/assets/not-found.svg +446 -126
  6. package/lib/assets/offline.svg +118 -0
  7. package/lib/assets/under-construction.svg +245 -193
  8. package/lib/assets/zoom.svg +85 -0
  9. package/lib/components/backup-state/VtsBackupState.vue +20 -17
  10. package/lib/components/cell-object/VtsCellObject.vue +4 -1
  11. package/lib/components/console/VtsActionsConsole.vue +7 -4
  12. package/lib/components/console/VtsClipboardConsole.vue +9 -6
  13. package/lib/components/copy-button/VtsCopyButton.vue +7 -14
  14. package/lib/components/dropdown/DropdownTitle.vue +5 -2
  15. package/lib/components/icon/NewVtsIcon.vue +49 -0
  16. package/lib/components/input-group/VtsInputGroup.vue +41 -0
  17. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  18. package/lib/components/layout/VtsLayoutSidebar.vue +6 -3
  19. package/lib/components/linear-chart/VtsLinearChart.vue +4 -0
  20. package/lib/components/object-icon/VtsObjectIcon.vue +22 -0
  21. package/lib/components/quick-info-card/VtsQuickInfoCard.vue +4 -1
  22. package/lib/components/select/VtsOption.vue +10 -6
  23. package/lib/components/select/VtsSelect.vue +74 -50
  24. package/lib/components/state-hero/VtsAllDoneHero.vue +16 -0
  25. package/lib/components/state-hero/VtsAllGoodHero.vue +16 -0
  26. package/lib/components/state-hero/VtsComingSoonHero.vue +4 -1
  27. package/lib/components/state-hero/VtsErrorNoDataHero.vue +4 -1
  28. package/lib/components/state-hero/VtsLoadingHero.vue +4 -1
  29. package/lib/components/state-hero/VtsNoDataHero.vue +4 -1
  30. package/lib/components/state-hero/VtsNoSelectionHero.vue +4 -1
  31. package/lib/components/state-hero/VtsObjectNotFoundHero.vue +4 -1
  32. package/lib/components/state-hero/VtsOfflineHero.vue +16 -0
  33. package/lib/components/state-hero/VtsPageNotFoundHero.vue +4 -1
  34. package/lib/components/state-hero/VtsStateHero.vue +10 -1
  35. package/lib/components/table/ColumnTitle.vue +2 -2
  36. package/lib/components/task/VtsQuickTaskButton.vue +4 -1
  37. package/lib/components/task/VtsQuickTaskList.vue +5 -2
  38. package/lib/components/task/VtsQuickTaskTabBar.vue +8 -5
  39. package/lib/components/ui/card-numbers/UiCardNumbers.vue +15 -33
  40. package/lib/components/ui/character-limit/UiCharacterLimit.vue +4 -1
  41. package/lib/components/ui/input/UiInput.vue +2 -2
  42. package/lib/components/ui/label/UiLabel.vue +4 -1
  43. package/lib/components/ui/progress-bar/UiProgressBar.vue +5 -2
  44. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +9 -6
  45. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +6 -3
  46. package/lib/components/ui/stacked-bar/StackedBarSegment.vue +4 -1
  47. package/lib/components/ui/table-pagination/UiTablePagination.vue +6 -3
  48. package/lib/components/ui/text-area/UiTextarea.vue +4 -1
  49. package/lib/components/ui/top-bottom-table/UiTopBottomTable.vue +6 -3
  50. package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +4 -1
  51. package/lib/composables/local-time-ago.composable.ts +53 -0
  52. package/lib/composables/locale-time-ago.composable.ts +53 -0
  53. package/lib/icons/fa-icons.ts +164 -0
  54. package/lib/icons/index.ts +15 -0
  55. package/lib/icons/legacy-icons.ts +80 -0
  56. package/lib/icons/object-icons.ts +187 -0
  57. package/lib/layouts/CoreLayout.vue +7 -3
  58. package/lib/locales/cs.json +73 -7
  59. package/lib/locales/de.json +5 -1
  60. package/lib/locales/en.json +33 -3
  61. package/lib/locales/es.json +9 -5
  62. package/lib/locales/fr.json +32 -2
  63. package/lib/locales/it.json +1 -1
  64. package/lib/locales/nl.json +51 -9
  65. package/lib/locales/ru.json +28 -1
  66. package/lib/locales/sv.json +77 -13
  67. package/lib/packages/collection/README.md +23 -18
  68. package/lib/packages/collection/create-collection.ts +22 -21
  69. package/lib/packages/collection/create-item.ts +21 -20
  70. package/lib/packages/collection/create-use-subset.ts +23 -0
  71. package/lib/packages/collection/guess-item-id.ts +26 -16
  72. package/lib/packages/collection/index.ts +4 -0
  73. package/lib/packages/collection/types.ts +65 -37
  74. package/lib/packages/collection/use-collection.ts +68 -18
  75. package/lib/packages/collection/use-flag-registry.ts +38 -17
  76. package/lib/packages/form-select/guess-label.ts +45 -0
  77. package/lib/packages/form-select/guess-value.ts +23 -0
  78. package/lib/packages/form-select/index.ts +6 -0
  79. package/lib/packages/form-select/normalize-search-term.ts +11 -0
  80. package/lib/packages/form-select/types.ts +90 -42
  81. package/lib/packages/form-select/use-form-option-controller.ts +7 -3
  82. package/lib/packages/form-select/use-form-select-controller.ts +38 -27
  83. package/lib/packages/form-select/use-form-select-keyboard-navigation.ts +1 -1
  84. package/lib/packages/form-select/use-form-select.ts +308 -130
  85. package/lib/packages/icon/DisplayIcon.vue +25 -0
  86. package/lib/packages/icon/DisplayIconAny.vue +16 -0
  87. package/lib/packages/icon/DisplayIconSingle.vue +35 -0
  88. package/lib/packages/icon/DisplayIconStack.vue +34 -0
  89. package/lib/packages/icon/README.md +286 -0
  90. package/lib/packages/icon/create-icon-bindings.ts +27 -0
  91. package/lib/packages/icon/define-icon-pack.ts +23 -0
  92. package/lib/packages/icon/define-icon-single.ts +17 -0
  93. package/lib/packages/icon/define-icon-stack.ts +20 -0
  94. package/lib/packages/icon/define-icon.ts +40 -0
  95. package/lib/packages/icon/generate-icon-variants.ts +17 -0
  96. package/lib/packages/icon/index.ts +8 -0
  97. package/lib/packages/icon/is-icon-stack.ts +5 -0
  98. package/lib/packages/icon/merge-icons.ts +25 -0
  99. package/lib/packages/icon/merge-transforms.ts +12 -0
  100. package/lib/packages/icon/normalize-icon.ts +25 -0
  101. package/lib/packages/icon/to-tuple.ts +7 -0
  102. package/lib/packages/icon/types.ts +72 -0
  103. package/lib/packages/job/README.md +2 -2
  104. package/lib/packages/mapper/README.md +166 -0
  105. package/lib/packages/mapper/convert-to-map.ts +5 -0
  106. package/lib/packages/mapper/create-mapper.ts +30 -0
  107. package/lib/packages/mapper/index.ts +4 -0
  108. package/lib/packages/mapper/types.ts +1 -0
  109. package/lib/packages/mapper/use-mapper.ts +31 -0
  110. package/lib/stores/sidebar.store.ts +1 -1
  111. package/lib/types/chart.ts +2 -2
  112. package/lib/types/utility.type.ts +9 -0
  113. package/lib/utils/object.util.ts +16 -0
  114. package/lib/utils/size.util.ts +4 -2
  115. package/package.json +2 -1
  116. package/lib/components/backup-item/VtsBackupItem.vue +0 -47
  117. package/lib/composables/mapper.composable.md +0 -74
  118. package/lib/composables/mapper.composable.ts +0 -18
@@ -6,9 +6,9 @@ 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
+ itemId: source => source.theId, // Required only if TSource doesn't have an `id` property
9
10
  flags: ['selected', 'active', { highlighted: { multiple: false } }],
10
11
  properties: source => ({
11
- id: source.theId, // Required if TSource doesn't have an `id` property
12
12
  isAvailable: source.status === 'available',
13
13
  fullName: `${source.firstName} ${source.lastName}`,
14
14
  }),
@@ -18,8 +18,8 @@ const { items, useSubset, useFlag } = useCollection(sources, {
18
18
  ## Core Concepts
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
- - **Flags**: Boolean states attached to items (like 'selected', 'active', 'highlighted')
22
- - **Properties**: Additional custom values
21
+ - **Flags**: Arbitrary `boolean` states attached to items (like `selected`, `active`, `highlighted`, etc.)
22
+ - **Properties**: Additional custom values attached to items, computed from the source object (like `fullName`, `isAvailable`, etc.)
23
23
 
24
24
  ## `useCollection` parameters
25
25
 
@@ -30,21 +30,22 @@ const { items, useSubset, useFlag } = useCollection(sources, {
30
30
 
31
31
  ### `options` object
32
32
 
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) |
33
+ | Name | Type | Required | Description |
34
+ | ------------ | ---------------------------------------------- | :------: | ----------------------------------------------------------------------------------------------- |
35
+ | `itemId` | `keyof TSource \| ((source: TSource) => TId)` | ~ | Function to retrieve the item ID or property of TSource (if not provided, `TSource.id` is used) |
36
+ | `flags` | `FlagsConfig<TFlag>` | | Flags that can be applied to items in the collection |
37
+ | `properties` | `(source: TSource) => Record<string, unknown>` | | Function that returns additional properties for each item (see below) |
37
38
 
38
39
  ### Item ID
39
40
 
40
41
  The item ID will be retrieved automatically from `TSource.id`
41
42
 
42
- If `TSource` doesn't provide an `id`, then `options.properties` will be required and must return at least an `id`
43
+ If `TSource` doesn't provide an `id`, then `options.itemId` will be required.
43
44
 
44
45
  ### `FlagsConfig` type
45
46
 
46
47
  ```typescript
47
- type FlagsConfig<TFlag extends string> = TFlag[] | { [K in TFlag]: { multiple?: boolean } }
48
+ type FlagsConfig = string[] | Record<string, { multiple?: MaybeRef<boolean> }>
48
49
  ```
49
50
 
50
51
  Values for `multiple`:
@@ -63,13 +64,13 @@ Values for `multiple`:
63
64
 
64
65
  ### `CollectionItem` object
65
66
 
66
- | Name | Type | Description |
67
- | ------------ | ------------------------------ | --------------------------------------------------------- |
68
- | `id` | `TId` | Unique identifier for the item (string, number or symbol) |
69
- | `source` | `TSource` | The original source object |
70
- | `flags` | `Record<TFlag, boolean>` | Object containing the state of all flags for this item |
71
- | `properties` | `TProperties` | Object containing all computed properties for this item |
72
- | `toggleFlag` | `(flag, forcedValue?) => void` | Method to toggle a flag on this item |
67
+ | Name | Type | Description |
68
+ | ------------ | ---------------------------------- | --------------------------------------------------------- |
69
+ | `id` | `TId` | Unique identifier for the item (string, number or symbol) |
70
+ | `source` | `TSource` | The original source object |
71
+ | `flags` | `Record<TFlag, boolean>` | Object containing the state of all flags for this item |
72
+ | `properties` | `TProperties` | Object containing all computed properties for this item |
73
+ | `toggleFlag` | `(flag, shouldBeFlagged?) => void` | Method to toggle a flag on this item |
73
74
 
74
75
  ### UseFlagReturn object
75
76
 
@@ -81,8 +82,8 @@ Values for `multiple`:
81
82
  | `areAllOn` | `ComputedRef<boolean>` | Whether all items in the collection have this flag set |
82
83
  | `areSomeOn` | `ComputedRef<boolean>` | Whether at least one item has this flag set |
83
84
  | `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 |
85
+ | `toggle` | `(id, shouldBeFlagged?) => void` | Toggle this flag on a specific item |
86
+ | `toggleAll` | `(shouldBeFlagged?) => void` | Toggle this flag on all items in the collection |
86
87
  | `useSubset` | `(filter: (item) => boolean) => Collection` | Creates a sub collection matching the filter |
87
88
 
88
89
  ## Flag Operations
@@ -169,4 +170,8 @@ const { items: users, useSubset } = useCollection(rawUsers, {
169
170
  const { items: admins, useSubset: useAdminSubset } = useSubset(item => item.source.group === 'admin')
170
171
 
171
172
  const { items: activeAdmins } = useAdminSubset(item => item.source.status === 'active')
173
+
174
+ // This would be equivalent to:
175
+
176
+ const { items: activeAdmins } = useSubset(item => item.source.group === 'admin' && item.source.status === 'active')
172
177
  ```
@@ -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 {