@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.
@@ -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
  })