@xen-orchestra/web-core 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/icon/VtsIcon.vue +9 -1
- package/lib/components/input-wrapper/VtsInputWrapper.vue +1 -0
- package/lib/components/{charts/LinearChart.md → linear-chart/VtsLinearChart.md} +3 -3
- package/lib/components/{charts/LinearChart.vue → linear-chart/VtsLinearChart.vue} +12 -11
- package/lib/components/quick-info-card/VtsQuickInfoCard.vue +29 -0
- package/lib/components/quick-info-column/VtsQuickInfoColumn.vue +13 -0
- package/lib/components/quick-info-row/VtsQuickInfoRow.vue +40 -0
- package/lib/components/ui/alert/UiAlert.vue +105 -0
- package/lib/components/ui/card/UiCard.vue +12 -0
- package/lib/components/ui/circle-progress-bar/UiCircleProgressBar.vue +212 -0
- package/lib/components/ui/info/UiInfo.vue +5 -3
- package/lib/components/ui/input/UiInput.vue +126 -109
- package/lib/components/ui/radio-button/UiRadioButton.vue +1 -1
- package/lib/components/ui/toaster/UiToaster.vue +6 -2
- package/lib/composables/chart-theme.composable.ts +5 -5
- package/lib/composables/relative-time.composable.md +18 -0
- package/lib/composables/relative-time.composable.ts +66 -0
- package/lib/i18n.ts +16 -0
- package/lib/locales/cs.json +98 -18
- package/lib/locales/de.json +233 -24
- package/lib/locales/en.json +58 -2
- package/lib/locales/es.json +94 -14
- package/lib/locales/fa.json +259 -9
- package/lib/locales/fr.json +58 -2
- package/lib/locales/it.json +316 -0
- package/lib/locales/nl.json +503 -0
- package/lib/locales/ru.json +181 -0
- package/lib/locales/sv.json +92 -10
- package/lib/locales/uk.json +1 -0
- package/lib/packages/collection/README.md +167 -0
- package/lib/packages/collection/build-item.ts +45 -0
- package/lib/packages/collection/create-collection.ts +60 -0
- package/lib/packages/collection/index.ts +5 -0
- package/lib/packages/collection/types.ts +29 -0
- package/lib/packages/collection/use-collection.ts +15 -0
- package/lib/packages/collection/use-flag-registry.ts +47 -0
- package/lib/utils/time.util.ts +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# `useCollection` composable
|
|
2
|
+
|
|
3
|
+
The `useCollection` composable helps you manage a collection of items with flags (boolean states) and computed properties. It provides a type-safe way to filter, flag, and manipulate items in a collection.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const { items, useSubset, useFlag } = useCollection(sources, {
|
|
9
|
+
identifier: source => source.id,
|
|
10
|
+
flags: ['selected', 'active', { highlighted: { multiple: false } }],
|
|
11
|
+
properties: source => ({
|
|
12
|
+
isAvailable: computed(() => source.status === 'available'),
|
|
13
|
+
fullName: computed(() => `${source.firstName} ${source.lastName}`),
|
|
14
|
+
}),
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
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**: Computed values derived from the source object
|
|
23
|
+
|
|
24
|
+
## `useCollection` parameters
|
|
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) |
|
|
30
|
+
|
|
31
|
+
### `options` object
|
|
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 |
|
|
38
|
+
|
|
39
|
+
### `FlagsConfig` type
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
type FlagsConfig<TFlag extends string> = TFlag[] | { [K in TFlag]: { multiple?: boolean } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Values for `multiple`:
|
|
46
|
+
|
|
47
|
+
- `true` (default): multiple items can have the same flag.
|
|
48
|
+
- `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
|
+
|
|
50
|
+
## Return Value
|
|
51
|
+
|
|
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 |
|
|
58
|
+
|
|
59
|
+
### `CollectionItem` object
|
|
60
|
+
|
|
61
|
+
| Name | Type | Description |
|
|
62
|
+
| ------------ | ------------------------------ | --------------------------------------------------------- |
|
|
63
|
+
| `id` | `TId` | Unique identifier for the item (string, number or symbol) |
|
|
64
|
+
| `source` | `TSource` | The original source object |
|
|
65
|
+
| `flags` | `Record<TFlag, boolean>` | Object containing the state of all flags for this item |
|
|
66
|
+
| `properties` | `TProperties` | Object containing all computed properties for this item |
|
|
67
|
+
| `toggleFlag` | `(flag, forcedValue?) => void` | Method to toggle a flag on this item |
|
|
68
|
+
|
|
69
|
+
### Return value of `useFlag(flag)`
|
|
70
|
+
|
|
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 |
|
|
81
|
+
|
|
82
|
+
## Flag Operations
|
|
83
|
+
|
|
84
|
+
The composable provides several ways to work with flags:
|
|
85
|
+
|
|
86
|
+
### Setting and Toggling Flags
|
|
87
|
+
|
|
88
|
+
You can work with flags directly on collection items:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Set flag for a single item
|
|
92
|
+
item.flags.selected = true
|
|
93
|
+
|
|
94
|
+
// Toggle the 'selected' flag on an item
|
|
95
|
+
// Same as item.flags.selected = !item.flags.selected
|
|
96
|
+
item.toggleFlag('selected')
|
|
97
|
+
|
|
98
|
+
// Force the 'selected' flag to a specific value
|
|
99
|
+
// Same as item.flags.selected = true
|
|
100
|
+
item.toggleFlag('selected', true)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can also use the utilities provided by `useFlag`:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Get utilities for the 'selected' flag
|
|
107
|
+
const { toggle: toggleSelected, toggleAll: toggleAllSelected } = useFlag('selected')
|
|
108
|
+
|
|
109
|
+
// Toggle flag for a specific item
|
|
110
|
+
toggleSelected(itemId)
|
|
111
|
+
|
|
112
|
+
// Force flag to true for a specific item
|
|
113
|
+
toggleSelected(itemId, true)
|
|
114
|
+
|
|
115
|
+
// Toggle flag on all items
|
|
116
|
+
toggleAllSelected()
|
|
117
|
+
|
|
118
|
+
// Force all items to have the flag
|
|
119
|
+
toggleAllSelected(true)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Checking Flags
|
|
123
|
+
|
|
124
|
+
You can check if an item has a flag:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Check if an item has a specific flag
|
|
128
|
+
const isSelected = item.flags.selected
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Example
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const {
|
|
135
|
+
items: users,
|
|
136
|
+
useSubset,
|
|
137
|
+
count,
|
|
138
|
+
} = useCollection(rawUsers, {
|
|
139
|
+
identifier: user => user.id,
|
|
140
|
+
flags: ['selected'],
|
|
141
|
+
properties: user => ({
|
|
142
|
+
fullName: computed(() => `${user.firstName} ${user.lastName} (${user.group})`),
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const { items: admins, useFlag: useAdminFlag, count: adminCount } = useSubset(item => item.source.group === 'admin')
|
|
147
|
+
|
|
148
|
+
const {
|
|
149
|
+
items: selectedAdmins,
|
|
150
|
+
areAllOn: areAllAdminSelected,
|
|
151
|
+
toggleAll: toggleAllAdminSelection,
|
|
152
|
+
count: selectedAdminCount,
|
|
153
|
+
toggle: toggleAdminSelection,
|
|
154
|
+
} = useAdminFlag('selected')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
It's also possible to create a subset of subset:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const { items: users, useSubset } = useCollection(rawUsers, {
|
|
161
|
+
/* ... */
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const { items: admins, useSubset: useAdminSubset } = useSubset(item => item.source.group === 'admin')
|
|
165
|
+
|
|
166
|
+
const { items: activeAdmins } = useAdminSubset(item => item.source.status === 'active')
|
|
167
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CollectionItem, CollectionOptions, FlagRegistry } from '@core/packages/collection'
|
|
2
|
+
import { unref, type UnwrapRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function buildItem<
|
|
5
|
+
TSource,
|
|
6
|
+
TId extends PropertyKey,
|
|
7
|
+
TFlag extends string,
|
|
8
|
+
TProperties extends Record<string, unknown>,
|
|
9
|
+
>(
|
|
10
|
+
source: TSource,
|
|
11
|
+
options: CollectionOptions<TSource, TId, TFlag, TProperties>,
|
|
12
|
+
flagRegistry: FlagRegistry<TFlag>
|
|
13
|
+
): CollectionItem<TSource, TId, TFlag, UnwrapRef<TProperties>> {
|
|
14
|
+
const id = options.identifier(source)
|
|
15
|
+
const properties = options.properties?.(source)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
source,
|
|
20
|
+
toggleFlag(flag: TFlag, forcedValue?: boolean) {
|
|
21
|
+
flagRegistry.toggleFlag(id, flag, forcedValue)
|
|
22
|
+
},
|
|
23
|
+
flags: new Proxy({} as Record<TFlag, boolean>, {
|
|
24
|
+
has(target, flag: TFlag) {
|
|
25
|
+
return flagRegistry.isFlagDefined(flag)
|
|
26
|
+
},
|
|
27
|
+
get(target, flag: TFlag) {
|
|
28
|
+
return flagRegistry.isFlagged(id, flag)
|
|
29
|
+
},
|
|
30
|
+
set(target, flag: TFlag, value) {
|
|
31
|
+
flagRegistry.toggleFlag(id, flag, value)
|
|
32
|
+
|
|
33
|
+
return true
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
properties: new Proxy({} as UnwrapRef<TProperties>, {
|
|
37
|
+
has(target, prop: Extract<keyof TProperties, string>) {
|
|
38
|
+
return properties !== undefined && prop in properties
|
|
39
|
+
},
|
|
40
|
+
get(target, prop: Extract<keyof TProperties, string>) {
|
|
41
|
+
return unref(properties?.[prop])
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CollectionItem, FlagRegistry } from '@core/packages/collection'
|
|
2
|
+
import { useArrayFilter, useArrayMap } from '@vueuse/core'
|
|
3
|
+
import { computed, type ComputedRef } from 'vue'
|
|
4
|
+
|
|
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
|
+
const flaggedItems = useArrayFilter(items, item => item.flags[flag])
|
|
13
|
+
|
|
14
|
+
const ids = useArrayMap(flaggedItems, item => item.id)
|
|
15
|
+
|
|
16
|
+
const count = computed(() => flaggedItems.value.length)
|
|
17
|
+
|
|
18
|
+
const areAllOn = computed(() => items.value.length === count.value)
|
|
19
|
+
|
|
20
|
+
const areSomeOn = computed(() => count.value > 0)
|
|
21
|
+
|
|
22
|
+
const areNoneOn = computed(() => count.value === 0)
|
|
23
|
+
|
|
24
|
+
function toggle(id: TId, forcedValue?: boolean) {
|
|
25
|
+
flagRegistry.toggleFlag(id, flag, forcedValue)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toggleAll(forcedValue = !areAllOn.value) {
|
|
29
|
+
for (const item of items.value) {
|
|
30
|
+
flagRegistry.toggleFlag(item.id, flag, forcedValue)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
items: flaggedItems,
|
|
36
|
+
ids,
|
|
37
|
+
count,
|
|
38
|
+
areAllOn,
|
|
39
|
+
areSomeOn,
|
|
40
|
+
areNoneOn,
|
|
41
|
+
toggle,
|
|
42
|
+
toggleAll,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
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)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const count = computed(() => items.value.length)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
items,
|
|
56
|
+
count,
|
|
57
|
+
useFlag,
|
|
58
|
+
useSubset,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { useFlagRegistry } from '@core/packages/collection'
|
|
2
|
+
|
|
3
|
+
export type FlagsConfig<TFlag extends string> = TFlag[] | { [K in TFlag]: { multiple?: boolean } }
|
|
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
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CollectionItem<
|
|
17
|
+
TSource,
|
|
18
|
+
TId extends PropertyKey,
|
|
19
|
+
TFlag extends string,
|
|
20
|
+
TProperties extends Record<string, any>,
|
|
21
|
+
> = {
|
|
22
|
+
id: TId
|
|
23
|
+
source: TSource
|
|
24
|
+
flags: Record<TFlag, boolean>
|
|
25
|
+
properties: TProperties
|
|
26
|
+
toggleFlag: (flag: TFlag, forcedValue?: boolean) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FlagRegistry<TFlag extends string> = ReturnType<typeof useFlagRegistry<TFlag>>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildItem, type CollectionOptions, createCollection, useFlagRegistry } from '@core/packages/collection'
|
|
2
|
+
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useCollection<
|
|
5
|
+
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)
|
|
11
|
+
|
|
12
|
+
const items = computed(() => toValue(sources).map(source => buildItem(source, options, flagRegistry)))
|
|
13
|
+
|
|
14
|
+
return createCollection(items, flagRegistry)
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FlagsConfig } from '@core/packages/collection/types.ts'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useFlagRegistry<TFlag extends string>(_flags: FlagsConfig<TFlag> = []) {
|
|
5
|
+
const registry = reactive(new Map()) as Map<TFlag, Set<PropertyKey> | undefined>
|
|
6
|
+
|
|
7
|
+
const flags = Array.isArray(_flags) ? Object.fromEntries(_flags.map(flag => [flag, { multiple: true }])) : _flags
|
|
8
|
+
|
|
9
|
+
function isFlagDefined(flag: TFlag) {
|
|
10
|
+
return flags[flag] !== undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isFlagged(id: PropertyKey, flag: TFlag) {
|
|
14
|
+
return registry.get(flag)?.has(id) ?? false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toggleFlag(id: PropertyKey, flag: TFlag, forcedValue = !isFlagged(id, flag)) {
|
|
18
|
+
if (!registry.has(flag)) {
|
|
19
|
+
registry.set(flag, new Set())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (forcedValue) {
|
|
23
|
+
if (!isMultipleAllowed(flag)) {
|
|
24
|
+
clearFlag(flag)
|
|
25
|
+
}
|
|
26
|
+
registry.get(flag)!.add(id)
|
|
27
|
+
} else {
|
|
28
|
+
registry.get(flag)!.delete(id)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clearFlag(flag: TFlag) {
|
|
33
|
+
registry.set(flag, new Set())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMultipleAllowed(flag: TFlag) {
|
|
37
|
+
return flags[flag]?.multiple ?? false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
isFlagged,
|
|
42
|
+
isFlagDefined,
|
|
43
|
+
toggleFlag,
|
|
44
|
+
clearFlag,
|
|
45
|
+
isMultipleAllowed,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { utcParse } from 'd3-time-format'
|
|
2
|
+
|
|
3
|
+
export function parseDateTime(dateTime: Date | string | number): number {
|
|
4
|
+
if (typeof dateTime === 'number') {
|
|
5
|
+
return dateTime
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (dateTime instanceof Date) {
|
|
9
|
+
return dateTime.getTime()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
dateTime = dateTime.replace(/(-|\.\d{3})/g, '') // Allow toISOString() date-time format
|
|
13
|
+
const date = utcParse('%Y%m%dT%H:%M:%SZ')(dateTime)
|
|
14
|
+
if (date === null) {
|
|
15
|
+
throw new RangeError(`unable to parse XAPI datetime ${JSON.stringify(dateTime)}`)
|
|
16
|
+
}
|
|
17
|
+
return date.getTime()
|
|
18
|
+
}
|