@xen-orchestra/web-core 0.46.0 → 0.48.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/console/VtsLayoutConsole.vue +1 -1
- package/lib/components/console/VtsRemoteConsole.vue +3 -2
- package/lib/components/table/cells/VtsLinkCell.vue +23 -4
- package/lib/components/ui/stepper/Step.vue +28 -0
- package/lib/components/ui/stepper/UiStepper.vue +61 -0
- package/lib/composables/stepper.composable.ts +43 -0
- package/lib/i18n.ts +8 -0
- package/lib/icons/object-icons.ts +22 -0
- package/lib/locales/cs.json +31 -7
- package/lib/locales/de.json +39 -5
- package/lib/locales/en.json +25 -8
- package/lib/locales/es.json +65 -3
- package/lib/locales/fa.json +2 -1
- package/lib/locales/fr.json +24 -7
- package/lib/locales/it.json +2 -1
- package/lib/locales/nb-NO.json +2 -1
- package/lib/locales/nl.json +59 -9
- package/lib/locales/pt-BR.json +322 -12
- package/lib/locales/pt.json +936 -0
- package/lib/locales/ru.json +2 -1
- package/lib/locales/sk.json +484 -0
- package/lib/locales/sv.json +219 -9
- package/lib/locales/uk.json +2 -1
- package/lib/locales/zh-Hans.json +61 -9
- package/lib/packages/query-builder/README.md +692 -0
- package/lib/packages/remote-resource/sse.store.ts +3 -1
- package/lib/tables/column-definitions/link-or-text-column.ts +22 -0
- package/lib/tables/column-sets/snapshot-columns.ts +19 -0
- package/lib/utils/download-bugtools.utils.ts +5 -0
- package/lib/utils/download-file.utils.ts +13 -0
- package/lib/utils/ip-address.utils.ts +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
# Query Builder Package
|
|
2
|
+
|
|
3
|
+
A powerful, type-safe Vue.js composable library for building dynamic filter interfaces. Query Builder enables developers to create flexible, user-friendly query systems with minimal boilerplate code.
|
|
4
|
+
|
|
5
|
+
**Built on Complex Matcher**: Query Builder leverages the [Complex Matcher](https://npmjs.org/package/complex-matcher) package under the hood, which provides advanced search syntax parsing and filtering capabilities.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
### Installation
|
|
10
|
+
|
|
11
|
+
The Query Builder package is part of the core library and can be imported directly:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { useQueryBuilder } from '@core/packages/query-builder/use-query-builder'
|
|
15
|
+
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter'
|
|
16
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Quick Start Example
|
|
20
|
+
|
|
21
|
+
Here's a minimal example to filter a list of products:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ref } from 'vue'
|
|
25
|
+
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter'
|
|
26
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
27
|
+
|
|
28
|
+
// 1. Define your data type
|
|
29
|
+
interface Product {
|
|
30
|
+
id: string
|
|
31
|
+
name: string
|
|
32
|
+
price: number
|
|
33
|
+
category: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Create your data source
|
|
37
|
+
const products = ref<Product[]>([
|
|
38
|
+
{ id: '1', name: 'Laptop', price: 999, category: 'Electronics' },
|
|
39
|
+
{ id: '2', name: 'Desk', price: 299, category: 'Furniture' },
|
|
40
|
+
{ id: '3', name: 'Mouse', price: 29, category: 'Electronics' },
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
// 3. Define the filterable schema
|
|
44
|
+
const schema = useQueryBuilderSchema<Product>({
|
|
45
|
+
'': { label: 'Any property', operators: { contains: { label: 'Contains' } } },
|
|
46
|
+
name: { label: 'Product Name', operators: { contains: { label: 'Contains' } } },
|
|
47
|
+
category: {
|
|
48
|
+
label: 'Category',
|
|
49
|
+
operators: { is: { label: 'Is', values: { Electronics: 'Electronics', Furniture: 'Furniture' } } },
|
|
50
|
+
},
|
|
51
|
+
price: { label: 'Price', operators: { greaterThan: { label: '>' }, lessThan: { label: '<' } } },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 4. Use the filter composable
|
|
55
|
+
const { items: filteredProducts, filter } = useQueryBuilderFilter('products', () => products.value)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Alternative: Using Schema Utilities**
|
|
59
|
+
|
|
60
|
+
The example above manually defines operators for each property. For a cleaner approach, use the schema utilities:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { ref } from 'vue'
|
|
64
|
+
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter'
|
|
65
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
66
|
+
import { useStringSchema } from '@core/utils/query-builder/use-string-schema'
|
|
67
|
+
import { useNumberSchema } from '@core/utils/query-builder/use-number-schema'
|
|
68
|
+
|
|
69
|
+
interface Product {
|
|
70
|
+
id: string
|
|
71
|
+
name: string
|
|
72
|
+
price: number
|
|
73
|
+
category: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const products = ref<Product[]>([
|
|
77
|
+
{ id: '1', name: 'Laptop', price: 999, category: 'Electronics' },
|
|
78
|
+
{ id: '2', name: 'Desk', price: 299, category: 'Furniture' },
|
|
79
|
+
{ id: '3', name: 'Mouse', price: 29, category: 'Electronics' },
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
// Simpler schema definition with utilities
|
|
83
|
+
const schema = useQueryBuilderSchema<Product>({
|
|
84
|
+
'': useStringSchema('Any property'),
|
|
85
|
+
name: useStringSchema('Product Name'),
|
|
86
|
+
category: useStringSchema('Category', { Electronics: 'Electronics', Furniture: 'Furniture' }),
|
|
87
|
+
price: useNumberSchema('Price'),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const { items: filteredProducts, filter } = useQueryBuilderFilter('products', () => products.value)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Glossary
|
|
94
|
+
|
|
95
|
+
### Core Terms
|
|
96
|
+
|
|
97
|
+
**Filter Expression**
|
|
98
|
+
A human-readable query string that describes which items to keep, using Complex Matcher syntax. Example: `name:Laptop` or `price>100 category:Electronics`
|
|
99
|
+
|
|
100
|
+
**Schema**
|
|
101
|
+
The configuration that defines which properties can be filtered, what operators are available for each property, and what values are allowed. It acts as a blueprint for valid queries.
|
|
102
|
+
|
|
103
|
+
**Property**
|
|
104
|
+
A field from your data object that users can filter by. Examples: `name`, `price`, `category`. In TypeScript, nested properties use dot notation (e.g., `address.city`), but in filter queries they're accessed with colon notation (e.g., `address:city`).
|
|
105
|
+
|
|
106
|
+
**Operator**
|
|
107
|
+
A condition type that defines how to compare a property value. Examples: `contains`, `is`, `greaterThan`, `startsWith`, `matchesRegex`.
|
|
108
|
+
|
|
109
|
+
**Value**
|
|
110
|
+
The comparison value provided by the user. Examples: `"Laptop"`, `100`, `"Electronics"`.
|
|
111
|
+
|
|
112
|
+
**Group**
|
|
113
|
+
A logical container for combining multiple conditions with AND or OR operators. Used internally to represent complex query logic.
|
|
114
|
+
|
|
115
|
+
**Filter Node**
|
|
116
|
+
An individual filter condition (a property, operator, and value). Filters are the leaf nodes in the filter tree.
|
|
117
|
+
|
|
118
|
+
**Group Node**
|
|
119
|
+
A node that contains multiple child nodes (filters or other groups) combined with an AND or OR operator.
|
|
120
|
+
|
|
121
|
+
**Raw Filter**
|
|
122
|
+
The string representation of a filter expression. This is what gets serialized and can be passed between components or stored in URLs.
|
|
123
|
+
|
|
124
|
+
**Predicate**
|
|
125
|
+
A JavaScript function that tests whether an item matches the filter criteria. Returns `true` if the item should be included, `false` otherwise.
|
|
126
|
+
|
|
127
|
+
## Core Concepts & Usage
|
|
128
|
+
|
|
129
|
+
### Understanding the Schema
|
|
130
|
+
|
|
131
|
+
The schema is the most important part of Query Builder. It defines what users can filter and how. Here's a detailed example:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
135
|
+
|
|
136
|
+
interface Article {
|
|
137
|
+
id: string
|
|
138
|
+
title: string
|
|
139
|
+
status: 'draft' | 'published' | 'archived'
|
|
140
|
+
views: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const schema = useQueryBuilderSchema<Article>({
|
|
144
|
+
// The empty string key defines the "any property" fallback
|
|
145
|
+
'': {
|
|
146
|
+
label: 'Any property',
|
|
147
|
+
operators: {
|
|
148
|
+
contains: { label: 'Contains' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Text properties with multiple operators
|
|
153
|
+
title: {
|
|
154
|
+
label: 'Article Title',
|
|
155
|
+
operators: {
|
|
156
|
+
contains: { label: 'Contains' },
|
|
157
|
+
startsWith: { label: 'Starts with' },
|
|
158
|
+
endsWith: { label: 'Ends with' },
|
|
159
|
+
matchesRegex: { label: 'Matches regex' },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Enum-like property with predefined values
|
|
164
|
+
status: {
|
|
165
|
+
label: 'Status',
|
|
166
|
+
operators: {
|
|
167
|
+
is: {
|
|
168
|
+
label: 'Is',
|
|
169
|
+
values: {
|
|
170
|
+
draft: 'Draft',
|
|
171
|
+
published: 'Published',
|
|
172
|
+
archived: 'Archived',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
isNot: {
|
|
176
|
+
label: 'Is not',
|
|
177
|
+
values: {
|
|
178
|
+
draft: 'Draft',
|
|
179
|
+
published: 'Published',
|
|
180
|
+
archived: 'Archived',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Numeric property with comparison operators
|
|
187
|
+
views: {
|
|
188
|
+
label: 'Number of Views',
|
|
189
|
+
operators: {
|
|
190
|
+
greaterThan: { label: '>' },
|
|
191
|
+
greaterThanOrEqual: { label: '>=' },
|
|
192
|
+
lessThan: { label: '<' },
|
|
193
|
+
lessThanOrEqual: { label: '<=' },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### useQueryBuilderFilter: Filtering Data
|
|
200
|
+
|
|
201
|
+
The `useQueryBuilderFilter` composable handles filtering and URL persistence:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter'
|
|
205
|
+
|
|
206
|
+
const items = ref<User[]>([...])
|
|
207
|
+
const schema = useQueryBuilderSchema<User>({...})
|
|
208
|
+
|
|
209
|
+
// The composable automatically:
|
|
210
|
+
// - Persists the filter in the URL (e.g., ?qb.users=name:John)
|
|
211
|
+
// - Filters your items in real-time
|
|
212
|
+
// - Handles invalid filter expressions gracefully
|
|
213
|
+
const { items: filteredItems, filter } = useQueryBuilderFilter(
|
|
214
|
+
'users', // Unique ID (used in URL)
|
|
215
|
+
() => items.value, // Your data source
|
|
216
|
+
{
|
|
217
|
+
initialFilter: 'name:John' // Optional: pre-set filter
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Use filteredItems in your template or computed properties
|
|
222
|
+
console.log(filteredItems.value) // Reactive array of filtered items
|
|
223
|
+
|
|
224
|
+
// Access the filter string
|
|
225
|
+
console.log(filter.value) // Current filter expression
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### useQueryBuilder: Advanced Query Building
|
|
229
|
+
|
|
230
|
+
For building interactive query builder UIs, use `useQueryBuilder`:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { useQueryBuilder } from '@core/packages/query-builder/use-query-builder'
|
|
234
|
+
|
|
235
|
+
const filterString = ref('status:published')
|
|
236
|
+
const schema = useQueryBuilderSchema<Article>({...})
|
|
237
|
+
|
|
238
|
+
const { rootGroup, isUsable, updateFilter, resetFilter } = useQueryBuilder(filterString, schema)
|
|
239
|
+
|
|
240
|
+
// rootGroup is a reactive tree structure you can traverse and modify
|
|
241
|
+
// It represents the parsed query
|
|
242
|
+
|
|
243
|
+
// If the filter string is invalid, isUsable will be false
|
|
244
|
+
if (!isUsable.value) {
|
|
245
|
+
console.log('Current filter is not valid')
|
|
246
|
+
resetFilter() // Reset to an empty query
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// After modifying the tree, sync back to the filter string
|
|
250
|
+
updateFilter() // Updates filterString.value from rootGroup
|
|
251
|
+
|
|
252
|
+
// You can also directly modify the tree structure:
|
|
253
|
+
rootGroup.value.addChildFilter() // Add a new filter condition
|
|
254
|
+
rootGroup.value.operator.value = 'or' // Change AND to OR
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Building Schemas with Utilities
|
|
258
|
+
|
|
259
|
+
Instead of manually defining operators for every property, you can use the provided schema builder utilities. These helpers eliminate repetition and ensure consistent operator sets:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
263
|
+
import { useStringSchema } from '@core/utils/query-builder/use-string-schema'
|
|
264
|
+
import { useNumberSchema } from '@core/utils/query-builder/use-number-schema'
|
|
265
|
+
import { useBooleanSchema } from '@core/utils/query-builder/use-boolean-schema'
|
|
266
|
+
|
|
267
|
+
interface Product {
|
|
268
|
+
name: string
|
|
269
|
+
description: string
|
|
270
|
+
category: 'electronics' | 'furniture' | 'clothing'
|
|
271
|
+
price: number
|
|
272
|
+
inStock: boolean
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const schema = useQueryBuilderSchema<Product>({
|
|
276
|
+
// Catch-all search
|
|
277
|
+
'': useStringSchema('Any property'),
|
|
278
|
+
|
|
279
|
+
// String properties - full operators (contains, startsWith, regex, etc.)
|
|
280
|
+
name: useStringSchema('Product Name'),
|
|
281
|
+
description: useStringSchema('Description'),
|
|
282
|
+
|
|
283
|
+
// String with predefined values - only exact match operators
|
|
284
|
+
category: useStringSchema('Category', {
|
|
285
|
+
electronics: 'Electronics',
|
|
286
|
+
furniture: 'Furniture',
|
|
287
|
+
clothing: 'Clothing',
|
|
288
|
+
}),
|
|
289
|
+
|
|
290
|
+
// Numeric properties - comparison operators
|
|
291
|
+
price: useNumberSchema('Price', {
|
|
292
|
+
'100': '$100',
|
|
293
|
+
'500': '$500',
|
|
294
|
+
'1000': '$1000',
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
// Boolean properties
|
|
298
|
+
inStock: useBooleanSchema('In Stock'),
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The utilities create appropriate operator sets:
|
|
303
|
+
|
|
304
|
+
- **`useStringSchema(label, values?)`**: Creates string operators (contains, startsWith, endsWith, regex, glob, etc.). With predefined `values`, it limits to exact match operators (is, isNot).
|
|
305
|
+
- **`useNumberSchema(label, values?)`**: Creates numeric comparison operators (>, <, >=, <=, =, !=).
|
|
306
|
+
- **`useBooleanSchema(label, values)`**: Creates boolean operators (isEmpty, isNotEmpty).
|
|
307
|
+
|
|
308
|
+
### Supported Operators
|
|
309
|
+
|
|
310
|
+
Query Builder supports these operators out of the box:
|
|
311
|
+
|
|
312
|
+
| Operator | Description | Expects Value | Example |
|
|
313
|
+
| -------------------- | --------------------------------- | ------------- | ----------------------- |
|
|
314
|
+
| `contains` | Text contains substring | Yes | `name:john` or `Laptop` |
|
|
315
|
+
| `doesNotContain` | Text doesn't contain substring | Yes | `!name:admin` |
|
|
316
|
+
| `is` | Exact match (case-sensitive) | Yes | `status:active` |
|
|
317
|
+
| `isNot` | Exact match doesn't apply | Yes | `!status:inactive` |
|
|
318
|
+
| `startsWith` | Text starts with substring | Yes | `email:/^test/i` |
|
|
319
|
+
| `doesNotStartWith` | Text doesn't start with substring | Yes | `!email:/^admin/i` |
|
|
320
|
+
| `endsWith` | Text ends with substring | Yes | `domain:/.com$/` |
|
|
321
|
+
| `doesNotEndWith` | Text doesn't end with substring | Yes | `!(domain:/.local$/)` |
|
|
322
|
+
| `matchesRegex` | Matches regex pattern | Yes | `code:/^[A-Z]{3}/` |
|
|
323
|
+
| `doesNotMatchRegex` | Doesn't match regex pattern | Yes | `!(code:/^TEMP/)` |
|
|
324
|
+
| `matchesGlob` | Matches glob pattern | Yes | `filename:*.txt` |
|
|
325
|
+
| `doesNotMatchGlob` | Doesn't match glob pattern | Yes | `!(filename:*.log)` |
|
|
326
|
+
| `greaterThan` | Numeric comparison > | Yes | `price:>100` |
|
|
327
|
+
| `greaterThanOrEqual` | Numeric comparison >= | Yes | `price:>=100` |
|
|
328
|
+
| `lessThan` | Numeric comparison < | Yes | `price:<100` |
|
|
329
|
+
| `lessThanOrEqual` | Numeric comparison <= | Yes | `price:<=100` |
|
|
330
|
+
| `isEmpty` | Value is empty/null | No | `description?` |
|
|
331
|
+
| `isNotEmpty` | Value is not empty/null | No | `!description?` |
|
|
332
|
+
|
|
333
|
+
## Vue Components
|
|
334
|
+
|
|
335
|
+
Query Builder includes a set of Vue components to help you build interactive filter interfaces without writing complex UI logic from scratch.
|
|
336
|
+
|
|
337
|
+
### Main Component: VtsQueryBuilder
|
|
338
|
+
|
|
339
|
+
The `VtsQueryBuilder` component is the primary component you'll use. It provides:
|
|
340
|
+
|
|
341
|
+
- A text input field for entering filter expressions directly
|
|
342
|
+
- A visual query builder button that opens an interactive modal
|
|
343
|
+
- Real-time validation and visual feedback
|
|
344
|
+
- Responsive design (adapts to mobile and desktop)
|
|
345
|
+
|
|
346
|
+
**Usage:**
|
|
347
|
+
|
|
348
|
+
```vue
|
|
349
|
+
<template>
|
|
350
|
+
<VtsQueryBuilder v-model="filter" :schema />
|
|
351
|
+
</template>
|
|
352
|
+
|
|
353
|
+
<script setup lang="ts">
|
|
354
|
+
import { ref } from 'vue'
|
|
355
|
+
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
|
|
356
|
+
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema'
|
|
357
|
+
import { useStringSchema } from '@core/utils/query-builder/use-string-schema'
|
|
358
|
+
|
|
359
|
+
const filter = ref('')
|
|
360
|
+
|
|
361
|
+
const schema = useQueryBuilderSchema({
|
|
362
|
+
'': useStringSchema('Any property'),
|
|
363
|
+
name: useStringSchema('Name'),
|
|
364
|
+
status: useStringSchema('Status', { active: 'Active', inactive: 'Inactive' }),
|
|
365
|
+
})
|
|
366
|
+
</script>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The component handles the complete filtering workflow:
|
|
370
|
+
|
|
371
|
+
1. Users type filter expressions directly in the input field
|
|
372
|
+
2. Or click the builder button to construct queries visually with dropdowns and inputs
|
|
373
|
+
3. The `v-model` binding keeps the filter string synchronized
|
|
374
|
+
|
|
375
|
+
### Building Blocks
|
|
376
|
+
|
|
377
|
+
The `VtsQueryBuilder` component is built from smaller, composable components:
|
|
378
|
+
|
|
379
|
+
- `VtsQueryBuilderButton`: Opens the modal interface
|
|
380
|
+
- `VtsQueryBuilderModal`: The interactive query builder UI
|
|
381
|
+
- `VtsQueryBuilderFilter`: Renders individual filter conditions
|
|
382
|
+
- `VtsQueryBuilderGroup`: Container for grouped conditions with AND/OR logic
|
|
383
|
+
- `VtsQueryBuilderRow`: Wrapper for filter rows with controls
|
|
384
|
+
|
|
385
|
+
You can use these components directly if you need a fully custom UI, but `VtsQueryBuilder` covers most use cases.
|
|
386
|
+
|
|
387
|
+
## Best Practices
|
|
388
|
+
|
|
389
|
+
### 1. Define Schemas Clearly
|
|
390
|
+
|
|
391
|
+
Always define schemas that match your data structure exactly. Use descriptive labels:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Good: Clear labels that users understand
|
|
395
|
+
const schema = useQueryBuilderSchema<User>({
|
|
396
|
+
email: {
|
|
397
|
+
label: 'Email Address', // User-friendly
|
|
398
|
+
operators: { contains: { label: 'Contains' } },
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// Avoid: Unclear or technical labels
|
|
403
|
+
const schema = useQueryBuilderSchema<User>({
|
|
404
|
+
email: {
|
|
405
|
+
label: 'email', // Not user-friendly
|
|
406
|
+
operators: { contains: { label: 'cont' } }, // Abbreviations confuse users
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 2. Provide Enum Values When Appropriate
|
|
412
|
+
|
|
413
|
+
For properties with limited options, always provide predefined values. This prevents errors and improves UX:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Good: Users select from a dropdown
|
|
417
|
+
const schema = useQueryBuilderSchema<Order>({
|
|
418
|
+
status: {
|
|
419
|
+
label: 'Order Status',
|
|
420
|
+
operators: {
|
|
421
|
+
is: {
|
|
422
|
+
label: 'Is',
|
|
423
|
+
values: {
|
|
424
|
+
pending: 'Pending',
|
|
425
|
+
shipped: 'Shipped',
|
|
426
|
+
delivered: 'Delivered',
|
|
427
|
+
cancelled: 'Cancelled',
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// Avoid: Users must type exact values (error-prone)
|
|
435
|
+
const schema = useQueryBuilderSchema<Order>({
|
|
436
|
+
status: {
|
|
437
|
+
label: 'Order Status',
|
|
438
|
+
operators: {
|
|
439
|
+
is: { label: 'Is' }, // No predefined values = bad UX
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 3. Include a Catch-All Property
|
|
446
|
+
|
|
447
|
+
Always include an empty string key (`''`) in your schema as a fallback for "search any property":
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const schema = useQueryBuilderSchema<User>({
|
|
451
|
+
'': {
|
|
452
|
+
label: 'Any property',
|
|
453
|
+
operators: {
|
|
454
|
+
contains: { label: 'Contains' },
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
// ... other properties
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 4. Keep Operators Minimal
|
|
462
|
+
|
|
463
|
+
Avoid offering operators that users won't need. Simpler schemas are easier to use:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// Good: Only relevant operators
|
|
467
|
+
const schema = useQueryBuilderSchema<User>({
|
|
468
|
+
age: {
|
|
469
|
+
label: 'Age',
|
|
470
|
+
operators: {
|
|
471
|
+
greaterThan: { label: '>' },
|
|
472
|
+
lessThan: { label: '<' },
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Avoid: Offering all operators for every property
|
|
478
|
+
const schema = useQueryBuilderSchema<User>({
|
|
479
|
+
age: {
|
|
480
|
+
label: 'Age',
|
|
481
|
+
operators: {
|
|
482
|
+
contains: { label: 'Contains' }, // Doesn't make sense for numbers
|
|
483
|
+
matchesRegex: { label: 'Regex' }, // Confusing for numbers
|
|
484
|
+
greaterThan: { label: '>' },
|
|
485
|
+
// ... 15 more operators
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 5. Handle Invalid Filters Gracefully
|
|
492
|
+
|
|
493
|
+
Always check `isUsable` before relying on the parsed query:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const { rootGroup, isUsable, resetFilter } = useQueryBuilder(filterString, schema)
|
|
497
|
+
|
|
498
|
+
// In your UI:
|
|
499
|
+
if (!isUsable.value) {
|
|
500
|
+
// Show error or reset
|
|
501
|
+
console.warn('Filter syntax is invalid')
|
|
502
|
+
resetFilter()
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Advanced Patterns
|
|
507
|
+
|
|
508
|
+
### Working with Nested Properties
|
|
509
|
+
|
|
510
|
+
Query Builder supports filtering on nested object properties using colon notation:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
interface Company {
|
|
514
|
+
id: string
|
|
515
|
+
name: string
|
|
516
|
+
address: {
|
|
517
|
+
city: string
|
|
518
|
+
country: string
|
|
519
|
+
}
|
|
520
|
+
employees: Array<{
|
|
521
|
+
name: string
|
|
522
|
+
department: string
|
|
523
|
+
}>
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const schema = useQueryBuilderSchema<Company>({
|
|
527
|
+
'': {
|
|
528
|
+
label: 'Any property',
|
|
529
|
+
operators: { contains: { label: 'Contains' } },
|
|
530
|
+
},
|
|
531
|
+
name: {
|
|
532
|
+
label: 'Company Name',
|
|
533
|
+
operators: { contains: { label: 'Contains' } },
|
|
534
|
+
},
|
|
535
|
+
// Access nested properties using colon notation
|
|
536
|
+
'address:city': {
|
|
537
|
+
label: 'City',
|
|
538
|
+
operators: { is: { label: 'Is' } },
|
|
539
|
+
},
|
|
540
|
+
'address:country': {
|
|
541
|
+
label: 'Country',
|
|
542
|
+
operators: { is: { label: 'Is' } },
|
|
543
|
+
},
|
|
544
|
+
'employees:name': {
|
|
545
|
+
label: 'Employee Name',
|
|
546
|
+
operators: { contains: { label: 'Contains' } },
|
|
547
|
+
},
|
|
548
|
+
'employees:department': {
|
|
549
|
+
label: 'Department',
|
|
550
|
+
operators: { contains: { label: 'Contains' } },
|
|
551
|
+
},
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
// Users can now write queries like:
|
|
555
|
+
// 'address:city:"New York" employees:department:Engineering'
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Complex Logical Queries
|
|
559
|
+
|
|
560
|
+
Query Builder automatically handles complex AND/OR combinations:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
const filterString = ref('')
|
|
564
|
+
|
|
565
|
+
const { rootGroup, updateFilter } = useQueryBuilder(filterString, schema)
|
|
566
|
+
|
|
567
|
+
// Build programmatically
|
|
568
|
+
rootGroup.value.addChildFilter() // Add first filter
|
|
569
|
+
rootGroup.value.children[0].property.value = 'status'
|
|
570
|
+
rootGroup.value.children[0].operator.value = 'is'
|
|
571
|
+
rootGroup.value.children[0].value.value = 'active'
|
|
572
|
+
|
|
573
|
+
rootGroup.value.addChildGroup() // Add a group
|
|
574
|
+
rootGroup.value.children[1].operator.value = 'or'
|
|
575
|
+
rootGroup.value.children[1].addChildFilter()
|
|
576
|
+
rootGroup.value.children[1].children[0].property.value = 'archived'
|
|
577
|
+
rootGroup.value.children[1].children[0].operator.value = 'isNotEmpty'
|
|
578
|
+
|
|
579
|
+
updateFilter() // Sync to filter string
|
|
580
|
+
|
|
581
|
+
// This example builds: status:active (archived?)
|
|
582
|
+
// Which expands to: "status is active" AND ("archived is not empty")
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Duplicating and Managing Filter Conditions
|
|
586
|
+
|
|
587
|
+
Interactive query builders often need to duplicate or remove filters:
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
const { rootGroup } = useQueryBuilder(filterString, schema)
|
|
591
|
+
|
|
592
|
+
// Duplicate a child (useful for "add another condition like this")
|
|
593
|
+
rootGroup.value.duplicateChild(0)
|
|
594
|
+
|
|
595
|
+
// Remove a child
|
|
596
|
+
rootGroup.value.removeChild(1)
|
|
597
|
+
|
|
598
|
+
// Remove a child but keep its children (flatten)
|
|
599
|
+
rootGroup.value.removeChild(2, true)
|
|
600
|
+
|
|
601
|
+
// Convert a filter to a group (add sub-conditions)
|
|
602
|
+
rootGroup.value.convertChildToGroup(0)
|
|
603
|
+
|
|
604
|
+
// Wrap all current conditions in a group (useful for OR queries)
|
|
605
|
+
rootGroup.value.wrapInGroup()
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Combining Multiple Filtered Lists
|
|
609
|
+
|
|
610
|
+
You can use multiple filter instances for different data sources in the same component:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const users = ref<User[]>([...])
|
|
614
|
+
const posts = ref<Post[]>([...])
|
|
615
|
+
|
|
616
|
+
const userSchema = useQueryBuilderSchema<User>({...})
|
|
617
|
+
const postSchema = useQueryBuilderSchema<Post>({...})
|
|
618
|
+
|
|
619
|
+
const { items: filteredUsers, filter: userFilter } = useQueryBuilderFilter(
|
|
620
|
+
'users',
|
|
621
|
+
() => users.value
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const { items: filteredPosts, filter: postFilter } = useQueryBuilderFilter(
|
|
625
|
+
'posts',
|
|
626
|
+
() => posts.value
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
// Each filter is independent and synced to different URL params
|
|
630
|
+
// URL: ?qb.users=...&qb.posts=...
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Building a Custom Query Builder UI
|
|
634
|
+
|
|
635
|
+
If you need full control over the UI, work directly with the root group structure:
|
|
636
|
+
|
|
637
|
+
```vue
|
|
638
|
+
// In your component:
|
|
639
|
+
<template>
|
|
640
|
+
<div v-if="!isUsable" class="error">Invalid filter</div>
|
|
641
|
+
|
|
642
|
+
<div v-else class="query-builder">
|
|
643
|
+
<div class="group-controls">
|
|
644
|
+
<button @click="rootGroup.addChildFilter()">+ Condition</button>
|
|
645
|
+
<button @click="rootGroup.addChildGroup()">+ Group</button>
|
|
646
|
+
<select v-model="rootGroup.operator">
|
|
647
|
+
<option value="and">AND</option>
|
|
648
|
+
<option value="or">OR</option>
|
|
649
|
+
</select>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div class="children-list">
|
|
653
|
+
<div v-for="(child, index) in rootGroup.children" :key="child.id">
|
|
654
|
+
<FilterRow v-if="!child.isGroup" :filter="child" @remove="rootGroup.removeChild(index)" />
|
|
655
|
+
<GroupRow v-else :group="child" @remove="rootGroup.removeChild(index)" />
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</template>
|
|
660
|
+
|
|
661
|
+
<script setup lang="ts">
|
|
662
|
+
const filterString = ref('')
|
|
663
|
+
const { rootGroup, isUsable, updateFilter } = useQueryBuilder(filterString, schema)
|
|
664
|
+
|
|
665
|
+
// Update the filter string when the tree changes
|
|
666
|
+
watch(
|
|
667
|
+
() => rootGroup.value.rawFilter,
|
|
668
|
+
() => updateFilter(),
|
|
669
|
+
{ deep: true }
|
|
670
|
+
)
|
|
671
|
+
</script>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Text Search Every Property
|
|
675
|
+
|
|
676
|
+
For a simple "search everywhere" feature:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
const searchQuery = ref('')
|
|
680
|
+
|
|
681
|
+
const { items: searchResults } = useQueryBuilderFilter('search', () => items.value, {
|
|
682
|
+
initialFilter: computed(() => searchQuery.value),
|
|
683
|
+
})
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
This leverages the catch-all `''` property defined in the schema to search all properties at once.
|
|
687
|
+
|
|
688
|
+
### Filtering Non-Existent Properties
|
|
689
|
+
|
|
690
|
+
When users write filter expressions that reference properties not defined in the schema, Query Builder gracefully handles this by falling back to the "any property" (`''`) filter. This allows for flexible search experiences where exact property names don't need to be known in advance.
|
|
691
|
+
|
|
692
|
+
For example, if a schema doesn't define a `custom_field` property but a user types `custom_field:value`, the query parser will fall back to the catch-all property. The result will be no matches (since the property doesn't exist on your objects), which is the expected behavior.
|
|
@@ -54,6 +54,8 @@ export const useSseStore = defineStore('sse', () => {
|
|
|
54
54
|
|
|
55
55
|
const hasErrorSse = computed(() => isError.value || sse.value.errorSse !== null)
|
|
56
56
|
|
|
57
|
+
const isConnected = computed(() => sse.value.id !== undefined)
|
|
58
|
+
|
|
57
59
|
function setErrorSse(error: unknown | null) {
|
|
58
60
|
sse.value.errorSse = error
|
|
59
61
|
}
|
|
@@ -164,5 +166,5 @@ export const useSseStore = defineStore('sse', () => {
|
|
|
164
166
|
window.location.reload()
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
return { watch, unwatch, retry, hasErrorSse, setErrorSse, setPing }
|
|
169
|
+
return { watch, unwatch, retry, hasErrorSse, setErrorSse, setPing, isConnected }
|
|
168
170
|
})
|