@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.
Files changed (104) hide show
  1. package/lib/components/button-group/VtsButtonGroup.vue +1 -1
  2. package/lib/components/columns/VtsColumns.vue +1 -1
  3. package/lib/components/console/VtsLayoutConsole.vue +1 -1
  4. package/lib/components/console/VtsRemoteConsole.vue +1 -1
  5. package/lib/components/layout/VtsLayoutSidebar.vue +5 -5
  6. package/lib/components/menu/MenuList.vue +1 -1
  7. package/lib/components/query-builder/VtsQueryBuilder.vue +104 -0
  8. package/lib/components/query-builder/VtsQueryBuilderButton.vue +52 -0
  9. package/lib/components/query-builder/VtsQueryBuilderFilter.vue +126 -0
  10. package/lib/components/query-builder/VtsQueryBuilderGroup.vue +157 -0
  11. package/lib/components/query-builder/VtsQueryBuilderModal.vue +62 -0
  12. package/lib/components/query-builder/VtsQueryBuilderRow.vue +88 -0
  13. package/lib/components/query-builder/VtsQueryBuilderTreeLine.vue +42 -0
  14. package/lib/components/quick-info-row/VtsQuickInfoRow.vue +1 -1
  15. package/lib/components/select/VtsSelect.vue +6 -2
  16. package/lib/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue +1 -1
  17. package/lib/components/tab/TabItem.vue +2 -2
  18. package/lib/components/tree/VtsTreeItem.vue +1 -1
  19. package/lib/components/ui/button-icon/UiButtonIcon.vue +6 -10
  20. package/lib/components/ui/input/UiInput.vue +19 -3
  21. package/lib/components/ui/modal/UiModal.vue +0 -1
  22. package/lib/components/ui/panel/UiPanel.vue +1 -1
  23. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +4 -4
  24. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +2 -2
  25. package/lib/composables/debounced-ref.composable.ts +40 -0
  26. package/lib/composables/pagination.composable.ts +1 -1
  27. package/lib/composables/tree-filter.composable.ts +16 -7
  28. package/lib/i18n.ts +4 -0
  29. package/lib/icons/fa-icons.ts +7 -1
  30. package/lib/locales/cs.json +74 -17
  31. package/lib/locales/da.json +14 -14
  32. package/lib/locales/de.json +79 -19
  33. package/lib/locales/en.json +44 -17
  34. package/lib/locales/es.json +34 -15
  35. package/lib/locales/fa.json +14 -14
  36. package/lib/locales/fi.json +0 -8
  37. package/lib/locales/fr.json +45 -18
  38. package/lib/locales/it.json +15 -14
  39. package/lib/locales/ko.json +59 -10
  40. package/lib/locales/nb-NO.json +0 -6
  41. package/lib/locales/nl.json +94 -17
  42. package/lib/locales/pl.json +0 -6
  43. package/lib/locales/pt-BR.json +10 -12
  44. package/lib/locales/ru.json +14 -14
  45. package/lib/locales/sv.json +14 -14
  46. package/lib/locales/uk.json +14 -14
  47. package/lib/locales/zh-Hans.json +884 -0
  48. package/lib/packages/collection/use-collection.ts +6 -6
  49. package/lib/packages/form-select/guess-value.ts +3 -2
  50. package/lib/packages/form-select/types.ts +1 -1
  51. package/lib/packages/form-select/use-form-select-controller.ts +9 -1
  52. package/lib/packages/form-select/use-form-select.ts +15 -15
  53. package/lib/packages/icon/DisplayIconSingle.vue +29 -2
  54. package/lib/packages/icon/create-icon-bindings.ts +3 -0
  55. package/lib/packages/icon/types.ts +2 -0
  56. package/lib/packages/query-builder/filter/convert-filter-to-group.ts +20 -0
  57. package/lib/packages/query-builder/filter/create-empty-filter.ts +11 -0
  58. package/lib/packages/query-builder/filter/create-query-builder-filter.ts +73 -0
  59. package/lib/packages/query-builder/filter/duplicate-filter.ts +11 -0
  60. package/lib/packages/query-builder/filter/is-filter-expression.ts +26 -0
  61. package/lib/packages/query-builder/filter/normalize-boolean-expression.ts +63 -0
  62. package/lib/packages/query-builder/filter/parsers/parse-contains-value.ts +5 -0
  63. package/lib/packages/query-builder/filter/parsers/parse-ends-with-value.ts +6 -0
  64. package/lib/packages/query-builder/filter/parsers/parse-glob-value.ts +5 -0
  65. package/lib/packages/query-builder/filter/parsers/parse-is-value.ts +10 -0
  66. package/lib/packages/query-builder/filter/parsers/parse-number-value.ts +9 -0
  67. package/lib/packages/query-builder/filter/parsers/parse-regex-value.ts +13 -0
  68. package/lib/packages/query-builder/filter/parsers/parse-starts-with-value.ts +6 -0
  69. package/lib/packages/query-builder/filter/render-filter.ts +11 -0
  70. package/lib/packages/query-builder/filter/use-filter-operator.ts +36 -0
  71. package/lib/packages/query-builder/filter/use-filter-property.ts +19 -0
  72. package/lib/packages/query-builder/filter/use-filter-value.ts +42 -0
  73. package/lib/packages/query-builder/filter/use-raw-filter.ts +66 -0
  74. package/lib/packages/query-builder/group/build-root-group.ts +20 -0
  75. package/lib/packages/query-builder/group/create-query-builder-group.ts +117 -0
  76. package/lib/packages/query-builder/group/duplicate-group.ts +13 -0
  77. package/lib/packages/query-builder/node/duplicate-node.ts +7 -0
  78. package/lib/packages/query-builder/node/get-comparison-operator.ts +18 -0
  79. package/lib/packages/query-builder/node/get-raw-filter.ts +15 -0
  80. package/lib/packages/query-builder/node/handle-comparison-node.ts +18 -0
  81. package/lib/packages/query-builder/node/handle-glob-pattern-node.ts +17 -0
  82. package/lib/packages/query-builder/node/handle-group-node.ts +22 -0
  83. package/lib/packages/query-builder/node/handle-node.ts +72 -0
  84. package/lib/packages/query-builder/node/handle-null-node.ts +11 -0
  85. package/lib/packages/query-builder/node/handle-property-group-node.ts +25 -0
  86. package/lib/packages/query-builder/node/handle-property-node.ts +94 -0
  87. package/lib/packages/query-builder/node/handle-regexp-node.ts +62 -0
  88. package/lib/packages/query-builder/node/handle-string-or-number-node.ts +48 -0
  89. package/lib/packages/query-builder/node/handle-truthy-property-node.ts +42 -0
  90. package/lib/packages/query-builder/query-builder-error.ts +7 -0
  91. package/lib/packages/query-builder/schema/parse-operators.ts +28 -0
  92. package/lib/packages/query-builder/schema/parse-values.ts +19 -0
  93. package/lib/packages/query-builder/schema/use-query-builder-schema.ts +25 -0
  94. package/lib/packages/query-builder/types.ts +119 -0
  95. package/lib/packages/query-builder/use-query-builder-filter.ts +39 -0
  96. package/lib/packages/query-builder/use-query-builder.ts +52 -0
  97. package/lib/stores/panel.store.ts +1 -1
  98. package/lib/stores/sidebar.store.ts +6 -6
  99. package/lib/stores/ui.store.ts +12 -8
  100. package/lib/tables/column-definitions/input-column.ts +2 -2
  101. package/lib/utils/query-builder/use-boolean-schema.ts +11 -0
  102. package/lib/utils/query-builder/use-number-schema.ts +20 -0
  103. package/lib/utils/query-builder/use-string-schema.ts +36 -0
  104. 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: [shift(), flip(), size()],
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, toggleAll: toggleSelectAll } = useFlag('selected')
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
- .find(option => toRaw(option.properties.value) === toRaw(modelValue))
371
- ?.toggleFlag('selected', true)
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 v-if="icon.paths.length > 0" :viewBox="icon.viewBox" class="display-icon-single" v-bind="icon.bindings">
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,5 @@
1
+ import { StringNode } from 'complex-matcher'
2
+
3
+ export function parseContainsValue(value: string = '') {
4
+ return new StringNode(value).toString()
5
+ }
@@ -0,0 +1,6 @@
1
+ import { RegExpNode } from 'complex-matcher'
2
+ import { escapeRegExp } from 'lodash-es'
3
+
4
+ export function parseEndsWithValue(value: string = '') {
5
+ return new RegExpNode(escapeRegExp(value)).toString().replace(/^\/(.*)\/$/, '/$1$/i')
6
+ }
@@ -0,0 +1,5 @@
1
+ import { GlobPattern as GlobPatternNode } from 'complex-matcher'
2
+
3
+ export function parseGlobValue(value: string = '') {
4
+ return new GlobPatternNode(value).toString().replace(/\s+/g, '*')
5
+ }
@@ -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,9 @@
1
+ export function parseNumberValue(value: string = '') {
2
+ const number = parseFloat(value)
3
+
4
+ if (isNaN(number)) {
5
+ return ''
6
+ }
7
+
8
+ return number
9
+ }
@@ -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,6 @@
1
+ import { RegExpNode } from 'complex-matcher'
2
+ import { escapeRegExp } from 'lodash-es'
3
+
4
+ export function parseStartsWithValue(value: string = '') {
5
+ return new RegExpNode(escapeRegExp(value)).toString().replace(/^\/(.*)\/$/, '/^$1/i')
6
+ }
@@ -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
+ }