@xen-orchestra/web-core 0.43.0 → 0.44.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/button-group/VtsButtonGroup.vue +1 -1
- package/lib/components/columns/VtsColumns.vue +1 -1
- package/lib/components/console/VtsLayoutConsole.vue +1 -1
- package/lib/components/console/VtsRemoteConsole.vue +1 -1
- package/lib/components/layout/VtsLayoutSidebar.vue +5 -5
- package/lib/components/menu/MenuList.vue +1 -1
- package/lib/components/query-builder/VtsQueryBuilder.vue +104 -0
- package/lib/components/query-builder/VtsQueryBuilderButton.vue +52 -0
- package/lib/components/query-builder/VtsQueryBuilderFilter.vue +126 -0
- package/lib/components/query-builder/VtsQueryBuilderGroup.vue +157 -0
- package/lib/components/query-builder/VtsQueryBuilderModal.vue +62 -0
- package/lib/components/query-builder/VtsQueryBuilderRow.vue +88 -0
- package/lib/components/query-builder/VtsQueryBuilderTreeLine.vue +42 -0
- package/lib/components/quick-info-row/VtsQuickInfoRow.vue +1 -1
- package/lib/components/select/VtsSelect.vue +6 -2
- package/lib/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue +1 -1
- package/lib/components/tab/TabItem.vue +2 -2
- package/lib/components/tree/VtsTreeItem.vue +1 -1
- package/lib/components/ui/button-icon/UiButtonIcon.vue +6 -10
- package/lib/components/ui/input/UiInput.vue +19 -3
- package/lib/components/ui/modal/UiModal.vue +0 -1
- package/lib/components/ui/panel/UiPanel.vue +1 -1
- package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +4 -4
- package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +2 -2
- package/lib/composables/debounced-ref.composable.ts +40 -0
- package/lib/composables/pagination.composable.ts +1 -1
- package/lib/composables/tree-filter.composable.ts +16 -7
- package/lib/i18n.ts +4 -0
- package/lib/icons/fa-icons.ts +7 -1
- package/lib/locales/cs.json +74 -17
- package/lib/locales/da.json +14 -14
- package/lib/locales/de.json +79 -19
- package/lib/locales/en.json +44 -17
- package/lib/locales/es.json +34 -15
- package/lib/locales/fa.json +14 -14
- package/lib/locales/fi.json +0 -8
- package/lib/locales/fr.json +45 -18
- package/lib/locales/it.json +15 -14
- package/lib/locales/ko.json +59 -10
- package/lib/locales/nb-NO.json +0 -6
- package/lib/locales/nl.json +94 -17
- package/lib/locales/pl.json +0 -6
- package/lib/locales/pt-BR.json +10 -12
- package/lib/locales/ru.json +14 -14
- package/lib/locales/sv.json +14 -14
- package/lib/locales/uk.json +14 -14
- package/lib/locales/zh-Hans.json +884 -0
- package/lib/packages/collection/use-collection.ts +6 -6
- package/lib/packages/form-select/guess-value.ts +3 -2
- package/lib/packages/form-select/types.ts +1 -1
- package/lib/packages/form-select/use-form-select-controller.ts +9 -1
- package/lib/packages/form-select/use-form-select.ts +15 -15
- package/lib/packages/icon/DisplayIconSingle.vue +29 -2
- package/lib/packages/icon/create-icon-bindings.ts +3 -0
- package/lib/packages/icon/types.ts +2 -0
- package/lib/packages/query-builder/filter/convert-filter-to-group.ts +20 -0
- package/lib/packages/query-builder/filter/create-empty-filter.ts +11 -0
- package/lib/packages/query-builder/filter/create-query-builder-filter.ts +73 -0
- package/lib/packages/query-builder/filter/duplicate-filter.ts +11 -0
- package/lib/packages/query-builder/filter/is-filter-expression.ts +26 -0
- package/lib/packages/query-builder/filter/normalize-boolean-expression.ts +63 -0
- package/lib/packages/query-builder/filter/parsers/parse-contains-value.ts +5 -0
- package/lib/packages/query-builder/filter/parsers/parse-ends-with-value.ts +6 -0
- package/lib/packages/query-builder/filter/parsers/parse-glob-value.ts +5 -0
- package/lib/packages/query-builder/filter/parsers/parse-is-value.ts +10 -0
- package/lib/packages/query-builder/filter/parsers/parse-number-value.ts +9 -0
- package/lib/packages/query-builder/filter/parsers/parse-regex-value.ts +13 -0
- package/lib/packages/query-builder/filter/parsers/parse-starts-with-value.ts +6 -0
- package/lib/packages/query-builder/filter/render-filter.ts +11 -0
- package/lib/packages/query-builder/filter/use-filter-operator.ts +36 -0
- package/lib/packages/query-builder/filter/use-filter-property.ts +19 -0
- package/lib/packages/query-builder/filter/use-filter-value.ts +42 -0
- package/lib/packages/query-builder/filter/use-raw-filter.ts +66 -0
- package/lib/packages/query-builder/group/build-root-group.ts +20 -0
- package/lib/packages/query-builder/group/create-query-builder-group.ts +117 -0
- package/lib/packages/query-builder/group/duplicate-group.ts +13 -0
- package/lib/packages/query-builder/node/duplicate-node.ts +7 -0
- package/lib/packages/query-builder/node/get-comparison-operator.ts +18 -0
- package/lib/packages/query-builder/node/get-raw-filter.ts +15 -0
- package/lib/packages/query-builder/node/handle-comparison-node.ts +18 -0
- package/lib/packages/query-builder/node/handle-glob-pattern-node.ts +17 -0
- package/lib/packages/query-builder/node/handle-group-node.ts +22 -0
- package/lib/packages/query-builder/node/handle-node.ts +72 -0
- package/lib/packages/query-builder/node/handle-null-node.ts +11 -0
- package/lib/packages/query-builder/node/handle-property-group-node.ts +25 -0
- package/lib/packages/query-builder/node/handle-property-node.ts +94 -0
- package/lib/packages/query-builder/node/handle-regexp-node.ts +62 -0
- package/lib/packages/query-builder/node/handle-string-or-number-node.ts +48 -0
- package/lib/packages/query-builder/node/handle-truthy-property-node.ts +42 -0
- package/lib/packages/query-builder/query-builder-error.ts +7 -0
- package/lib/packages/query-builder/schema/parse-operators.ts +28 -0
- package/lib/packages/query-builder/schema/parse-values.ts +19 -0
- package/lib/packages/query-builder/schema/use-query-builder-schema.ts +25 -0
- package/lib/packages/query-builder/types.ts +119 -0
- package/lib/packages/query-builder/use-query-builder-filter.ts +39 -0
- package/lib/packages/query-builder/use-query-builder.ts +52 -0
- package/lib/stores/panel.store.ts +1 -1
- package/lib/stores/sidebar.store.ts +6 -6
- package/lib/stores/ui.store.ts +12 -8
- package/lib/tables/column-definitions/input-column.ts +2 -2
- package/lib/utils/query-builder/use-boolean-schema.ts +11 -0
- package/lib/utils/query-builder/use-number-schema.ts +20 -0
- package/lib/utils/query-builder/use-string-schema.ts +36 -0
- package/package.json +3 -1
|
@@ -27,7 +27,7 @@ export function useCollection<
|
|
|
27
27
|
config?: {
|
|
28
28
|
itemId?: TGetId | ((source: TSource) => CollectionItemId)
|
|
29
29
|
flags?: CollectionConfigFlags<TFlag>
|
|
30
|
-
properties?: (source: TSource) => TProperties
|
|
30
|
+
properties?: (source: TSource, index: number) => TProperties
|
|
31
31
|
}
|
|
32
32
|
): Collection<TSource, TFlag, TProperties, $TId>
|
|
33
33
|
|
|
@@ -44,7 +44,7 @@ export function useCollection<
|
|
|
44
44
|
config?: {
|
|
45
45
|
itemId?: TGetId | ((source: TSource) => CollectionItemId)
|
|
46
46
|
flags?: CollectionConfigFlags<TFlag>
|
|
47
|
-
properties?: (source: TSource) => TProperties
|
|
47
|
+
properties?: (source: TSource, index: number) => TProperties
|
|
48
48
|
}
|
|
49
49
|
): Collection<TSource, TFlag, TProperties, $TId>
|
|
50
50
|
|
|
@@ -61,7 +61,7 @@ export function useCollection<
|
|
|
61
61
|
config: {
|
|
62
62
|
itemId: TGetId | ((source: TSource) => CollectionItemId)
|
|
63
63
|
flags?: CollectionConfigFlags<TFlag>
|
|
64
|
-
properties?: (source: TSource) => TProperties
|
|
64
|
+
properties?: (source: TSource, index: number) => TProperties
|
|
65
65
|
}
|
|
66
66
|
): Collection<TSource, TFlag, TProperties, $TId>
|
|
67
67
|
|
|
@@ -78,7 +78,7 @@ export function useCollection<
|
|
|
78
78
|
config?: {
|
|
79
79
|
itemId?: TGetId
|
|
80
80
|
flags?: CollectionConfigFlags<TFlag>
|
|
81
|
-
properties?: (source: TSource) => TProperties
|
|
81
|
+
properties?: (source: TSource, index: number) => TProperties
|
|
82
82
|
}
|
|
83
83
|
): Collection<TSource, TFlag, TProperties, $TId> {
|
|
84
84
|
const flagRegistry = useFlagRegistry<TFlag, $TId>(config?.flags)
|
|
@@ -86,9 +86,9 @@ export function useCollection<
|
|
|
86
86
|
const sources = toComputed(_sources)
|
|
87
87
|
|
|
88
88
|
const items = computed(() =>
|
|
89
|
-
sources.value.map(source => {
|
|
89
|
+
sources.value.map((source, index) => {
|
|
90
90
|
const id = guessItemId(source, config?.itemId) as $TId
|
|
91
|
-
const properties = config?.properties?.(source) ?? ({} as TProperties)
|
|
91
|
+
const properties = config?.properties?.(source, index) ?? ({} as TProperties)
|
|
92
92
|
|
|
93
93
|
return createItem<TSource, TFlag, TProperties, $TId>(id, source, properties, flagRegistry)
|
|
94
94
|
})
|
|
@@ -5,14 +5,15 @@ import type { GetOptionValue } from './types.ts'
|
|
|
5
5
|
export function guessValue<TSource, TValue, TCustomProperties extends CollectionItemProperties>(
|
|
6
6
|
source: TSource,
|
|
7
7
|
customProperties: TCustomProperties,
|
|
8
|
-
getter: GetOptionValue<TSource, TCustomProperties
|
|
8
|
+
getter: GetOptionValue<TSource, TCustomProperties>,
|
|
9
|
+
index: number
|
|
9
10
|
): TValue {
|
|
10
11
|
if (getter === undefined) {
|
|
11
12
|
return source as unknown as TValue
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
if (typeof getter === 'function') {
|
|
15
|
-
return getter(source, customProperties) as TValue
|
|
16
|
+
return getter(source, customProperties, index) as TValue
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
if (hasObjectProperty(source, getter)) {
|
|
@@ -118,7 +118,7 @@ export type UseFormSelectReturn<
|
|
|
118
118
|
export type GetOptionValue<TSource, TCustomProperties extends CollectionItemProperties> =
|
|
119
119
|
| undefined
|
|
120
120
|
| keyof TSource
|
|
121
|
-
| ((source: TSource, properties: TCustomProperties) => unknown)
|
|
121
|
+
| ((source: TSource, properties: TCustomProperties, index: number) => unknown)
|
|
122
122
|
|
|
123
123
|
export type GetOptionLabel<TSource, TCustomProperties extends CollectionItemProperties> =
|
|
124
124
|
| undefined
|
|
@@ -50,7 +50,15 @@ export function useFormSelectController(select: FormSelect) {
|
|
|
50
50
|
|
|
51
51
|
const { floatingStyles } = useFloating(triggerRef, dropdownRef, {
|
|
52
52
|
whileElementsMounted: autoUpdate,
|
|
53
|
-
middleware: [
|
|
53
|
+
middleware: [
|
|
54
|
+
shift(),
|
|
55
|
+
flip(),
|
|
56
|
+
size({
|
|
57
|
+
apply: ({ availableHeight, elements }) => {
|
|
58
|
+
elements.floating.style.maxHeight = `${Math.min(500, availableHeight)}px`
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
54
62
|
placement: 'bottom-start',
|
|
55
63
|
open: isOpen,
|
|
56
64
|
strategy: 'fixed',
|
|
@@ -52,7 +52,7 @@ export function useFormSelect<
|
|
|
52
52
|
}>
|
|
53
53
|
option?: {
|
|
54
54
|
id?: GetItemId<TSource>
|
|
55
|
-
value?: TGetValue | ((source: TSource, properties: TCustomProperties) => $TValue)
|
|
55
|
+
value?: TGetValue | ((source: TSource, properties: TCustomProperties, index: number) => $TValue)
|
|
56
56
|
properties?: (source: TSource) => TCustomProperties
|
|
57
57
|
label?: GetOptionLabel<TSource, TCustomProperties>
|
|
58
58
|
selectedLabel?: (source: TSource, properties: TCustomProperties) => string
|
|
@@ -91,7 +91,7 @@ export function useFormSelect<
|
|
|
91
91
|
}>
|
|
92
92
|
option?: {
|
|
93
93
|
id?: GetItemId<TSource>
|
|
94
|
-
value?: TGetValue | ((source: TSource, properties: TCustomProperties) => $TValue)
|
|
94
|
+
value?: TGetValue | ((source: TSource, properties: TCustomProperties, index: number) => $TValue)
|
|
95
95
|
properties?: (source: TSource) => TCustomProperties
|
|
96
96
|
label?: GetOptionLabel<TSource, TCustomProperties>
|
|
97
97
|
selectedLabel?: (source: TSource, properties: TCustomProperties) => string
|
|
@@ -130,7 +130,7 @@ export function useFormSelect<
|
|
|
130
130
|
}>
|
|
131
131
|
option: {
|
|
132
132
|
id?: GetItemId<TSource>
|
|
133
|
-
value?: TGetValue | ((source: TSource, properties: TCustomProperties) => $TValue)
|
|
133
|
+
value?: TGetValue | ((source: TSource, properties: TCustomProperties, index: number) => $TValue)
|
|
134
134
|
properties?: (source: TSource) => TCustomProperties
|
|
135
135
|
label: GetOptionLabel<TSource, TCustomProperties>
|
|
136
136
|
selectedLabel?: (source: TSource, properties: TCustomProperties) => string
|
|
@@ -169,7 +169,7 @@ export function useFormSelect<
|
|
|
169
169
|
}>
|
|
170
170
|
option: {
|
|
171
171
|
id: GetItemId<TSource>
|
|
172
|
-
value?: TGetValue | ((source: TSource, properties: TCustomProperties) => $TValue)
|
|
172
|
+
value?: TGetValue | ((source: TSource, properties: TCustomProperties, index: number) => $TValue)
|
|
173
173
|
properties?: (source: TSource) => TCustomProperties
|
|
174
174
|
label?: GetOptionLabel<TSource, TCustomProperties>
|
|
175
175
|
selectedLabel?: (source: TSource, properties: TCustomProperties) => string
|
|
@@ -208,7 +208,7 @@ export function useFormSelect<
|
|
|
208
208
|
}>
|
|
209
209
|
option: {
|
|
210
210
|
id: GetItemId<TSource>
|
|
211
|
-
value?: TGetValue | ((source: TSource, properties: TCustomProperties) => $TValue)
|
|
211
|
+
value?: TGetValue | ((source: TSource, properties: TCustomProperties, index: number) => $TValue)
|
|
212
212
|
properties?: (source: TSource) => TCustomProperties
|
|
213
213
|
label: GetOptionLabel<TSource, TCustomProperties>
|
|
214
214
|
selectedLabel?: (source: TSource, properties: TCustomProperties) => string
|
|
@@ -289,7 +289,7 @@ export function useFormSelect<
|
|
|
289
289
|
active: { multiple: false },
|
|
290
290
|
selected: { multiple: isMultiple },
|
|
291
291
|
},
|
|
292
|
-
properties: (source): FormOptionCollectionItemProperties<TCustomProperties, $TValue> => {
|
|
292
|
+
properties: (source, index): FormOptionCollectionItemProperties<TCustomProperties, $TValue> => {
|
|
293
293
|
if (source === EMPTY_OPTION) {
|
|
294
294
|
const emptyOption = toValue(config?.emptyOption)
|
|
295
295
|
|
|
@@ -305,7 +305,7 @@ export function useFormSelect<
|
|
|
305
305
|
|
|
306
306
|
const customProperties = config?.option?.properties?.(source) ?? ({} as TCustomProperties)
|
|
307
307
|
const label = computed(() => guessLabel(source, customProperties, config?.option?.label))
|
|
308
|
-
const value = computed(() => guessValue(source, customProperties, config?.option?.value) as $TValue)
|
|
308
|
+
const value = computed(() => guessValue(source, customProperties, config?.option?.value, index) as $TValue)
|
|
309
309
|
const disabled = computed(() => isDisabled.value || config?.option?.disabled?.(source, customProperties) === true)
|
|
310
310
|
const searchableTerm = computed(() => config?.option?.searchableTerm?.(source, customProperties))
|
|
311
311
|
const selectedLabel = computed(() => config?.option?.selectedLabel?.(source, customProperties))
|
|
@@ -335,7 +335,7 @@ export function useFormSelect<
|
|
|
335
335
|
|
|
336
336
|
const { items: options } = useSubset(option => option.properties.matching)
|
|
337
337
|
|
|
338
|
-
const { items: selectedOptions
|
|
338
|
+
const { items: selectedOptions } = useFlag('selected')
|
|
339
339
|
|
|
340
340
|
const selectedOption = computed(() => selectedOptions.value[0])
|
|
341
341
|
|
|
@@ -357,18 +357,18 @@ export function useFormSelect<
|
|
|
357
357
|
watch(
|
|
358
358
|
model,
|
|
359
359
|
modelValue => {
|
|
360
|
-
toggleSelectAll(false)
|
|
361
|
-
|
|
362
360
|
if (isMultiple.value) {
|
|
363
361
|
allOptions.value.forEach(option => {
|
|
364
362
|
if ((modelValue as $TValue[]).includes(toRaw(option.properties.value) as $TValue)) {
|
|
365
363
|
option.toggleFlag('selected', true)
|
|
364
|
+
} else {
|
|
365
|
+
option.toggleFlag('selected', false)
|
|
366
366
|
}
|
|
367
367
|
})
|
|
368
368
|
} else {
|
|
369
|
-
allOptions.value
|
|
370
|
-
.
|
|
371
|
-
|
|
369
|
+
allOptions.value.forEach(option => {
|
|
370
|
+
option.toggleFlag('selected', toRaw(option.properties.value) === toRaw(modelValue))
|
|
371
|
+
})
|
|
372
372
|
}
|
|
373
373
|
},
|
|
374
374
|
{ immediate: true }
|
|
@@ -379,11 +379,11 @@ export function useFormSelect<
|
|
|
379
379
|
newValues => {
|
|
380
380
|
if (isMultiple.value) {
|
|
381
381
|
model.value = newValues as TMultiple extends true ? $TValue[] : $TValue
|
|
382
|
-
} else {
|
|
382
|
+
} else if (newValues.length > 0) {
|
|
383
383
|
model.value = newValues[0] as TMultiple extends true ? $TValue[] : $TValue
|
|
384
384
|
}
|
|
385
385
|
},
|
|
386
|
-
{ deep: 1 }
|
|
386
|
+
{ deep: 1, flush: 'post' }
|
|
387
387
|
)
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<svg
|
|
2
|
+
<svg
|
|
3
|
+
v-if="icon.paths.length > 0"
|
|
4
|
+
:viewBox="icon.viewBox"
|
|
5
|
+
class="display-icon-single"
|
|
6
|
+
:class="spinClass"
|
|
7
|
+
v-bind="icon.bindings"
|
|
8
|
+
>
|
|
3
9
|
<path
|
|
4
10
|
v-for="(path, index) of icon.paths"
|
|
5
11
|
:key="index"
|
|
@@ -13,11 +19,16 @@
|
|
|
13
19
|
|
|
14
20
|
<script lang="ts" setup>
|
|
15
21
|
import type { IconSingle } from './types.ts'
|
|
22
|
+
import { computed } from 'vue'
|
|
16
23
|
|
|
17
|
-
defineProps<{
|
|
24
|
+
const { icon } = defineProps<{
|
|
18
25
|
icon: IconSingle
|
|
19
26
|
stroke?: string
|
|
20
27
|
}>()
|
|
28
|
+
|
|
29
|
+
const spinClass = computed(() => {
|
|
30
|
+
return icon.bindings.style?.['--spin-duration'] ? 'spinning' : undefined
|
|
31
|
+
})
|
|
21
32
|
</script>
|
|
22
33
|
|
|
23
34
|
<style lang="postcss" scoped>
|
|
@@ -28,8 +39,24 @@ defineProps<{
|
|
|
28
39
|
grid-row: 1;
|
|
29
40
|
grid-column: 1;
|
|
30
41
|
|
|
42
|
+
&.spinning {
|
|
43
|
+
animation-name: spin;
|
|
44
|
+
animation-timing-function: linear;
|
|
45
|
+
animation-iteration-count: infinite;
|
|
46
|
+
animation-duration: var(--spin-duration, 2s);
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
.icon-path {
|
|
32
50
|
fill: currentColor;
|
|
33
51
|
}
|
|
34
52
|
}
|
|
53
|
+
|
|
54
|
+
@keyframes spin {
|
|
55
|
+
from {
|
|
56
|
+
transform: rotate(0deg);
|
|
57
|
+
}
|
|
58
|
+
to {
|
|
59
|
+
transform: rotate(360deg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
35
62
|
</style>
|
|
@@ -16,12 +16,15 @@ export function createIconBindings(transforms: IconTransforms): IconBindings {
|
|
|
16
16
|
|
|
17
17
|
const shouldScale = scaleX !== 1 || scaleY !== 1
|
|
18
18
|
|
|
19
|
+
const spinDuration = typeof transforms.spin === 'number' ? transforms.spin : transforms.spin === true ? 2 : 0
|
|
20
|
+
|
|
19
21
|
return {
|
|
20
22
|
style: {
|
|
21
23
|
color: transforms.color,
|
|
22
24
|
translate: shouldTranslate ? `${(translateX / 16) * 100}% ${(translateY / 16) * 100}%` : undefined,
|
|
23
25
|
rotate: transforms.rotate ? `${transforms.rotate}deg` : undefined,
|
|
24
26
|
scale: shouldScale ? `${scaleX} ${scaleY}` : undefined,
|
|
27
|
+
'--spin-duration': spinDuration ? `${spinDuration}s` : undefined,
|
|
25
28
|
},
|
|
26
29
|
}
|
|
27
30
|
}
|
|
@@ -9,6 +9,7 @@ export type IconTransforms = {
|
|
|
9
9
|
size?: number | [number, number]
|
|
10
10
|
rotate?: number
|
|
11
11
|
flip?: 'horizontal' | 'vertical' | 'both'
|
|
12
|
+
spin?: boolean | number
|
|
12
13
|
color?: string
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -24,6 +25,7 @@ export type IconBindings = {
|
|
|
24
25
|
rotate?: string
|
|
25
26
|
translate?: string
|
|
26
27
|
scale?: string
|
|
28
|
+
'--spin-duration'?: string
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createEmptyFilter } from '@core/packages/query-builder/filter/create-empty-filter.ts'
|
|
2
|
+
import { createQueryBuilderGroup } from '@core/packages/query-builder/group/create-query-builder-group.ts'
|
|
3
|
+
import type {
|
|
4
|
+
GroupOperator,
|
|
5
|
+
QueryBuilderFilter,
|
|
6
|
+
QueryBuilderGroup,
|
|
7
|
+
QueryBuilderSchema,
|
|
8
|
+
} from '@core/packages/query-builder/types.ts'
|
|
9
|
+
|
|
10
|
+
export function convertFilterToGroup(
|
|
11
|
+
filter: QueryBuilderFilter,
|
|
12
|
+
groupOperator: GroupOperator,
|
|
13
|
+
schema: QueryBuilderSchema
|
|
14
|
+
): QueryBuilderGroup {
|
|
15
|
+
return createQueryBuilderGroup({
|
|
16
|
+
operator: groupOperator,
|
|
17
|
+
children: [filter, createEmptyFilter(schema)],
|
|
18
|
+
schema,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createQueryBuilderFilter } from '@core/packages/query-builder/filter/create-query-builder-filter.ts'
|
|
2
|
+
import type { QueryBuilderFilter, QueryBuilderSchema } from '@core/packages/query-builder/types.ts'
|
|
3
|
+
|
|
4
|
+
export function createEmptyFilter(schema: QueryBuilderSchema): QueryBuilderFilter {
|
|
5
|
+
return createQueryBuilderFilter({
|
|
6
|
+
property: '',
|
|
7
|
+
operator: 'contains',
|
|
8
|
+
value: '',
|
|
9
|
+
schema,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useFilterOperator } from '@core/packages/query-builder/filter/use-filter-operator.ts'
|
|
2
|
+
import { useFilterProperty } from '@core/packages/query-builder/filter/use-filter-property.ts'
|
|
3
|
+
import { useFilterValue } from '@core/packages/query-builder/filter/use-filter-value.ts'
|
|
4
|
+
import { useRawFilter } from '@core/packages/query-builder/filter/use-raw-filter.ts'
|
|
5
|
+
import type { PropertyOperator, QueryBuilderConfig, QueryBuilderFilter } from '@core/packages/query-builder/types.ts'
|
|
6
|
+
import { logicAnd } from '@vueuse/math'
|
|
7
|
+
import { computed, reactive, watch } from 'vue'
|
|
8
|
+
|
|
9
|
+
let nextId = 1
|
|
10
|
+
|
|
11
|
+
export function createQueryBuilderFilter(config: QueryBuilderConfig): QueryBuilderFilter {
|
|
12
|
+
const id = `filter-${nextId++}`
|
|
13
|
+
|
|
14
|
+
const { property, isPropertyValid, propertyOptions, currentPropertySchema } = useFilterProperty(
|
|
15
|
+
config.schema,
|
|
16
|
+
config.property
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const { operator, currentOperatorSchema, isOperatorValid, operatorsSchema, operatorOptions } = useFilterOperator(
|
|
20
|
+
currentPropertySchema,
|
|
21
|
+
config.operator
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const { value, isValueValid, valuesSchema, valueOptions } = useFilterValue(currentOperatorSchema, config.value)
|
|
25
|
+
|
|
26
|
+
const isValid = logicAnd(isPropertyValid, isOperatorValid, isValueValid)
|
|
27
|
+
|
|
28
|
+
function checkValue() {
|
|
29
|
+
const values = operatorsSchema.value[operator.value].values
|
|
30
|
+
|
|
31
|
+
if (values && values[value.value] === undefined) {
|
|
32
|
+
value.value = Object.keys(values)[0]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
watch(operatorsSchema, () => {
|
|
37
|
+
if (operatorsSchema.value !== undefined && operatorsSchema.value[operator.value] === undefined) {
|
|
38
|
+
operator.value = Object.keys(operatorsSchema.value)[0] as PropertyOperator
|
|
39
|
+
} else {
|
|
40
|
+
checkValue()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
watch(operator, () => checkValue())
|
|
45
|
+
|
|
46
|
+
const valueType = computed(() => {
|
|
47
|
+
if (currentOperatorSchema.value?.expectValue === false) {
|
|
48
|
+
return 'none'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (valuesSchema.value !== undefined) {
|
|
52
|
+
return 'select'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'input'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const rawFilter = useRawFilter(property, operator, value)
|
|
59
|
+
|
|
60
|
+
return reactive({
|
|
61
|
+
id,
|
|
62
|
+
property,
|
|
63
|
+
operator,
|
|
64
|
+
value,
|
|
65
|
+
isGroup: false,
|
|
66
|
+
rawFilter,
|
|
67
|
+
propertyOptions,
|
|
68
|
+
operatorOptions,
|
|
69
|
+
valueOptions,
|
|
70
|
+
isValid,
|
|
71
|
+
valueType,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createQueryBuilderFilter } from '@core/packages/query-builder/filter/create-query-builder-filter.ts'
|
|
2
|
+
import type { QueryBuilderFilter, QueryBuilderSchema } from '@core/packages/query-builder/types.ts'
|
|
3
|
+
|
|
4
|
+
export function duplicateFilter(filter: QueryBuilderFilter, schema: QueryBuilderSchema) {
|
|
5
|
+
return createQueryBuilderFilter({
|
|
6
|
+
property: filter.property!,
|
|
7
|
+
operator: filter.operator!,
|
|
8
|
+
value: filter.value,
|
|
9
|
+
schema,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parse } from 'complex-matcher'
|
|
2
|
+
import type { Property as PropertyNode } from 'complex-matcher'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a value looks like a filter expression (e.g., a property path like "boot:firmware:uefi")
|
|
6
|
+
* rather than a literal string to match.
|
|
7
|
+
*
|
|
8
|
+
* Returns true if:
|
|
9
|
+
* - The value can be parsed by complex-matcher
|
|
10
|
+
* - The parsed result is a PropertyNode (indicating a property path)
|
|
11
|
+
* - Contains at least one colon suggesting property nesting
|
|
12
|
+
*/
|
|
13
|
+
export function isFilterExpression(value: string): boolean {
|
|
14
|
+
if (!value.includes(':')) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const parsed = parse(value)
|
|
20
|
+
// Check if it parses as a PropertyNode, which indicates it's a property path
|
|
21
|
+
// PropertyNode instances have a 'name' property and potentially a 'child'
|
|
22
|
+
return parsed != null && 'name' in parsed && typeof (parsed as PropertyNode).name === 'string'
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parse, Property as PropertyNode, StringNode, NumberOrStringNode, type Node } from 'complex-matcher'
|
|
2
|
+
|
|
3
|
+
interface PropertyPathResult {
|
|
4
|
+
propertyPath: string
|
|
5
|
+
terminalValue: string | undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts the property path and terminal value from a parsed Complex Matcher node.
|
|
10
|
+
* Traverses nested Property nodes to build the full path (e.g., "boot:firmware")
|
|
11
|
+
* and returns the terminal string value if present.
|
|
12
|
+
*/
|
|
13
|
+
function extractPropertyPathAndValue(node: Node): PropertyPathResult {
|
|
14
|
+
const propertyNames: string[] = []
|
|
15
|
+
let currentNode: Node = node
|
|
16
|
+
|
|
17
|
+
// Traverse nested Property nodes to accumulate the full path
|
|
18
|
+
while (currentNode instanceof PropertyNode) {
|
|
19
|
+
propertyNames.push(currentNode.name)
|
|
20
|
+
currentNode = currentNode.child
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const propertyPath = propertyNames.join(':')
|
|
24
|
+
|
|
25
|
+
// Check for terminal string value (StringNode or NumberOrStringNode)
|
|
26
|
+
let terminalValue: string | undefined
|
|
27
|
+
if (currentNode instanceof StringNode || currentNode instanceof NumberOrStringNode) {
|
|
28
|
+
terminalValue = currentNode.value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { propertyPath, terminalValue }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalizes a boolean filter expression to Complex Matcher boolean syntax.
|
|
36
|
+
* Converts "prop:true" to "prop?" and "prop:false" to "!prop?".
|
|
37
|
+
* Returns the original expression if it's not a boolean expression or if parsing fails.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeBooleanExpression(expression: string): string {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = parse(expression)
|
|
42
|
+
const { propertyPath, terminalValue } = extractPropertyPathAndValue(parsed)
|
|
43
|
+
|
|
44
|
+
if (propertyPath === '' || terminalValue === undefined) {
|
|
45
|
+
return expression
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const normalizedValue = terminalValue.trim().toLowerCase()
|
|
49
|
+
|
|
50
|
+
if (normalizedValue === 'true') {
|
|
51
|
+
return `${propertyPath}?`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (normalizedValue === 'false') {
|
|
55
|
+
return `!${propertyPath}?`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return expression
|
|
59
|
+
} catch {
|
|
60
|
+
// If parsing fails, return the original expression
|
|
61
|
+
return expression
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RegExpNode } from 'complex-matcher'
|
|
2
|
+
import { escapeRegExp } from 'lodash-es'
|
|
3
|
+
|
|
4
|
+
export function parseIsValue(value: string = '') {
|
|
5
|
+
if (value.match(/^\d+$/)) {
|
|
6
|
+
return value
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return new RegExpNode(escapeRegExp(value)).toString().replace(/^\/(.*)\/$/, '/^$1$/')
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RegExpNode } from 'complex-matcher'
|
|
2
|
+
|
|
3
|
+
export function parseRegexValue(value: string = '') {
|
|
4
|
+
const regexMatch = value.match(/^\/(.*)\/([gimsuy]*)$/)
|
|
5
|
+
|
|
6
|
+
if (regexMatch) {
|
|
7
|
+
const pattern = regexMatch[1]
|
|
8
|
+
const flags = regexMatch[2]
|
|
9
|
+
return new RegExpNode(pattern, flags).toString()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return new RegExpNode(value).toString()
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function renderFilter(property: string, value: string, negate: boolean) {
|
|
2
|
+
if (value === '') {
|
|
3
|
+
return ''
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const negateToken = negate ? '!' : ''
|
|
7
|
+
|
|
8
|
+
const joinToken = property === '' ? '' : ':'
|
|
9
|
+
|
|
10
|
+
return `${negateToken}${property}${joinToken}${value}`
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OperatorSchema, PropertyOperator, PropertySchema } from '@core/packages/query-builder/types.ts'
|
|
2
|
+
import { type ComputedRef, ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useFilterOperator(propertySchema: ComputedRef<PropertySchema>, initialValue: PropertyOperator) {
|
|
5
|
+
const operator = ref<PropertyOperator>(initialValue)
|
|
6
|
+
|
|
7
|
+
const operatorsSchema = computed(() => {
|
|
8
|
+
return propertySchema.value?.operators
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const currentOperatorSchema = computed(() => {
|
|
12
|
+
if (operatorsSchema.value === undefined) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return operatorsSchema.value[operator.value]
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const isOperatorValid = computed(() => currentOperatorSchema.value !== undefined)
|
|
20
|
+
|
|
21
|
+
const operatorOptions = computed((previousValue: OperatorSchema[] | undefined) => {
|
|
22
|
+
if (operatorsSchema.value !== undefined) {
|
|
23
|
+
return Object.values(operatorsSchema.value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return previousValue ?? []
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
operator,
|
|
31
|
+
operatorsSchema,
|
|
32
|
+
currentOperatorSchema,
|
|
33
|
+
isOperatorValid,
|
|
34
|
+
operatorOptions,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { QueryBuilderSchema } from '@core/packages/query-builder/types.ts'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useFilterProperty(schema: QueryBuilderSchema, initialValue: string) {
|
|
5
|
+
const property = ref<string>(initialValue)
|
|
6
|
+
|
|
7
|
+
const currentPropertySchema = computed(() => schema[property.value])
|
|
8
|
+
|
|
9
|
+
const isPropertyValid = computed(() => currentPropertySchema.value !== undefined)
|
|
10
|
+
|
|
11
|
+
const propertyOptions = computed(() => Object.values(schema))
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
property,
|
|
15
|
+
isPropertyValid,
|
|
16
|
+
propertyOptions,
|
|
17
|
+
currentPropertySchema,
|
|
18
|
+
}
|
|
19
|
+
}
|