@xen-orchestra/web-core 0.42.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 (114) hide show
  1. package/lib/components/banner/VtsBanner.vue +50 -0
  2. package/lib/components/button-group/VtsButtonGroup.vue +1 -1
  3. package/lib/components/columns/VtsColumns.vue +1 -1
  4. package/lib/components/console/VtsLayoutConsole.vue +1 -1
  5. package/lib/components/console/VtsRemoteConsole.vue +1 -1
  6. package/lib/components/input-wrapper/VtsInputWrapper.vue +1 -0
  7. package/lib/components/layout/VtsLayoutSidebar.vue +5 -5
  8. package/lib/components/menu/MenuList.vue +1 -1
  9. package/lib/components/query-builder/VtsQueryBuilder.vue +104 -0
  10. package/lib/components/query-builder/VtsQueryBuilderButton.vue +52 -0
  11. package/lib/components/query-builder/VtsQueryBuilderFilter.vue +126 -0
  12. package/lib/components/query-builder/VtsQueryBuilderGroup.vue +157 -0
  13. package/lib/components/query-builder/VtsQueryBuilderModal.vue +62 -0
  14. package/lib/components/query-builder/VtsQueryBuilderRow.vue +88 -0
  15. package/lib/components/query-builder/VtsQueryBuilderTreeLine.vue +42 -0
  16. package/lib/components/quick-info-row/VtsQuickInfoRow.vue +1 -1
  17. package/lib/components/select/VtsSelect.vue +6 -2
  18. package/lib/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue +1 -1
  19. package/lib/components/tab/TabItem.vue +2 -2
  20. package/lib/components/tree/VtsTreeItem.vue +1 -1
  21. package/lib/components/ui/button-icon/UiButtonIcon.vue +6 -10
  22. package/lib/components/ui/checkbox/UiCheckbox.vue +1 -1
  23. package/lib/components/ui/head-bar/UiHeadBar.vue +1 -0
  24. package/lib/components/ui/input/UiInput.vue +19 -3
  25. package/lib/components/ui/modal/UiModal.vue +0 -1
  26. package/lib/components/ui/panel/UiPanel.vue +1 -1
  27. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +4 -4
  28. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +2 -2
  29. package/lib/components/ui/radio-button/UiRadioButton.vue +1 -1
  30. package/lib/components/ui/radio-button-group/UiRadioButtonGroup.vue +16 -3
  31. package/lib/components/ui/rich-radio-button/UiRichRadioButton.vue +247 -0
  32. package/lib/composables/debounced-ref.composable.ts +40 -0
  33. package/lib/composables/pagination.composable.ts +1 -1
  34. package/lib/composables/tree-filter.composable.ts +16 -7
  35. package/lib/i18n.ts +4 -0
  36. package/lib/icons/fa-icons.ts +7 -1
  37. package/lib/layouts/CoreLayout.vue +26 -0
  38. package/lib/locales/cs.json +74 -17
  39. package/lib/locales/da.json +14 -14
  40. package/lib/locales/de.json +79 -19
  41. package/lib/locales/en.json +46 -17
  42. package/lib/locales/es.json +34 -15
  43. package/lib/locales/fa.json +14 -14
  44. package/lib/locales/fi.json +0 -8
  45. package/lib/locales/fr.json +47 -18
  46. package/lib/locales/it.json +15 -14
  47. package/lib/locales/ko.json +59 -10
  48. package/lib/locales/nb-NO.json +0 -6
  49. package/lib/locales/nl.json +94 -17
  50. package/lib/locales/pl.json +0 -6
  51. package/lib/locales/pt-BR.json +10 -12
  52. package/lib/locales/ru.json +14 -14
  53. package/lib/locales/sv.json +14 -14
  54. package/lib/locales/uk.json +14 -14
  55. package/lib/locales/zh-Hans.json +884 -0
  56. package/lib/packages/collection/use-collection.ts +6 -6
  57. package/lib/packages/form-select/guess-value.ts +3 -2
  58. package/lib/packages/form-select/types.ts +1 -1
  59. package/lib/packages/form-select/use-form-select-controller.ts +9 -1
  60. package/lib/packages/form-select/use-form-select.ts +15 -15
  61. package/lib/packages/icon/DisplayIconSingle.vue +29 -2
  62. package/lib/packages/icon/create-icon-bindings.ts +3 -0
  63. package/lib/packages/icon/types.ts +2 -0
  64. package/lib/packages/query-builder/filter/convert-filter-to-group.ts +20 -0
  65. package/lib/packages/query-builder/filter/create-empty-filter.ts +11 -0
  66. package/lib/packages/query-builder/filter/create-query-builder-filter.ts +73 -0
  67. package/lib/packages/query-builder/filter/duplicate-filter.ts +11 -0
  68. package/lib/packages/query-builder/filter/is-filter-expression.ts +26 -0
  69. package/lib/packages/query-builder/filter/normalize-boolean-expression.ts +63 -0
  70. package/lib/packages/query-builder/filter/parsers/parse-contains-value.ts +5 -0
  71. package/lib/packages/query-builder/filter/parsers/parse-ends-with-value.ts +6 -0
  72. package/lib/packages/query-builder/filter/parsers/parse-glob-value.ts +5 -0
  73. package/lib/packages/query-builder/filter/parsers/parse-is-value.ts +10 -0
  74. package/lib/packages/query-builder/filter/parsers/parse-number-value.ts +9 -0
  75. package/lib/packages/query-builder/filter/parsers/parse-regex-value.ts +13 -0
  76. package/lib/packages/query-builder/filter/parsers/parse-starts-with-value.ts +6 -0
  77. package/lib/packages/query-builder/filter/render-filter.ts +11 -0
  78. package/lib/packages/query-builder/filter/use-filter-operator.ts +36 -0
  79. package/lib/packages/query-builder/filter/use-filter-property.ts +19 -0
  80. package/lib/packages/query-builder/filter/use-filter-value.ts +42 -0
  81. package/lib/packages/query-builder/filter/use-raw-filter.ts +66 -0
  82. package/lib/packages/query-builder/group/build-root-group.ts +20 -0
  83. package/lib/packages/query-builder/group/create-query-builder-group.ts +117 -0
  84. package/lib/packages/query-builder/group/duplicate-group.ts +13 -0
  85. package/lib/packages/query-builder/node/duplicate-node.ts +7 -0
  86. package/lib/packages/query-builder/node/get-comparison-operator.ts +18 -0
  87. package/lib/packages/query-builder/node/get-raw-filter.ts +15 -0
  88. package/lib/packages/query-builder/node/handle-comparison-node.ts +18 -0
  89. package/lib/packages/query-builder/node/handle-glob-pattern-node.ts +17 -0
  90. package/lib/packages/query-builder/node/handle-group-node.ts +22 -0
  91. package/lib/packages/query-builder/node/handle-node.ts +72 -0
  92. package/lib/packages/query-builder/node/handle-null-node.ts +11 -0
  93. package/lib/packages/query-builder/node/handle-property-group-node.ts +25 -0
  94. package/lib/packages/query-builder/node/handle-property-node.ts +94 -0
  95. package/lib/packages/query-builder/node/handle-regexp-node.ts +62 -0
  96. package/lib/packages/query-builder/node/handle-string-or-number-node.ts +48 -0
  97. package/lib/packages/query-builder/node/handle-truthy-property-node.ts +42 -0
  98. package/lib/packages/query-builder/query-builder-error.ts +7 -0
  99. package/lib/packages/query-builder/schema/parse-operators.ts +28 -0
  100. package/lib/packages/query-builder/schema/parse-values.ts +19 -0
  101. package/lib/packages/query-builder/schema/use-query-builder-schema.ts +25 -0
  102. package/lib/packages/query-builder/types.ts +119 -0
  103. package/lib/packages/query-builder/use-query-builder-filter.ts +39 -0
  104. package/lib/packages/query-builder/use-query-builder.ts +52 -0
  105. package/lib/packages/remote-resource/define-remote-resource.ts +11 -9
  106. package/lib/packages/remote-resource/sse.store.ts +31 -3
  107. package/lib/stores/panel.store.ts +1 -1
  108. package/lib/stores/sidebar.store.ts +6 -6
  109. package/lib/stores/ui.store.ts +12 -8
  110. package/lib/tables/column-definitions/input-column.ts +2 -2
  111. package/lib/utils/query-builder/use-boolean-schema.ts +11 -0
  112. package/lib/utils/query-builder/use-number-schema.ts +20 -0
  113. package/lib/utils/query-builder/use-string-schema.ts +36 -0
  114. package/package.json +3 -1
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div :class="className" class="vts-banner">
3
+ <slot />
4
+ <div v-if="slots.addons">
5
+ <slot name="addons" />
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { toVariants } from '@core/utils/to-variants.util.ts'
12
+ import { computed } from 'vue'
13
+
14
+ export type BannerAccent = 'brand' | 'warning' | 'danger'
15
+
16
+ const { accent } = defineProps<{
17
+ accent: BannerAccent
18
+ }>()
19
+
20
+ const slots = defineSlots<{
21
+ default(): any
22
+ addons?(): any
23
+ }>()
24
+
25
+ const className = computed(() => toVariants({ accent }))
26
+ </script>
27
+
28
+ <style scoped lang="postcss">
29
+ .vts-banner {
30
+ display: flex;
31
+ justify-content: center;
32
+ align-items: center;
33
+ gap: 1.6rem;
34
+ padding: 0.8rem 1.6rem;
35
+ border-block-end: 0.1rem solid var(--color-neutral-border);
36
+
37
+ /* ACCENT */
38
+ &.accent--brand {
39
+ background-color: var(--color-brand-background-selected);
40
+ }
41
+
42
+ &.accent--warning {
43
+ background-color: var(--color-warning-background-selected);
44
+ }
45
+
46
+ &.accent--danger {
47
+ background-color: var(--color-danger-background-selected);
48
+ }
49
+ }
50
+ </style>
@@ -30,7 +30,7 @@ const slots = defineSlots<{
30
30
  align-items: center;
31
31
  gap: 1.6rem;
32
32
 
33
- @media (--mobile) {
33
+ @media (--small) {
34
34
  flex-direction: column-reverse;
35
35
  }
36
36
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="vts-columns" :class="{ mobile: uiStore.isMobile }">
2
+ <div class="vts-columns" :class="{ mobile: uiStore.isSmall }">
3
3
  <component :is="nodes[index - 1] ?? VtsColumn" v-for="index of columns" :key="index" />
4
4
  </div>
5
5
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="uiStore.isMobile ? 'mobile' : undefined" class="vts-layout-console">
2
+ <div :class="uiStore.isSmall ? 'mobile' : undefined" class="vts-layout-console">
3
3
  <slot />
4
4
  <UiCard class="card">
5
5
  <slot name="actions" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="uiStore.isMobile ? 'mobile' : undefined" class="vts-remote-console">
2
+ <div :class="uiStore.isSmall ? 'mobile' : undefined" class="vts-remote-console">
3
3
  <VtsStateHero v-if="!isReady" format="page" type="busy" size="medium">{{ t('loading') }}</VtsStateHero>
4
4
  <div ref="console-container" class="console" />
5
5
  </div>
@@ -78,6 +78,7 @@ provide(IK_INPUT_WRAPPER_CONTROLLER, wrapperController)
78
78
  display: flex;
79
79
  flex-direction: column;
80
80
  gap: 0.4rem;
81
+ min-width: 0;
81
82
 
82
83
  .label {
83
84
  min-height: 2.4rem;
@@ -1,14 +1,14 @@
1
1
  <template>
2
2
  <div
3
3
  :class="{
4
- locked: sidebar.isLocked && !ui.isMobile,
4
+ locked: sidebar.isLocked && !ui.isSmall,
5
5
  expanded: sidebar.isExpanded,
6
- mobile: ui.isMobile,
7
- 'border-right': !ui.isMobile,
6
+ mobile: ui.isSmall,
7
+ 'border-right': !ui.isSmall,
8
8
  }"
9
9
  class="vts-layout-sidebar"
10
10
  >
11
- <div v-if="!ui.isMobile" class="lock">
11
+ <div v-if="!ui.isSmall" class="lock">
12
12
  <UiButtonIcon
13
13
  v-tooltip="{
14
14
  content: sidebar.isLocked ? t('action:sidebar-unlock') : t('action:sidebar-lock'),
@@ -30,7 +30,7 @@
30
30
  <slot name="footer" />
31
31
  </div>
32
32
  <div
33
- v-if="!ui.isMobile"
33
+ v-if="!ui.isSmall"
34
34
  :class="{ active: sidebar.isResizing }"
35
35
  class="resize-handle"
36
36
  @mousedown="sidebar.startResize"
@@ -90,7 +90,7 @@ const open = (event: MouseEvent) => {
90
90
  color: var(--color-neutral-txt-primary);
91
91
  border-radius: 0.4rem;
92
92
  background-color: var(--color-neutral-background-primary);
93
- z-index: 1010;
93
+ z-index: 1011;
94
94
  overflow: auto;
95
95
 
96
96
  &.horizontal {
@@ -0,0 +1,104 @@
1
+ <template>
2
+ <form class="vts-query-builder" @submit.prevent="handleSubmit()">
3
+ <label :for="id" class="typo-body-regular-small label">
4
+ {{ t('query-builder:label') }}
5
+ </label>
6
+ <div class="input-container">
7
+ <UiInput
8
+ v-model="localFilter"
9
+ :accent="isUsable ? 'brand' : 'danger'"
10
+ :aria-label="uiStore.isSmall ? t('query-builder:label') : undefined"
11
+ :placeholder="t('query-builder:placeholder')"
12
+ clearable
13
+ @clear="filter = localFilter"
14
+ />
15
+ </div>
16
+
17
+ <template v-if="uiStore.isSmallOrMedium">
18
+ <UiButtonIcon icon="fa:magnifying-glass" size="medium" accent="brand" />
19
+ <VtsQueryBuilderButton
20
+ v-model="rootGroup"
21
+ small
22
+ :disabled="!isUsable"
23
+ @confirm="updateFilter()"
24
+ @cancel="resetFilter"
25
+ />
26
+ </template>
27
+ <template v-else>
28
+ <UiButton size="medium" accent="brand" variant="secondary" type="submit">
29
+ {{ t('action:search') }}
30
+ </UiButton>
31
+
32
+ <VtsDivider type="stretch" />
33
+
34
+ <VtsQueryBuilderButton
35
+ v-model="rootGroup"
36
+ :disabled="!isUsable"
37
+ @confirm="updateFilter()"
38
+ @cancel="resetFilter"
39
+ />
40
+ </template>
41
+ </form>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import VtsDivider from '@core/components/divider/VtsDivider.vue'
46
+ import VtsQueryBuilderButton from '@core/components/query-builder/VtsQueryBuilderButton.vue'
47
+ import UiButton from '@core/components/ui/button/UiButton.vue'
48
+ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
49
+ import UiInput from '@core/components/ui/input/UiInput.vue'
50
+ import type { QueryBuilderSchema } from '@core/packages/query-builder/types'
51
+ import { useQueryBuilder } from '@core/packages/query-builder/use-query-builder'
52
+ import { useUiStore } from '@core/stores/ui.store'
53
+ import { onMounted, ref, useId, watch } from 'vue'
54
+ import { useI18n } from 'vue-i18n'
55
+
56
+ const { schema } = defineProps<{
57
+ schema: QueryBuilderSchema
58
+ }>()
59
+
60
+ const filter = defineModel<string>({ required: true })
61
+
62
+ const localFilter = ref('')
63
+
64
+ onMounted(() => {
65
+ localFilter.value = filter.value
66
+ })
67
+
68
+ const { t } = useI18n()
69
+
70
+ const uiStore = useUiStore()
71
+
72
+ const id = useId()
73
+
74
+ const { rootGroup, isUsable, updateFilter, resetFilter } = useQueryBuilder(filter, () => schema)
75
+
76
+ function handleSubmit() {
77
+ filter.value = localFilter.value
78
+ }
79
+
80
+ watch(filter, newFilter => {
81
+ localFilter.value = newFilter
82
+ })
83
+ </script>
84
+
85
+ <style lang="postcss" scoped>
86
+ .vts-query-builder {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 1.6rem;
90
+
91
+ @media (--small-or-medium) {
92
+ gap: 1rem;
93
+
94
+ .label {
95
+ display: none;
96
+ }
97
+ }
98
+
99
+ .input-container {
100
+ flex: 1;
101
+ min-width: 0;
102
+ }
103
+ }
104
+ </style>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <UiButtonIcon
3
+ v-if="small"
4
+ v-tooltip="tooltip"
5
+ icon="fa:filter"
6
+ size="medium"
7
+ accent="brand"
8
+ :disabled
9
+ @click="openModal()"
10
+ />
11
+ <UiButton v-else v-tooltip="tooltip" :disabled accent="brand" variant="tertiary" size="medium" @click="openModal()">
12
+ {{ t('action:use-query-builder') }}
13
+ </UiButton>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import UiButton from '@core/components/ui/button/UiButton.vue'
18
+ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
19
+ import { vTooltip } from '@core/directives/tooltip.directive'
20
+ import { useModal } from '@core/packages/modal/use-modal'
21
+ import type { QueryBuilderGroup } from '@core/packages/query-builder/types'
22
+ import { computed } from 'vue'
23
+ import { useI18n } from 'vue-i18n'
24
+
25
+ const { disabled } = defineProps<{
26
+ disabled?: boolean
27
+ small?: boolean
28
+ }>()
29
+
30
+ const emit = defineEmits<{
31
+ confirm: []
32
+ cancel: []
33
+ }>()
34
+
35
+ const rootGroup = defineModel<QueryBuilderGroup>({ required: true })
36
+
37
+ const tooltip = computed(() => (disabled ? 'Current filter is invalid or too complex' : false))
38
+
39
+ const { t } = useI18n()
40
+
41
+ const openModal = useModal(() => ({
42
+ component: import('@core/components/query-builder/VtsQueryBuilderModal.vue'),
43
+ props: {
44
+ modelValue: rootGroup,
45
+ 'onUpdate:modelValue': (newRootGroup: QueryBuilderGroup) => {
46
+ rootGroup.value = newRootGroup
47
+ },
48
+ },
49
+ onConfirm: () => emit('confirm'),
50
+ onCancel: () => emit('cancel'),
51
+ }))
52
+ </script>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <div class="vts-query-builder-filter">
3
+ <VtsSelect :id="propertySelectId" ref="propertySelector" accent="brand" />
4
+ <VtsSelect :id="operatorSelectId" accent="brand" />
5
+ <UiInput v-if="valueType === 'input'" v-model="value" class="value" accent="brand" />
6
+ <VtsSelect v-else-if="valueType === 'select'" :id="valueSelectId" class="value" accent="brand" />
7
+ <div>
8
+ <MenuList :placement="uiStore.isSmall ? 'bottom' : 'bottom-end'">
9
+ <template #trigger="{ open }">
10
+ <UiButton
11
+ v-if="uiStore.isSmall"
12
+ class="actions-button"
13
+ variant="secondary"
14
+ icon="fa:ellipsis"
15
+ size="medium"
16
+ accent="brand"
17
+ @click="open"
18
+ >
19
+ {{ t('filter-actions') }}
20
+ </UiButton>
21
+ <UiButtonIcon v-else icon="fa:ellipsis" size="medium" accent="brand" @click="open" />
22
+ </template>
23
+ <MenuItem icon="fa:clone" @click="emit('duplicate')">{{ t('action:duplicate') }}</MenuItem>
24
+ <MenuItem icon="fa:layer-group" @click="emit('convertToGroup')">{{ t('action:turn-into-group') }}</MenuItem>
25
+ <MenuItem icon="fa:trash" @click="emit('remove')">{{ t('action:delete-filter') }}</MenuItem>
26
+ </MenuList>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import MenuItem from '@core/components/menu/MenuItem.vue'
33
+ import MenuList from '@core/components/menu/MenuList.vue'
34
+ import VtsSelect from '@core/components/select/VtsSelect.vue'
35
+ import UiButton from '@core/components/ui/button/UiButton.vue'
36
+ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
37
+ import UiInput from '@core/components/ui/input/UiInput.vue'
38
+ import { useFormSelect } from '@core/packages/form-select'
39
+ import type { PropertySchema, OperatorSchema, ValueSchema } from '@core/packages/query-builder/types'
40
+ import { useUiStore } from '@core/stores/ui.store.ts'
41
+ import { onMounted, useTemplateRef } from 'vue'
42
+ import { useI18n } from 'vue-i18n'
43
+
44
+ const { properties, operators, values, valueType } = defineProps<{
45
+ properties: PropertySchema[]
46
+ operators: OperatorSchema[]
47
+ values: ValueSchema[]
48
+ valueType: 'input' | 'select' | 'none'
49
+ }>()
50
+
51
+ const emit = defineEmits<{
52
+ remove: []
53
+ duplicate: []
54
+ convertToGroup: []
55
+ }>()
56
+
57
+ const property = defineModel<string | undefined>('property', { required: true })
58
+
59
+ const operator = defineModel<string | undefined>('operator', { required: true })
60
+
61
+ const value = defineModel<string | undefined>('value', { required: true })
62
+
63
+ const { t } = useI18n()
64
+
65
+ const uiStore = useUiStore()
66
+
67
+ const propertySelector = useTemplateRef('propertySelector')
68
+
69
+ const { id: propertySelectId } = useFormSelect(() => properties, {
70
+ model: property,
71
+ option: {
72
+ id: 'property',
73
+ value: 'property',
74
+ },
75
+ })
76
+
77
+ const { id: operatorSelectId } = useFormSelect(() => operators, {
78
+ model: operator,
79
+ option: {
80
+ id: 'operator',
81
+ value: 'operator',
82
+ },
83
+ })
84
+
85
+ const { id: valueSelectId } = useFormSelect(() => values, {
86
+ model: value,
87
+ option: {
88
+ id: 'value',
89
+ value: 'value',
90
+ },
91
+ })
92
+
93
+ onMounted(() => propertySelector.value?.focus())
94
+ </script>
95
+
96
+ <style lang="postcss" scoped>
97
+ .vts-query-builder-filter {
98
+ display: flex;
99
+ gap: 0.5rem;
100
+ align-items: center;
101
+ background-color: #00000010;
102
+ padding: 0.3rem;
103
+ margin-inline-end: 0.5rem;
104
+ max-width: 100%;
105
+ overflow: auto;
106
+ border-radius: 0.3rem;
107
+
108
+ @media (--small) {
109
+ flex-direction: column;
110
+ align-items: stretch;
111
+ }
112
+
113
+ .label {
114
+ white-space: nowrap;
115
+ margin-inline-end: 1rem;
116
+ }
117
+
118
+ .value {
119
+ flex: 1;
120
+ }
121
+
122
+ .actions-button {
123
+ width: 100%;
124
+ }
125
+ }
126
+ </style>
@@ -0,0 +1,157 @@
1
+ <template>
2
+ <UiCard class="vts-query-builder-group" :class="[{ root, highlighted }, item.operator]">
3
+ <div class="group">
4
+ <div class="header">
5
+ <VtsSelect :id="groupOperatorSelectId" class="group-operator" accent="brand" />
6
+ <MenuList v-if="!root" placement="bottom-end">
7
+ <template #trigger="{ open }">
8
+ <UiButtonIcon size="medium" accent="brand" icon="fa:ellipsis" @click="open" />
9
+ </template>
10
+ <MenuItem @click="emit('duplicate')">{{ t('action:duplicate') }}</MenuItem>
11
+ <MenuItem @click="emit('remove', true)">{{ t('action:move-filters-to-parent-group') }}</MenuItem>
12
+ <MenuItem @click="emit('remove')">{{ t('action:delete-group') }}</MenuItem>
13
+ </MenuList>
14
+ </div>
15
+ <div class="content">
16
+ <VtsQueryBuilderRow
17
+ v-for="(child, index) of item.children"
18
+ :key="child.id"
19
+ v-model:node="item.children[index]"
20
+ v-model:operator="item.operator"
21
+ :index
22
+ @convert-to-group="item.convertChildToGroup(index)"
23
+ @remove="keepChildren => item.removeChild(index, keepChildren)"
24
+ @duplicate="item.duplicateChild(index)"
25
+ />
26
+ <div class="add-child-buttons">
27
+ <VtsQueryBuilderTreeLine last />
28
+ <div class="buttons">
29
+ <UiButton variant="secondary" size="small" accent="brand" icon="fa:ellipsis" @click="item.addChildFilter()">
30
+ {{ t('action:add-filter') }}
31
+ </UiButton>
32
+ <UiButton variant="secondary" size="small" accent="brand" icon="fa:ellipsis" @click="item.addChildGroup()">
33
+ {{ t('action:add-group') }}
34
+ </UiButton>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ <div v-if="root && item.children.length > 1" class="group-filters-button">
40
+ <UiButton variant="secondary" size="small" accent="brand" icon="fa:ellipsis" @click="item.wrapInGroup()">
41
+ {{ item.operator === 'and' ? t('action:plus-or') : t('action:plus-and') }}
42
+ </UiButton>
43
+ </div>
44
+ </UiCard>
45
+ </template>
46
+
47
+ <script lang="ts" setup>
48
+ import MenuItem from '@core/components/menu/MenuItem.vue'
49
+ import MenuList from '@core/components/menu/MenuList.vue'
50
+ import VtsQueryBuilderRow from '@core/components/query-builder/VtsQueryBuilderRow.vue'
51
+ import VtsQueryBuilderTreeLine from '@core/components/query-builder/VtsQueryBuilderTreeLine.vue'
52
+ import VtsSelect from '@core/components/select/VtsSelect.vue'
53
+ import UiButton from '@core/components/ui/button/UiButton.vue'
54
+ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
55
+ import UiCard from '@core/components/ui/card/UiCard.vue'
56
+ import { useFormSelect } from '@core/packages/form-select'
57
+ import type { QueryBuilderGroup } from '@core/packages/query-builder/types'
58
+ import { computed } from 'vue'
59
+ import { useI18n } from 'vue-i18n'
60
+
61
+ defineProps<{
62
+ root?: boolean
63
+ highlighted?: boolean
64
+ }>()
65
+
66
+ const emit = defineEmits<{
67
+ duplicate: []
68
+ remove: [keepChildren?: boolean]
69
+ }>()
70
+
71
+ const item = defineModel<QueryBuilderGroup>({ required: true })
72
+
73
+ const { t } = useI18n()
74
+
75
+ const { id: groupOperatorSelectId } = useFormSelect(['and', 'or'], {
76
+ model: computed({
77
+ get: () => item.value.operator,
78
+ set: value => {
79
+ item.value.operator = value
80
+ },
81
+ }),
82
+ option: {
83
+ label: (option: string) => (option === 'and' ? t('and') : t('or')),
84
+ },
85
+ })
86
+ </script>
87
+
88
+ <style lang="postcss" scoped>
89
+ .vts-query-builder-group {
90
+ padding: 0;
91
+ border: none;
92
+
93
+ &.root {
94
+ margin-block-end: 1.4rem;
95
+ }
96
+
97
+ &.and {
98
+ --main-color: var(--color-brand-item-active);
99
+ --secondary-color: var(--color-brand-background-selected);
100
+ }
101
+
102
+ &.or {
103
+ --main-color: var(--color-success-item-active);
104
+ --secondary-color: var(--color-success-background-selected);
105
+ }
106
+
107
+ .group-operator:deep(.ui-input) {
108
+ padding-inline: 0.6rem;
109
+ gap: 0.6rem;
110
+ }
111
+
112
+ .group {
113
+ position: relative;
114
+ border: 0.1rem solid var(--main-color);
115
+ background-color: var(--secondary-color);
116
+
117
+ .header {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: space-between;
121
+ gap: 1rem;
122
+ padding: 0.3rem;
123
+ background-color: var(--main-color);
124
+ position: sticky;
125
+ top: 0;
126
+ z-index: 1;
127
+ }
128
+
129
+ .content {
130
+ display: flex;
131
+ flex-direction: column;
132
+ padding-block-end: 0.5rem;
133
+ padding-inline-start: 0.6rem;
134
+ max-width: 100%;
135
+ }
136
+ }
137
+
138
+ .add-child-buttons {
139
+ display: flex;
140
+
141
+ & > .buttons {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.8rem;
145
+ background-color: #00000010;
146
+ padding: 0.3rem;
147
+ border-radius: 0.3rem;
148
+ }
149
+ }
150
+
151
+ .group-filters-button {
152
+ position: absolute;
153
+ inset-block-end: -1.4rem;
154
+ inset-inline-end: 0.5rem;
155
+ }
156
+ }
157
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <VtsModal class="vts-query-builder-modal" accent="info" dismissible>
3
+ <template v-if="uiStore.isSmall" #title>{{ t('query-builder') }}</template>
4
+ <template #content>
5
+ <VtsQueryBuilderGroup v-model="rootGroup" root />
6
+ </template>
7
+ <template #buttons>
8
+ <VtsModalCancelButton />
9
+ <VtsModalConfirmButton>{{ t('action:save') }}</VtsModalConfirmButton>
10
+ </template>
11
+ </VtsModal>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import VtsModal from '@core/components/modal/VtsModal.vue'
16
+ import VtsModalCancelButton from '@core/components/modal/VtsModalCancelButton.vue'
17
+ import VtsModalConfirmButton from '@core/components/modal/VtsModalConfirmButton.vue'
18
+ import VtsQueryBuilderGroup from '@core/components/query-builder/VtsQueryBuilderGroup.vue'
19
+ import { type QueryBuilderGroup } from '@core/packages/query-builder/types'
20
+ import { useUiStore } from '@core/stores/ui.store.ts'
21
+ import { useI18n } from 'vue-i18n'
22
+
23
+ const rootGroup = defineModel<QueryBuilderGroup>({ required: true })
24
+
25
+ const { t } = useI18n()
26
+
27
+ const uiStore = useUiStore()
28
+ </script>
29
+
30
+ <style lang="postcss" scoped>
31
+ .vts-query-builder-modal {
32
+ @media (--small) {
33
+ &:deep(.modal) {
34
+ position: absolute;
35
+ inset: 0;
36
+ border-radius: 0;
37
+ max-width: 100vw;
38
+ max-height: 100vh;
39
+
40
+ & > .buttons .line {
41
+ flex-direction: row;
42
+ }
43
+ }
44
+ }
45
+
46
+ &:deep(.modal) {
47
+ background-color: var(--color-neutral-background-primary) !important;
48
+ padding: 0;
49
+ gap: 0;
50
+
51
+ & > .main {
52
+ margin: 1.6rem;
53
+ }
54
+
55
+ & > .buttons {
56
+ padding: 1.6rem;
57
+ border-top: 0.1rem solid var(--color-neutral-border);
58
+ align-items: flex-end;
59
+ }
60
+ }
61
+ }
62
+ </style>