@ticatec/uniface-flexi-module 0.0.2
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/FLEXICRITERIASET_GUIDE.md +1559 -0
- package/FLEXICRITERIASET_GUIDE_CN.md +1133 -0
- package/FLEXIDATATABLE_GUIDE.md +1650 -0
- package/FLEXIDATATABLE_GUIDE_CN.md +1650 -0
- package/FLEXIFORM_GUIDE.md +1068 -0
- package/FLEXIFORM_GUIDE_CN.md +1068 -0
- package/FLEXI_CONTEXT_GUIDE_CN.md +172 -0
- package/MODULE_LOADER_CN.md +228 -0
- package/README.md +307 -0
- package/README_CN.md +51 -0
- package/SANDBOX_CN.md +201 -0
- package/dist/FlexiContext.d.ts +28 -0
- package/dist/FlexiContext.js +45 -0
- package/dist/ModuleLoader.d.ts +41 -0
- package/dist/ModuleLoader.js +55 -0
- package/dist/Sandbox.d.ts +33 -0
- package/dist/Sandbox.js +101 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte +26 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte.d.ts +22 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte +10 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateRangeField.svelte +11 -0
- package/dist/criteria-panel/components/DateRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte +11 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte +9 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/SearchField.svelte +14 -0
- package/dist/criteria-panel/components/SearchField.svelte.d.ts +33 -0
- package/dist/criteria-panel/components/TextSearchField.svelte +9 -0
- package/dist/criteria-panel/components/TextSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte +9 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte.d.ts +24 -0
- package/dist/criteria-panel/index.d.ts +6 -0
- package/dist/criteria-panel/index.js +6 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.d.ts +19 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.js +31 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.d.ts +1 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.js +127 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.d.ts +38 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.js +31 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.d.ts +24 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.js +48 -0
- package/dist/flexi-datatable/FlexiDataTable.d.ts +111 -0
- package/dist/flexi-datatable/FlexiDataTable.js +90 -0
- package/dist/flexi-datatable/index.d.ts +2 -0
- package/dist/flexi-datatable/index.js +2 -0
- package/dist/flexi-form/FlexiCompound.d.ts +34 -0
- package/dist/flexi-form/FlexiCompound.js +84 -0
- package/dist/flexi-form/FlexiFormDialog.svelte +24 -0
- package/dist/flexi-form/FlexiFormDialog.svelte.d.ts +21 -0
- package/dist/flexi-form/FlexiFormPage.svelte +26 -0
- package/dist/flexi-form/FlexiFormPage.svelte.d.ts +25 -0
- package/dist/flexi-form/Schema.d.ts +6 -0
- package/dist/flexi-form/Schema.js +1 -0
- package/dist/flexi-form/components/BreakLine.svelte +1 -0
- package/dist/flexi-form/components/BreakLine.svelte.d.ts +26 -0
- package/dist/flexi-form/components/CardTitleBar.svelte +18 -0
- package/dist/flexi-form/components/CardTitleBar.svelte.d.ts +22 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/CellFieldBuilder.d.ts +1 -0
- package/dist/flexi-form/components/CellFieldBuilder.js +178 -0
- package/dist/flexi-form/components/DateField.svelte +12 -0
- package/dist/flexi-form/components/DateField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/DateTimeField.svelte +13 -0
- package/dist/flexi-form/components/DateTimeField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/MemoField.svelte +12 -0
- package/dist/flexi-form/components/MemoField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/NumberField.svelte +12 -0
- package/dist/flexi-form/components/NumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/TextField.svelte +12 -0
- package/dist/flexi-form/components/TextField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnitNumberField.svelte +12 -0
- package/dist/flexi-form/components/UnitNumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte +5 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte.d.ts +18 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte +13 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte.d.ts +33 -0
- package/dist/flexi-form/flexi_card/FlexiCard.d.ts +64 -0
- package/dist/flexi-form/flexi_card/FlexiCard.js +66 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte +57 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte.d.ts +22 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.d.ts +50 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.js +26 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte +42 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_composite/README.md +50 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.d.ts +4 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.js +2 -0
- package/dist/flexi-form/flexi_field/FlexiField.d.ts +76 -0
- package/dist/flexi-form/flexi_field/FlexiField.js +128 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte +35 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_field/UnknownField.d.ts +3 -0
- package/dist/flexi-form/flexi_field/UnknownField.js +3 -0
- package/dist/flexi-form/flexi_form/FlexiForm.d.ts +127 -0
- package/dist/flexi-form/flexi_form/FlexiForm.js +160 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte +57 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte.d.ts +25 -0
- package/dist/flexi-form/index.d.ts +11 -0
- package/dist/flexi-form/index.js +11 -0
- package/dist/flexi-form/lib/ComponentBuilder.d.ts +15 -0
- package/dist/flexi-form/lib/ComponentBuilder.js +31 -0
- package/dist/flexi-form/lib/index.d.ts +5 -0
- package/dist/flexi-form/lib/index.js +2 -0
- package/dist/flexi-form/lib/types.d.ts +7 -0
- package/dist/flexi-form/lib/types.js +6 -0
- package/dist/flexi-form/lib/utils.d.ts +10 -0
- package/dist/flexi-form/lib/utils.js +48 -0
- package/dist/i18n-res/i18nRes.d.ts +2 -0
- package/dist/i18n-res/i18nRes.js +8 -0
- package/dist/i18n-res/index.d.ts +2 -0
- package/dist/i18n-res/index.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/uniface-flexi-module.css +46 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +8 -0
- package/package.json +135 -0
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
# FlexiCriteriaSet Complete Usage Guide
|
|
2
|
+
|
|
3
|
+
A comprehensive guide to using FlexiCriteriaSet for building dynamic search and filtering panels in Svelte applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Basic Usage](#basic-usage)
|
|
9
|
+
3. [Schema Structure](#schema-structure)
|
|
10
|
+
4. [Criteria Field Types](#criteria-field-types)
|
|
11
|
+
5. [Advanced Search Patterns](#advanced-search-patterns)
|
|
12
|
+
6. [Dynamic Criteria Generation](#dynamic-criteria-generation)
|
|
13
|
+
7. [Events and Interactions](#events-and-interactions)
|
|
14
|
+
8. [Extending FlexiCriteriaSet](#extending-flexicriteriaset)
|
|
15
|
+
9. [Integration Patterns](#integration-patterns)
|
|
16
|
+
10. [Best Practices](#best-practices)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
FlexiCriteriaSet is a powerful search criteria builder that allows users to construct complex search queries through an intuitive interface. It supports various field types, operators, and provides extensible architecture for custom search components.
|
|
21
|
+
|
|
22
|
+
### Key Features
|
|
23
|
+
|
|
24
|
+
- **Flexible Search Fields**: Support for text, number, date, select, and range searches
|
|
25
|
+
- **Dynamic Operators**: Context-aware operators for different field types
|
|
26
|
+
- **Schema-Based Configuration**: JSON-driven criteria panel setup
|
|
27
|
+
- **Extensible Architecture**: Custom field types and search logic
|
|
28
|
+
- **Type Safety**: Full TypeScript support
|
|
29
|
+
- **Real-time Updates**: Immediate search criteria generation
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
### 1. Installation and Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import CriteriaPanel, { FlexiCriteriaSet } from '@ticatec/uniface-flexi-form/criteria-panel';
|
|
37
|
+
import '@ticatec/uniface-flexi-form/uniface-flexi-form.css';
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Simple Criteria Panel
|
|
41
|
+
|
|
42
|
+
```svelte
|
|
43
|
+
<!-- SimpleCriteriaPanel.svelte -->
|
|
44
|
+
<script lang="ts">
|
|
45
|
+
import CriteriaPanel from '@ticatec/uniface-flexi-form/criteria-panel';
|
|
46
|
+
|
|
47
|
+
// Search criteria data
|
|
48
|
+
let searchCriteria = {};
|
|
49
|
+
|
|
50
|
+
// Criteria panel schema
|
|
51
|
+
const userSearchSchema = {
|
|
52
|
+
arrangement: 'vertical',
|
|
53
|
+
variant: 'outlined',
|
|
54
|
+
fields: [
|
|
55
|
+
{
|
|
56
|
+
type: 'text-search',
|
|
57
|
+
name: 'userName',
|
|
58
|
+
label: 'User Name',
|
|
59
|
+
keys: { field: 'userName' },
|
|
60
|
+
size: 'x25',
|
|
61
|
+
props: {
|
|
62
|
+
placeholder: 'Enter user name'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'option-select-search',
|
|
67
|
+
name: 'status',
|
|
68
|
+
label: 'Status',
|
|
69
|
+
keys: { field: 'status' },
|
|
70
|
+
dictName: 'user-status',
|
|
71
|
+
size: 'x20',
|
|
72
|
+
props: {
|
|
73
|
+
placeholder: 'Select status'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'date-range',
|
|
78
|
+
name: 'registrationDate',
|
|
79
|
+
label: 'Registration Date',
|
|
80
|
+
keys: {
|
|
81
|
+
fromField: 'registeredFrom',
|
|
82
|
+
toField: 'registeredTo'
|
|
83
|
+
},
|
|
84
|
+
size: 'x30'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'number-range',
|
|
88
|
+
name: 'ageRange',
|
|
89
|
+
label: 'Age Range',
|
|
90
|
+
keys: {
|
|
91
|
+
minField: 'ageMin',
|
|
92
|
+
maxField: 'ageMax'
|
|
93
|
+
},
|
|
94
|
+
size: 'x25'
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function handleCriteriaChange(event) {
|
|
100
|
+
searchCriteria = event.detail.criteria;
|
|
101
|
+
console.log('Search criteria updated:', searchCriteria);
|
|
102
|
+
performSearch(searchCriteria);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleSearch(event) {
|
|
106
|
+
const criteria = event.detail.criteria;
|
|
107
|
+
console.log('Search triggered with criteria:', criteria);
|
|
108
|
+
executeSearch(criteria);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleReset() {
|
|
112
|
+
searchCriteria = {};
|
|
113
|
+
console.log('Search criteria reset');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function performSearch(criteria) {
|
|
117
|
+
// Implement your search logic
|
|
118
|
+
try {
|
|
119
|
+
const results = await searchAPI(criteria);
|
|
120
|
+
console.log('Search results:', results);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Search failed:', error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<div class="search-panel">
|
|
128
|
+
<h3>User Search</h3>
|
|
129
|
+
<CriteriaPanel
|
|
130
|
+
schema={userSearchSchema}
|
|
131
|
+
bind:criteria={searchCriteria}
|
|
132
|
+
on:change={handleCriteriaChange}
|
|
133
|
+
on:search={handleSearch}
|
|
134
|
+
on:reset={handleReset}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<style>
|
|
139
|
+
.search-panel {
|
|
140
|
+
padding: 16px;
|
|
141
|
+
border: 1px solid #ddd;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Schema Structure
|
|
148
|
+
|
|
149
|
+
### FlexiCriteriaSetSchema
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
interface FlexiCriteriaSetSchema {
|
|
153
|
+
class?: string; // CSS class for styling
|
|
154
|
+
variant?: 'outlined' | 'filled'; // Field variant
|
|
155
|
+
arrangement?: 'vertical' | 'horizontal'; // Layout direction
|
|
156
|
+
label$style?: string; // Label styling
|
|
157
|
+
fields: Array<FlexiCriteriaFieldSchema>; // Search fields
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### FlexiCriteriaFieldSchema
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
interface FlexiCriteriaFieldSchema {
|
|
165
|
+
type: string; // Field type identifier
|
|
166
|
+
name?: string; // Field name
|
|
167
|
+
label: string; // Display label
|
|
168
|
+
keys: { [key: string]: string }; // Data binding keys
|
|
169
|
+
dictName?: string; // Dictionary for options
|
|
170
|
+
visible?: boolean; // Field visibility
|
|
171
|
+
size?: 'x15' | 'x20' | 'x25' | 'x30' | 'x35' | 'x40'; // Field width
|
|
172
|
+
events?: Record<string, string>; // Event handlers
|
|
173
|
+
props?: any; // Field-specific properties
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Criteria Field Types
|
|
178
|
+
|
|
179
|
+
### Built-in Field Types
|
|
180
|
+
|
|
181
|
+
| Type | Description | Keys Required | Common Props |
|
|
182
|
+
|------|-------------|---------------|--------------|
|
|
183
|
+
| `text-search` | Text search field | `field` | `placeholder`, `operators` |
|
|
184
|
+
| `number-search` | Numeric search field | `field` | `min`, `max`, `operators` |
|
|
185
|
+
| `number-range` | Number range search | `minField`, `maxField` | `min`, `max`, `step` |
|
|
186
|
+
| `date-search` | Date search field | `field` | `min`, `max`, `format` |
|
|
187
|
+
| `date-range` | Date range search | `fromField`, `toField` | `min`, `max`, `format` |
|
|
188
|
+
| `datetime-search` | DateTime search | `field` | `min`, `max`, `format` |
|
|
189
|
+
| `option-select-search` | Single select search | `field` | `dictName`, `placeholder` |
|
|
190
|
+
| `option-multi-select-search` | Multi-select search | `field` | `dictName`, `maxSelections` |
|
|
191
|
+
| `input-option-select-search` | Searchable select | `field` | `getOptions`, `minLength` |
|
|
192
|
+
| `cascade-select-search` | Cascade select search | `field` | `dictName`, `levels` |
|
|
193
|
+
|
|
194
|
+
### Field Examples
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Text search with operators
|
|
198
|
+
{
|
|
199
|
+
type: 'text-search',
|
|
200
|
+
name: 'productName',
|
|
201
|
+
label: 'Product Name',
|
|
202
|
+
keys: { field: 'name' },
|
|
203
|
+
size: 'x30',
|
|
204
|
+
props: {
|
|
205
|
+
placeholder: 'Enter product name',
|
|
206
|
+
operators: ['contains', 'startsWith', 'endsWith', 'equals']
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Number range search
|
|
211
|
+
{
|
|
212
|
+
type: 'number-range',
|
|
213
|
+
name: 'priceRange',
|
|
214
|
+
label: 'Price Range',
|
|
215
|
+
keys: {
|
|
216
|
+
minField: 'priceMin',
|
|
217
|
+
maxField: 'priceMax'
|
|
218
|
+
},
|
|
219
|
+
size: 'x25',
|
|
220
|
+
props: {
|
|
221
|
+
min: 0,
|
|
222
|
+
max: 10000,
|
|
223
|
+
step: 10,
|
|
224
|
+
currency: 'USD'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Date range with constraints
|
|
229
|
+
{
|
|
230
|
+
type: 'date-range',
|
|
231
|
+
name: 'orderDate',
|
|
232
|
+
label: 'Order Date',
|
|
233
|
+
keys: {
|
|
234
|
+
fromField: 'orderDateFrom',
|
|
235
|
+
toField: 'orderDateTo'
|
|
236
|
+
},
|
|
237
|
+
size: 'x30',
|
|
238
|
+
props: {
|
|
239
|
+
min: '2020-01-01',
|
|
240
|
+
max: new Date().toISOString().split('T')[0],
|
|
241
|
+
format: 'YYYY-MM-DD'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Multi-select with dictionary
|
|
246
|
+
{
|
|
247
|
+
type: 'option-multi-select-search',
|
|
248
|
+
name: 'categories',
|
|
249
|
+
label: 'Categories',
|
|
250
|
+
keys: { field: 'categoryIds' },
|
|
251
|
+
dictName: 'product-categories',
|
|
252
|
+
size: 'x25',
|
|
253
|
+
props: {
|
|
254
|
+
maxSelections: 5,
|
|
255
|
+
placeholder: 'Select categories'
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Dynamic searchable select
|
|
260
|
+
{
|
|
261
|
+
type: 'input-option-select-search',
|
|
262
|
+
name: 'assignee',
|
|
263
|
+
label: 'Assigned To',
|
|
264
|
+
keys: { field: 'assigneeId' },
|
|
265
|
+
size: 'x25',
|
|
266
|
+
props: {
|
|
267
|
+
getOptions: 'loadUserSuggestions',
|
|
268
|
+
minLength: 2,
|
|
269
|
+
debounce: 300,
|
|
270
|
+
placeholder: 'Type to search users'
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Advanced Search Patterns
|
|
276
|
+
|
|
277
|
+
### Complex Search Builder
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
class AdvancedSearchBuilder {
|
|
281
|
+
private schema: FlexiCriteriaSetSchema;
|
|
282
|
+
|
|
283
|
+
constructor() {
|
|
284
|
+
this.schema = {
|
|
285
|
+
arrangement: 'vertical',
|
|
286
|
+
variant: 'outlined',
|
|
287
|
+
fields: []
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
addTextField(name: string, label: string, options: any = {}): this {
|
|
292
|
+
this.schema.fields.push({
|
|
293
|
+
type: 'text-search',
|
|
294
|
+
name,
|
|
295
|
+
label,
|
|
296
|
+
keys: { field: options.field || name },
|
|
297
|
+
size: options.size || 'x25',
|
|
298
|
+
props: {
|
|
299
|
+
placeholder: options.placeholder || `Enter ${label.toLowerCase()}`,
|
|
300
|
+
operators: options.operators || ['contains', 'equals']
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
addSelectField(name: string, label: string, dictName: string, options: any = {}): this {
|
|
307
|
+
this.schema.fields.push({
|
|
308
|
+
type: options.multi ? 'option-multi-select-search' : 'option-select-search',
|
|
309
|
+
name,
|
|
310
|
+
label,
|
|
311
|
+
keys: { field: options.field || name },
|
|
312
|
+
dictName,
|
|
313
|
+
size: options.size || 'x20',
|
|
314
|
+
props: {
|
|
315
|
+
placeholder: options.placeholder || `Select ${label.toLowerCase()}`,
|
|
316
|
+
maxSelections: options.maxSelections
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
addDateRangeField(name: string, label: string, options: any = {}): this {
|
|
323
|
+
this.schema.fields.push({
|
|
324
|
+
type: 'date-range',
|
|
325
|
+
name,
|
|
326
|
+
label,
|
|
327
|
+
keys: {
|
|
328
|
+
fromField: options.fromField || `${name}From`,
|
|
329
|
+
toField: options.toField || `${name}To`
|
|
330
|
+
},
|
|
331
|
+
size: options.size || 'x30',
|
|
332
|
+
props: {
|
|
333
|
+
min: options.min,
|
|
334
|
+
max: options.max,
|
|
335
|
+
format: options.format || 'YYYY-MM-DD'
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
addNumberRangeField(name: string, label: string, options: any = {}): this {
|
|
342
|
+
this.schema.fields.push({
|
|
343
|
+
type: 'number-range',
|
|
344
|
+
name,
|
|
345
|
+
label,
|
|
346
|
+
keys: {
|
|
347
|
+
minField: options.minField || `${name}Min`,
|
|
348
|
+
maxField: options.maxField || `${name}Max`
|
|
349
|
+
},
|
|
350
|
+
size: options.size || 'x25',
|
|
351
|
+
props: {
|
|
352
|
+
min: options.min,
|
|
353
|
+
max: options.max,
|
|
354
|
+
step: options.step,
|
|
355
|
+
precision: options.precision
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
build(): FlexiCriteriaSetSchema {
|
|
362
|
+
return this.schema;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Usage
|
|
367
|
+
const productSearchSchema = new AdvancedSearchBuilder()
|
|
368
|
+
.addTextField('name', 'Product Name', {
|
|
369
|
+
operators: ['contains', 'startsWith']
|
|
370
|
+
})
|
|
371
|
+
.addTextField('sku', 'SKU', {
|
|
372
|
+
operators: ['equals', 'startsWith']
|
|
373
|
+
})
|
|
374
|
+
.addSelectField('category', 'Category', 'product-categories')
|
|
375
|
+
.addSelectField('tags', 'Tags', 'product-tags', {
|
|
376
|
+
multi: true,
|
|
377
|
+
maxSelections: 3
|
|
378
|
+
})
|
|
379
|
+
.addNumberRangeField('price', 'Price Range', {
|
|
380
|
+
min: 0,
|
|
381
|
+
step: 0.01
|
|
382
|
+
})
|
|
383
|
+
.addDateRangeField('createdDate', 'Created Date')
|
|
384
|
+
.build();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Conditional Criteria Fields
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
class ConditionalCriteriaSet extends FlexiCriteriaSet {
|
|
391
|
+
private conditionalFields = new Map<string, string[]>();
|
|
392
|
+
|
|
393
|
+
constructor(schema: FlexiCriteriaSetSchema) {
|
|
394
|
+
super(schema);
|
|
395
|
+
this.setupConditionalLogic();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private setupConditionalLogic(): void {
|
|
399
|
+
// Define conditional relationships
|
|
400
|
+
this.conditionalFields.set('searchType', ['specificFields']);
|
|
401
|
+
|
|
402
|
+
// Setup change handlers
|
|
403
|
+
this.setupFieldChangeHandlers();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private setupFieldChangeHandlers(): void {
|
|
407
|
+
const searchTypeField = this.field['searchType'];
|
|
408
|
+
if (searchTypeField) {
|
|
409
|
+
searchTypeField.events.change = (value: string) => {
|
|
410
|
+
this.updateConditionalFields(value);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private updateConditionalFields(searchType: string): void {
|
|
416
|
+
const showAdvanced = searchType === 'advanced';
|
|
417
|
+
|
|
418
|
+
// Show/hide fields based on search type
|
|
419
|
+
this.fields.forEach(field => {
|
|
420
|
+
if (field.name?.startsWith('advanced_')) {
|
|
421
|
+
field.visible = showAdvanced;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
this.invalidate();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Usage
|
|
430
|
+
const conditionalSchema = {
|
|
431
|
+
arrangement: 'vertical',
|
|
432
|
+
fields: [
|
|
433
|
+
{
|
|
434
|
+
type: 'option-select-search',
|
|
435
|
+
name: 'searchType',
|
|
436
|
+
label: 'Search Type',
|
|
437
|
+
keys: { field: 'type' },
|
|
438
|
+
props: {
|
|
439
|
+
options: [
|
|
440
|
+
{ value: 'basic', label: 'Basic Search' },
|
|
441
|
+
{ value: 'advanced', label: 'Advanced Search' }
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
type: 'text-search',
|
|
447
|
+
name: 'basicQuery',
|
|
448
|
+
label: 'Search Query',
|
|
449
|
+
keys: { field: 'query' },
|
|
450
|
+
visible: true
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: 'text-search',
|
|
454
|
+
name: 'advanced_title',
|
|
455
|
+
label: 'Title',
|
|
456
|
+
keys: { field: 'title' },
|
|
457
|
+
visible: false
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
type: 'text-search',
|
|
461
|
+
name: 'advanced_content',
|
|
462
|
+
label: 'Content',
|
|
463
|
+
keys: { field: 'content' },
|
|
464
|
+
visible: false
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
};
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Dynamic Criteria Generation
|
|
471
|
+
|
|
472
|
+
### Loading Criteria from API
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
class DynamicCriteriaLoader {
|
|
476
|
+
private cache = new Map<string, FlexiCriteriaSetSchema>();
|
|
477
|
+
|
|
478
|
+
async loadCriteriaSchema(entityType: string): Promise<FlexiCriteriaSetSchema> {
|
|
479
|
+
if (this.cache.has(entityType)) {
|
|
480
|
+
return this.cache.get(entityType)!;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const response = await fetch(`/api/search/criteria/${entityType}`);
|
|
485
|
+
const schema = await response.json();
|
|
486
|
+
this.cache.set(entityType, schema);
|
|
487
|
+
return schema;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('Failed to load criteria schema:', error);
|
|
490
|
+
return this.getDefaultSchema();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async loadDictionaries(dictNames: string[]): Promise<Record<string, any[]>> {
|
|
495
|
+
const dictionaries: Record<string, any[]> = {};
|
|
496
|
+
|
|
497
|
+
for (const dictName of dictNames) {
|
|
498
|
+
try {
|
|
499
|
+
const response = await fetch(`/api/dictionaries/${dictName}`);
|
|
500
|
+
dictionaries[dictName] = await response.json();
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error(`Failed to load dictionary ${dictName}:`, error);
|
|
503
|
+
dictionaries[dictName] = [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return dictionaries;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private getDefaultSchema(): FlexiCriteriaSetSchema {
|
|
511
|
+
return {
|
|
512
|
+
arrangement: 'vertical',
|
|
513
|
+
fields: [
|
|
514
|
+
{
|
|
515
|
+
type: 'text-search',
|
|
516
|
+
name: 'query',
|
|
517
|
+
label: 'Search',
|
|
518
|
+
keys: { field: 'q' },
|
|
519
|
+
size: 'x40'
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Usage in Svelte component
|
|
527
|
+
let criteriaSchema = null;
|
|
528
|
+
let searchCriteria = {};
|
|
529
|
+
|
|
530
|
+
onMount(async () => {
|
|
531
|
+
const loader = new DynamicCriteriaLoader();
|
|
532
|
+
criteriaSchema = await loader.loadCriteriaSchema('products');
|
|
533
|
+
|
|
534
|
+
// Load required dictionaries
|
|
535
|
+
const dictNames = extractDictionaryNames(criteriaSchema);
|
|
536
|
+
const dictionaries = await loader.loadDictionaries(dictNames);
|
|
537
|
+
|
|
538
|
+
// Setup criteria set with dictionaries
|
|
539
|
+
setupCriteriaSet(criteriaSchema, dictionaries);
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Saved Search Patterns
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
class SavedSearchManager {
|
|
547
|
+
private storageKey = 'saved-searches';
|
|
548
|
+
|
|
549
|
+
saveSearch(name: string, criteria: any, schema: FlexiCriteriaSetSchema): void {
|
|
550
|
+
const savedSearches = this.getSavedSearches();
|
|
551
|
+
savedSearches[name] = {
|
|
552
|
+
criteria,
|
|
553
|
+
schema,
|
|
554
|
+
createdAt: new Date().toISOString()
|
|
555
|
+
};
|
|
556
|
+
localStorage.setItem(this.storageKey, JSON.stringify(savedSearches));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
loadSearch(name: string): { criteria: any; schema: FlexiCriteriaSetSchema } | null {
|
|
560
|
+
const savedSearches = this.getSavedSearches();
|
|
561
|
+
return savedSearches[name] || null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
listSavedSearches(): string[] {
|
|
565
|
+
const savedSearches = this.getSavedSearches();
|
|
566
|
+
return Object.keys(savedSearches);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
deleteSearch(name: string): void {
|
|
570
|
+
const savedSearches = this.getSavedSearches();
|
|
571
|
+
delete savedSearches[name];
|
|
572
|
+
localStorage.setItem(this.storageKey, JSON.stringify(savedSearches));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private getSavedSearches(): Record<string, any> {
|
|
576
|
+
try {
|
|
577
|
+
return JSON.parse(localStorage.getItem(this.storageKey) || '{}');
|
|
578
|
+
} catch {
|
|
579
|
+
return {};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Usage
|
|
585
|
+
const searchManager = new SavedSearchManager();
|
|
586
|
+
|
|
587
|
+
function saveCurrentSearch() {
|
|
588
|
+
const name = prompt('Enter search name:');
|
|
589
|
+
if (name) {
|
|
590
|
+
searchManager.saveSearch(name, searchCriteria, criteriaSchema);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function loadSavedSearch(name: string) {
|
|
595
|
+
const saved = searchManager.loadSearch(name);
|
|
596
|
+
if (saved) {
|
|
597
|
+
searchCriteria = saved.criteria;
|
|
598
|
+
criteriaSchema = saved.schema;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Events and Interactions
|
|
604
|
+
|
|
605
|
+
### Criteria Change Events
|
|
606
|
+
|
|
607
|
+
```svelte
|
|
608
|
+
<script>
|
|
609
|
+
function handleCriteriaChange(event) {
|
|
610
|
+
const { criteria, field, value } = event.detail;
|
|
611
|
+
console.log(`Field ${field} changed to:`, value);
|
|
612
|
+
console.log('Full criteria:', criteria);
|
|
613
|
+
|
|
614
|
+
// Trigger real-time search
|
|
615
|
+
debouncedSearch(criteria);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function handleFieldFocus(event) {
|
|
619
|
+
const { field } = event.detail;
|
|
620
|
+
console.log(`Field ${field} focused`);
|
|
621
|
+
|
|
622
|
+
// Load suggestions for this field
|
|
623
|
+
loadFieldSuggestions(field);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function handleOperatorChange(event) {
|
|
627
|
+
const { field, operator } = event.detail;
|
|
628
|
+
console.log(`Field ${field} operator changed to:`, operator);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Debounced search function
|
|
632
|
+
const debouncedSearch = debounce((criteria) => {
|
|
633
|
+
performSearch(criteria);
|
|
634
|
+
}, 300);
|
|
635
|
+
|
|
636
|
+
function debounce(func, wait) {
|
|
637
|
+
let timeout;
|
|
638
|
+
return function executedFunction(...args) {
|
|
639
|
+
const later = () => {
|
|
640
|
+
clearTimeout(timeout);
|
|
641
|
+
func(...args);
|
|
642
|
+
};
|
|
643
|
+
clearTimeout(timeout);
|
|
644
|
+
timeout = setTimeout(later, wait);
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
</script>
|
|
648
|
+
|
|
649
|
+
<CriteriaPanel
|
|
650
|
+
{schema}
|
|
651
|
+
bind:criteria={searchCriteria}
|
|
652
|
+
on:change={handleCriteriaChange}
|
|
653
|
+
on:fieldFocus={handleFieldFocus}
|
|
654
|
+
on:operatorChange={handleOperatorChange}
|
|
655
|
+
on:search={handleSearch}
|
|
656
|
+
on:reset={handleReset}
|
|
657
|
+
/>
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Custom Event Handlers
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
class CustomCriteriaEventHandler {
|
|
664
|
+
private criteriaSet: FlexiCriteriaSet;
|
|
665
|
+
|
|
666
|
+
constructor(criteriaSet: FlexiCriteriaSet) {
|
|
667
|
+
this.criteriaSet = criteriaSet;
|
|
668
|
+
this.setupEventHandlers();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private setupEventHandlers(): void {
|
|
672
|
+
// Setup field-specific event handlers
|
|
673
|
+
this.setupCategoryFieldHandler();
|
|
674
|
+
this.setupLocationFieldHandler();
|
|
675
|
+
this.setupDateRangeHandler();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private setupCategoryFieldHandler(): void {
|
|
679
|
+
const categoryField = this.criteriaSet.field['category'];
|
|
680
|
+
if (categoryField) {
|
|
681
|
+
categoryField.events.change = async (value: string) => {
|
|
682
|
+
console.log('Category changed:', value);
|
|
683
|
+
|
|
684
|
+
// Load subcategories based on selected category
|
|
685
|
+
await this.loadSubcategories(value);
|
|
686
|
+
|
|
687
|
+
// Update related fields
|
|
688
|
+
this.updateRelatedFields('category', value);
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private setupLocationFieldHandler(): void {
|
|
694
|
+
const countryField = this.criteriaSet.field['country'];
|
|
695
|
+
const stateField = this.criteriaSet.field['state'];
|
|
696
|
+
|
|
697
|
+
if (countryField && stateField) {
|
|
698
|
+
countryField.events.change = async (country: string) => {
|
|
699
|
+
// Clear state field
|
|
700
|
+
stateField.data = null;
|
|
701
|
+
|
|
702
|
+
// Load states for selected country
|
|
703
|
+
const states = await this.loadStates(country);
|
|
704
|
+
stateField.props.options = states;
|
|
705
|
+
|
|
706
|
+
this.criteriaSet.invalidate();
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private setupDateRangeHandler(): void {
|
|
712
|
+
const fromDateField = this.criteriaSet.field['fromDate'];
|
|
713
|
+
const toDateField = this.criteriaSet.field['toDate'];
|
|
714
|
+
|
|
715
|
+
if (fromDateField && toDateField) {
|
|
716
|
+
fromDateField.events.change = (fromDate: string) => {
|
|
717
|
+
// Update minimum date for toDate field
|
|
718
|
+
toDateField.props.min = fromDate;
|
|
719
|
+
this.criteriaSet.invalidate();
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
toDateField.events.change = (toDate: string) => {
|
|
723
|
+
// Update maximum date for fromDate field
|
|
724
|
+
fromDateField.props.max = toDate;
|
|
725
|
+
this.criteriaSet.invalidate();
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private async loadSubcategories(categoryId: string): Promise<void> {
|
|
731
|
+
try {
|
|
732
|
+
const response = await fetch(`/api/categories/${categoryId}/subcategories`);
|
|
733
|
+
const subcategories = await response.json();
|
|
734
|
+
|
|
735
|
+
const subcategoryField = this.criteriaSet.field['subcategory'];
|
|
736
|
+
if (subcategoryField) {
|
|
737
|
+
subcategoryField.props.options = subcategories;
|
|
738
|
+
this.criteriaSet.invalidate();
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error('Failed to load subcategories:', error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private async loadStates(countryCode: string): Promise<any[]> {
|
|
746
|
+
try {
|
|
747
|
+
const response = await fetch(`/api/countries/${countryCode}/states`);
|
|
748
|
+
return await response.json();
|
|
749
|
+
} catch (error) {
|
|
750
|
+
console.error('Failed to load states:', error);
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private updateRelatedFields(changedField: string, value: any): void {
|
|
756
|
+
// Implement business logic for updating related fields
|
|
757
|
+
console.log(`Updating fields related to ${changedField}:`, value);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Extending FlexiCriteriaSet
|
|
763
|
+
|
|
764
|
+
### Creating Custom Criteria Field Types
|
|
765
|
+
|
|
766
|
+
#### 1. Custom Range Slider Field
|
|
767
|
+
|
|
768
|
+
```svelte
|
|
769
|
+
<!-- CustomRangeSliderField.svelte -->
|
|
770
|
+
<script lang="ts">
|
|
771
|
+
import type FlexiCriteriaField from "$lib/criteria-panel/lib/FlexiCriteriaField";
|
|
772
|
+
|
|
773
|
+
export let field: FlexiCriteriaField;
|
|
774
|
+
export let readonly: boolean = false;
|
|
775
|
+
export let disabled: boolean = false;
|
|
776
|
+
|
|
777
|
+
$: props = field.props;
|
|
778
|
+
$: keys = field.keys;
|
|
779
|
+
|
|
780
|
+
let minValue = props.min || 0;
|
|
781
|
+
let maxValue = props.max || 100;
|
|
782
|
+
let currentMin = minValue;
|
|
783
|
+
let currentMax = maxValue;
|
|
784
|
+
|
|
785
|
+
// Initialize from data
|
|
786
|
+
if (field.data) {
|
|
787
|
+
currentMin = field.data[keys.minField] || minValue;
|
|
788
|
+
currentMax = field.data[keys.maxField] || maxValue;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function updateRange() {
|
|
792
|
+
if (!field.data) field.data = {};
|
|
793
|
+
field.data[keys.minField] = currentMin;
|
|
794
|
+
field.data[keys.maxField] = currentMax;
|
|
795
|
+
|
|
796
|
+
// Trigger change event
|
|
797
|
+
field.events.change?.({ min: currentMin, max: currentMax });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
$: if (currentMin !== undefined && currentMax !== undefined) {
|
|
801
|
+
updateRange();
|
|
802
|
+
}
|
|
803
|
+
</script>
|
|
804
|
+
|
|
805
|
+
<div class="range-slider-field" class:readonly class:disabled>
|
|
806
|
+
<label>{field.label}</label>
|
|
807
|
+
|
|
808
|
+
<div class="range-inputs">
|
|
809
|
+
<input
|
|
810
|
+
type="number"
|
|
811
|
+
bind:value={currentMin}
|
|
812
|
+
min={minValue}
|
|
813
|
+
max={currentMax}
|
|
814
|
+
{disabled}
|
|
815
|
+
{readonly}
|
|
816
|
+
class="range-input"
|
|
817
|
+
/>
|
|
818
|
+
<span class="range-separator">to</span>
|
|
819
|
+
<input
|
|
820
|
+
type="number"
|
|
821
|
+
bind:value={currentMax}
|
|
822
|
+
min={currentMin}
|
|
823
|
+
max={maxValue}
|
|
824
|
+
{disabled}
|
|
825
|
+
{readonly}
|
|
826
|
+
class="range-input"
|
|
827
|
+
/>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
<div class="slider-container">
|
|
831
|
+
<input
|
|
832
|
+
type="range"
|
|
833
|
+
bind:value={currentMin}
|
|
834
|
+
min={minValue}
|
|
835
|
+
max={maxValue}
|
|
836
|
+
step={props.step || 1}
|
|
837
|
+
{disabled}
|
|
838
|
+
{readonly}
|
|
839
|
+
class="slider slider-min"
|
|
840
|
+
/>
|
|
841
|
+
<input
|
|
842
|
+
type="range"
|
|
843
|
+
bind:value={currentMax}
|
|
844
|
+
min={minValue}
|
|
845
|
+
max={maxValue}
|
|
846
|
+
step={props.step || 1}
|
|
847
|
+
{disabled}
|
|
848
|
+
{readonly}
|
|
849
|
+
class="slider slider-max"
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<div class="range-display">
|
|
854
|
+
{props.prefix || ''}{currentMin} - {currentMax}{props.suffix || ''}
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<style>
|
|
859
|
+
.range-slider-field {
|
|
860
|
+
display: flex;
|
|
861
|
+
flex-direction: column;
|
|
862
|
+
gap: 8px;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.range-inputs {
|
|
866
|
+
display: flex;
|
|
867
|
+
align-items: center;
|
|
868
|
+
gap: 8px;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.range-input {
|
|
872
|
+
width: 80px;
|
|
873
|
+
padding: 4px 8px;
|
|
874
|
+
border: 1px solid #ddd;
|
|
875
|
+
border-radius: 4px;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.range-separator {
|
|
879
|
+
color: #666;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.slider-container {
|
|
883
|
+
position: relative;
|
|
884
|
+
height: 20px;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.slider {
|
|
888
|
+
position: absolute;
|
|
889
|
+
width: 100%;
|
|
890
|
+
height: 20px;
|
|
891
|
+
-webkit-appearance: none;
|
|
892
|
+
background: transparent;
|
|
893
|
+
outline: none;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.slider::-webkit-slider-track {
|
|
897
|
+
height: 4px;
|
|
898
|
+
background: #ddd;
|
|
899
|
+
border-radius: 2px;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.slider::-webkit-slider-thumb {
|
|
903
|
+
-webkit-appearance: none;
|
|
904
|
+
width: 16px;
|
|
905
|
+
height: 16px;
|
|
906
|
+
background: #007bff;
|
|
907
|
+
border-radius: 50%;
|
|
908
|
+
cursor: pointer;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.range-display {
|
|
912
|
+
text-align: center;
|
|
913
|
+
font-weight: bold;
|
|
914
|
+
color: #333;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.readonly, .disabled {
|
|
918
|
+
opacity: 0.6;
|
|
919
|
+
pointer-events: none;
|
|
920
|
+
}
|
|
921
|
+
</style>
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### 2. Register Custom Field Type
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
import CriteriaComponentBuilder from '@ticatec/uniface-flexi-form/criteria-panel/lib/CriteriaComponentBuilder';
|
|
928
|
+
import CustomRangeSliderField from './CustomRangeSliderField.svelte';
|
|
929
|
+
|
|
930
|
+
// Create builder function
|
|
931
|
+
const buildRangeSliderField = (schema: FlexiCriteriaFieldSchema, dictLoader: DictionaryLoader) => {
|
|
932
|
+
return {
|
|
933
|
+
component: CustomRangeSliderField,
|
|
934
|
+
props: schema.props,
|
|
935
|
+
keyFields: schema.keys
|
|
936
|
+
};
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// Register the custom field type
|
|
940
|
+
const componentBuilder = CriteriaComponentBuilder.getInstance();
|
|
941
|
+
componentBuilder.register('range-slider', buildRangeSliderField);
|
|
942
|
+
|
|
943
|
+
// Use in schema
|
|
944
|
+
const schemaWithRangeSlider = {
|
|
945
|
+
arrangement: 'vertical',
|
|
946
|
+
fields: [
|
|
947
|
+
{
|
|
948
|
+
type: 'range-slider',
|
|
949
|
+
name: 'priceRange',
|
|
950
|
+
label: 'Price Range',
|
|
951
|
+
keys: {
|
|
952
|
+
minField: 'priceMin',
|
|
953
|
+
maxField: 'priceMax'
|
|
954
|
+
},
|
|
955
|
+
size: 'x30',
|
|
956
|
+
props: {
|
|
957
|
+
min: 0,
|
|
958
|
+
max: 1000,
|
|
959
|
+
step: 10,
|
|
960
|
+
prefix: '$',
|
|
961
|
+
suffix: ' USD'
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
]
|
|
965
|
+
};
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Creating Custom Criteria Sets
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
class AdvancedProductCriteriaSet extends FlexiCriteriaSet {
|
|
972
|
+
private productCategories: any[] = [];
|
|
973
|
+
private priceHistory: any[] = [];
|
|
974
|
+
|
|
975
|
+
constructor(schema: FlexiCriteriaSetSchema) {
|
|
976
|
+
super(schema);
|
|
977
|
+
this.setupAdvancedFeatures();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private async setupAdvancedFeatures(): Promise<void> {
|
|
981
|
+
await this.loadProductCategories();
|
|
982
|
+
await this.loadPriceHistory();
|
|
983
|
+
this.setupSmartDefaults();
|
|
984
|
+
this.setupAdvancedValidation();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private async loadProductCategories(): Promise<void> {
|
|
988
|
+
try {
|
|
989
|
+
const response = await fetch('/api/products/categories/tree');
|
|
990
|
+
this.productCategories = await response.json();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error('Failed to load product categories:', error);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private async loadPriceHistory(): Promise<void> {
|
|
997
|
+
try {
|
|
998
|
+
const response = await fetch('/api/products/price-history');
|
|
999
|
+
this.priceHistory = await response.json();
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
console.error('Failed to load price history:', error);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private setupSmartDefaults(): void {
|
|
1006
|
+
// Set intelligent defaults based on user history or popular searches
|
|
1007
|
+
const categoryField = this.field['category'];
|
|
1008
|
+
const priceField = this.field['priceRange'];
|
|
1009
|
+
|
|
1010
|
+
if (categoryField && this.productCategories.length > 0) {
|
|
1011
|
+
// Set most popular category as default
|
|
1012
|
+
const popularCategory = this.getMostPopularCategory();
|
|
1013
|
+
categoryField.data = popularCategory?.id;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (priceField && this.priceHistory.length > 0) {
|
|
1017
|
+
// Set common price range as default
|
|
1018
|
+
const commonRange = this.getCommonPriceRange();
|
|
1019
|
+
priceField.data = commonRange;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private setupAdvancedValidation(): void {
|
|
1024
|
+
// Add cross-field validation
|
|
1025
|
+
this.addValidator('priceRange', (value: any) => {
|
|
1026
|
+
if (value.min > value.max) {
|
|
1027
|
+
return 'Minimum price cannot be greater than maximum price';
|
|
1028
|
+
}
|
|
1029
|
+
return null;
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
this.addValidator('dateRange', (value: any) => {
|
|
1033
|
+
if (new Date(value.from) > new Date(value.to)) {
|
|
1034
|
+
return 'Start date cannot be after end date';
|
|
1035
|
+
}
|
|
1036
|
+
return null;
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private getMostPopularCategory(): any {
|
|
1041
|
+
// Implement logic to determine most popular category
|
|
1042
|
+
return this.productCategories[0];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private getCommonPriceRange(): any {
|
|
1046
|
+
// Implement logic to determine common price range
|
|
1047
|
+
return { min: 10, max: 100 };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private addValidator(fieldName: string, validator: (value: any) => string | null): void {
|
|
1051
|
+
const field = this.field[fieldName];
|
|
1052
|
+
if (field) {
|
|
1053
|
+
const originalChange = field.events.change;
|
|
1054
|
+
field.events.change = (value: any) => {
|
|
1055
|
+
const error = validator(value);
|
|
1056
|
+
if (error) {
|
|
1057
|
+
console.error(`Validation error for ${fieldName}:`, error);
|
|
1058
|
+
// Handle validation error
|
|
1059
|
+
} else {
|
|
1060
|
+
originalChange?.(value);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Public API methods
|
|
1067
|
+
getCriteriaQuery(): any {
|
|
1068
|
+
const criteria = {};
|
|
1069
|
+
|
|
1070
|
+
for (const field of this.fields) {
|
|
1071
|
+
if (field.data !== null && field.data !== undefined) {
|
|
1072
|
+
// Convert field data to query format
|
|
1073
|
+
const queryPart = this.convertFieldToQuery(field);
|
|
1074
|
+
Object.assign(criteria, queryPart);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return criteria;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private convertFieldToQuery(field: FlexiCriteriaField): any {
|
|
1082
|
+
const query = {};
|
|
1083
|
+
|
|
1084
|
+
switch (field.component.name) {
|
|
1085
|
+
case 'TextSearchField':
|
|
1086
|
+
query[field.keys.field] = {
|
|
1087
|
+
operator: field.props.selectedOperator || 'contains',
|
|
1088
|
+
value: field.data
|
|
1089
|
+
};
|
|
1090
|
+
break;
|
|
1091
|
+
|
|
1092
|
+
case 'RangeSliderField':
|
|
1093
|
+
if (field.data.min !== undefined) {
|
|
1094
|
+
query[field.keys.minField] = {
|
|
1095
|
+
operator: 'gte',
|
|
1096
|
+
value: field.data.min
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
if (field.data.max !== undefined) {
|
|
1100
|
+
query[field.keys.maxField] = {
|
|
1101
|
+
operator: 'lte',
|
|
1102
|
+
value: field.data.max
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
|
|
1107
|
+
// Handle other field types...
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return query;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
exportCriteria(): string {
|
|
1114
|
+
return JSON.stringify(this.getCriteriaQuery(), null, 2);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
importCriteria(jsonString: string): void {
|
|
1118
|
+
try {
|
|
1119
|
+
const criteria = JSON.parse(jsonString);
|
|
1120
|
+
this.applyCriteria(criteria);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
console.error('Failed to import criteria:', error);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private applyCriteria(criteria: any): void {
|
|
1127
|
+
// Apply imported criteria to fields
|
|
1128
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
1129
|
+
const field = this.findFieldByKey(key);
|
|
1130
|
+
if (field) {
|
|
1131
|
+
field.data = value;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
this.invalidate();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private findFieldByKey(key: string): FlexiCriteriaField | null {
|
|
1138
|
+
for (const field of this.fields) {
|
|
1139
|
+
if (Object.values(field.keys).includes(key)) {
|
|
1140
|
+
return field;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
## Integration Patterns
|
|
1149
|
+
|
|
1150
|
+
### Integration with Data Tables
|
|
1151
|
+
|
|
1152
|
+
```svelte
|
|
1153
|
+
<!-- ProductSearchAndTable.svelte -->
|
|
1154
|
+
<script lang="ts">
|
|
1155
|
+
import CriteriaPanel from '@ticatec/uniface-flexi-form/criteria-panel';
|
|
1156
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
1157
|
+
|
|
1158
|
+
let searchCriteria = {};
|
|
1159
|
+
let tableData = [];
|
|
1160
|
+
let loading = false;
|
|
1161
|
+
let totalRecords = 0;
|
|
1162
|
+
let currentPage = 1;
|
|
1163
|
+
let pageSize = 20;
|
|
1164
|
+
|
|
1165
|
+
const criteriaSchema = {
|
|
1166
|
+
arrangement: 'horizontal',
|
|
1167
|
+
fields: [
|
|
1168
|
+
{
|
|
1169
|
+
type: 'text-search',
|
|
1170
|
+
name: 'name',
|
|
1171
|
+
label: 'Product Name',
|
|
1172
|
+
keys: { field: 'name' },
|
|
1173
|
+
size: 'x25'
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
type: 'option-select-search',
|
|
1177
|
+
name: 'category',
|
|
1178
|
+
label: 'Category',
|
|
1179
|
+
keys: { field: 'categoryId' },
|
|
1180
|
+
dictName: 'categories',
|
|
1181
|
+
size: 'x20'
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
type: 'number-range',
|
|
1185
|
+
name: 'price',
|
|
1186
|
+
label: 'Price Range',
|
|
1187
|
+
keys: { minField: 'priceMin', maxField: 'priceMax' },
|
|
1188
|
+
size: 'x25'
|
|
1189
|
+
}
|
|
1190
|
+
]
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const tableColumns = [
|
|
1194
|
+
{ field: 'name', text: 'Product Name', width: 200 },
|
|
1195
|
+
{ field: 'category', text: 'Category', width: 150 },
|
|
1196
|
+
{ field: 'price', text: 'Price', width: 100, formatter: 'currency' },
|
|
1197
|
+
{ field: 'stock', text: 'Stock', width: 100 },
|
|
1198
|
+
{ field: 'status', text: 'Status', width: 120 }
|
|
1199
|
+
];
|
|
1200
|
+
|
|
1201
|
+
// Reactive search when criteria changes
|
|
1202
|
+
$: if (searchCriteria) {
|
|
1203
|
+
performSearch();
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function performSearch() {
|
|
1207
|
+
loading = true;
|
|
1208
|
+
try {
|
|
1209
|
+
const queryParams = new URLSearchParams({
|
|
1210
|
+
page: currentPage.toString(),
|
|
1211
|
+
limit: pageSize.toString(),
|
|
1212
|
+
...flattenCriteria(searchCriteria)
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const response = await fetch(`/api/products/search?${queryParams}`);
|
|
1216
|
+
const result = await response.json();
|
|
1217
|
+
|
|
1218
|
+
tableData = result.data;
|
|
1219
|
+
totalRecords = result.total;
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
console.error('Search failed:', error);
|
|
1222
|
+
tableData = [];
|
|
1223
|
+
} finally {
|
|
1224
|
+
loading = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function flattenCriteria(criteria) {
|
|
1229
|
+
const flattened = {};
|
|
1230
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
1231
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
1232
|
+
flattened[key] = value;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return flattened;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function handlePageChange(event) {
|
|
1239
|
+
currentPage = event.detail.page;
|
|
1240
|
+
performSearch();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function handleCriteriaChange(event) {
|
|
1244
|
+
searchCriteria = event.detail.criteria;
|
|
1245
|
+
currentPage = 1; // Reset to first page on new search
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function handleReset() {
|
|
1249
|
+
searchCriteria = {};
|
|
1250
|
+
currentPage = 1;
|
|
1251
|
+
}
|
|
1252
|
+
</script>
|
|
1253
|
+
|
|
1254
|
+
<div class="search-and-table">
|
|
1255
|
+
<div class="search-panel">
|
|
1256
|
+
<h3>Product Search</h3>
|
|
1257
|
+
<CriteriaPanel
|
|
1258
|
+
schema={criteriaSchema}
|
|
1259
|
+
bind:criteria={searchCriteria}
|
|
1260
|
+
on:change={handleCriteriaChange}
|
|
1261
|
+
on:reset={handleReset}
|
|
1262
|
+
/>
|
|
1263
|
+
</div>
|
|
1264
|
+
|
|
1265
|
+
<div class="results-panel">
|
|
1266
|
+
<div class="results-header">
|
|
1267
|
+
<h3>Search Results ({totalRecords} items)</h3>
|
|
1268
|
+
{#if loading}
|
|
1269
|
+
<div class="loading">Searching...</div>
|
|
1270
|
+
{/if}
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
<DataTable
|
|
1274
|
+
columns={tableColumns}
|
|
1275
|
+
data={tableData}
|
|
1276
|
+
{totalRecords}
|
|
1277
|
+
{currentPage}
|
|
1278
|
+
{pageSize}
|
|
1279
|
+
on:pageChange={handlePageChange}
|
|
1280
|
+
/>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
|
|
1284
|
+
<style>
|
|
1285
|
+
.search-and-table {
|
|
1286
|
+
display: flex;
|
|
1287
|
+
flex-direction: column;
|
|
1288
|
+
gap: 20px;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.search-panel {
|
|
1292
|
+
background: #f8f9fa;
|
|
1293
|
+
padding: 16px;
|
|
1294
|
+
border-radius: 8px;
|
|
1295
|
+
border: 1px solid #dee2e6;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.results-panel {
|
|
1299
|
+
flex: 1;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.results-header {
|
|
1303
|
+
display: flex;
|
|
1304
|
+
justify-content: space-between;
|
|
1305
|
+
align-items: center;
|
|
1306
|
+
margin-bottom: 16px;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.loading {
|
|
1310
|
+
color: #6c757d;
|
|
1311
|
+
font-style: italic;
|
|
1312
|
+
}
|
|
1313
|
+
</style>
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### URL State Management
|
|
1317
|
+
|
|
1318
|
+
```typescript
|
|
1319
|
+
class CriteriaURLManager {
|
|
1320
|
+
private criteria: any = {};
|
|
1321
|
+
private schema: FlexiCriteriaSetSchema;
|
|
1322
|
+
|
|
1323
|
+
constructor(schema: FlexiCriteriaSetSchema) {
|
|
1324
|
+
this.schema = schema;
|
|
1325
|
+
this.loadFromURL();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
loadFromURL(): any {
|
|
1329
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1330
|
+
const criteria = {};
|
|
1331
|
+
|
|
1332
|
+
for (const field of this.schema.fields) {
|
|
1333
|
+
const keys = field.keys || {};
|
|
1334
|
+
|
|
1335
|
+
for (const [keyName, paramName] of Object.entries(keys)) {
|
|
1336
|
+
const value = urlParams.get(paramName);
|
|
1337
|
+
if (value !== null) {
|
|
1338
|
+
if (!criteria[field.name!]) {
|
|
1339
|
+
criteria[field.name!] = {};
|
|
1340
|
+
}
|
|
1341
|
+
criteria[field.name!][keyName] = this.parseURLValue(value, field.type);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
this.criteria = criteria;
|
|
1347
|
+
return criteria;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
updateURL(criteria: any): void {
|
|
1351
|
+
const urlParams = new URLSearchParams();
|
|
1352
|
+
|
|
1353
|
+
for (const field of this.schema.fields) {
|
|
1354
|
+
const fieldCriteria = criteria[field.name!];
|
|
1355
|
+
if (fieldCriteria) {
|
|
1356
|
+
const keys = field.keys || {};
|
|
1357
|
+
|
|
1358
|
+
for (const [keyName, paramName] of Object.entries(keys)) {
|
|
1359
|
+
const value = fieldCriteria[keyName];
|
|
1360
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
1361
|
+
urlParams.set(paramName, this.serializeURLValue(value));
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const newURL = `${window.location.pathname}?${urlParams.toString()}`;
|
|
1368
|
+
window.history.replaceState({}, '', newURL);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private parseURLValue(value: string, fieldType: string): any {
|
|
1372
|
+
switch (fieldType) {
|
|
1373
|
+
case 'number-search':
|
|
1374
|
+
case 'number-range':
|
|
1375
|
+
return parseFloat(value);
|
|
1376
|
+
case 'date-search':
|
|
1377
|
+
case 'date-range':
|
|
1378
|
+
return value; // Keep as string for date inputs
|
|
1379
|
+
case 'option-multi-select-search':
|
|
1380
|
+
return value.split(',');
|
|
1381
|
+
default:
|
|
1382
|
+
return value;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
private serializeURLValue(value: any): string {
|
|
1387
|
+
if (Array.isArray(value)) {
|
|
1388
|
+
return value.join(',');
|
|
1389
|
+
}
|
|
1390
|
+
return String(value);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
getCriteria(): any {
|
|
1394
|
+
return this.criteria;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Usage in component
|
|
1399
|
+
let criteriaURLManager: CriteriaURLManager;
|
|
1400
|
+
let searchCriteria = {};
|
|
1401
|
+
|
|
1402
|
+
onMount(() => {
|
|
1403
|
+
criteriaURLManager = new CriteriaURLManager(criteriaSchema);
|
|
1404
|
+
searchCriteria = criteriaURLManager.loadFromURL();
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
function handleCriteriaChange(event) {
|
|
1408
|
+
searchCriteria = event.detail.criteria;
|
|
1409
|
+
criteriaURLManager.updateURL(searchCriteria);
|
|
1410
|
+
}
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
## Best Practices
|
|
1414
|
+
|
|
1415
|
+
### 1. Schema Organization
|
|
1416
|
+
|
|
1417
|
+
```typescript
|
|
1418
|
+
// Good: Organized schema with constants
|
|
1419
|
+
const FIELD_SIZES = {
|
|
1420
|
+
SMALL: 'x15',
|
|
1421
|
+
MEDIUM: 'x25',
|
|
1422
|
+
LARGE: 'x35',
|
|
1423
|
+
FULL: 'x40'
|
|
1424
|
+
} as const;
|
|
1425
|
+
|
|
1426
|
+
const COMMON_FIELDS = {
|
|
1427
|
+
NAME_SEARCH: {
|
|
1428
|
+
type: 'text-search',
|
|
1429
|
+
keys: { field: 'name' },
|
|
1430
|
+
size: FIELD_SIZES.MEDIUM,
|
|
1431
|
+
props: {
|
|
1432
|
+
placeholder: 'Enter name',
|
|
1433
|
+
operators: ['contains', 'startsWith', 'equals']
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
|
|
1437
|
+
STATUS_SELECT: {
|
|
1438
|
+
type: 'option-select-search',
|
|
1439
|
+
keys: { field: 'status' },
|
|
1440
|
+
dictName: 'status-options',
|
|
1441
|
+
size: FIELD_SIZES.SMALL
|
|
1442
|
+
}
|
|
1443
|
+
} as const;
|
|
1444
|
+
|
|
1445
|
+
const PRODUCT_SEARCH_SCHEMA = {
|
|
1446
|
+
arrangement: 'horizontal' as const,
|
|
1447
|
+
variant: 'outlined' as const,
|
|
1448
|
+
fields: [
|
|
1449
|
+
{
|
|
1450
|
+
...COMMON_FIELDS.NAME_SEARCH,
|
|
1451
|
+
name: 'productName',
|
|
1452
|
+
label: 'Product Name'
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
...COMMON_FIELDS.STATUS_SELECT,
|
|
1456
|
+
name: 'status',
|
|
1457
|
+
label: 'Status'
|
|
1458
|
+
}
|
|
1459
|
+
]
|
|
1460
|
+
};
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
### 2. Performance Optimization
|
|
1464
|
+
|
|
1465
|
+
```typescript
|
|
1466
|
+
// Debounced search for real-time filtering
|
|
1467
|
+
class OptimizedCriteriaHandler {
|
|
1468
|
+
private searchDebounceTime = 300;
|
|
1469
|
+
private dictionaryCache = new Map();
|
|
1470
|
+
|
|
1471
|
+
createDebouncedSearch(searchFunction: Function) {
|
|
1472
|
+
let timeoutId: number;
|
|
1473
|
+
|
|
1474
|
+
return (criteria: any) => {
|
|
1475
|
+
clearTimeout(timeoutId);
|
|
1476
|
+
timeoutId = setTimeout(() => {
|
|
1477
|
+
searchFunction(criteria);
|
|
1478
|
+
}, this.searchDebounceTime);
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
async getCachedDictionary(dictName: string): Promise<any[]> {
|
|
1483
|
+
if (this.dictionaryCache.has(dictName)) {
|
|
1484
|
+
return this.dictionaryCache.get(dictName);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const dictionary = await this.loadDictionary(dictName);
|
|
1488
|
+
this.dictionaryCache.set(dictName, dictionary);
|
|
1489
|
+
return dictionary;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
private async loadDictionary(dictName: string): Promise<any[]> {
|
|
1493
|
+
const response = await fetch(`/api/dictionaries/${dictName}`);
|
|
1494
|
+
return await response.json();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
### 3. Error Handling
|
|
1500
|
+
|
|
1501
|
+
```typescript
|
|
1502
|
+
class CriteriaErrorHandler {
|
|
1503
|
+
static handleCriteriaError(error: any, criteriaSet: FlexiCriteriaSet): void {
|
|
1504
|
+
if (error.fieldErrors) {
|
|
1505
|
+
this.displayFieldErrors(error.fieldErrors, criteriaSet);
|
|
1506
|
+
} else if (error.networkError) {
|
|
1507
|
+
this.displayNetworkError();
|
|
1508
|
+
} else {
|
|
1509
|
+
this.displayGenericError(error);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private static displayFieldErrors(errors: Record<string, string>, criteriaSet: FlexiCriteriaSet): void {
|
|
1514
|
+
for (const [fieldName, errorMessage] of Object.entries(errors)) {
|
|
1515
|
+
const field = criteriaSet.field[fieldName];
|
|
1516
|
+
if (field) {
|
|
1517
|
+
// Display error near field
|
|
1518
|
+
console.error(`Field ${fieldName}: ${errorMessage}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private static displayNetworkError(): void {
|
|
1524
|
+
console.error('Network error occurred while searching');
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
private static displayGenericError(error: any): void {
|
|
1528
|
+
console.error('Search error:', error);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
### 4. Accessibility
|
|
1534
|
+
|
|
1535
|
+
```typescript
|
|
1536
|
+
const accessibleCriteriaSchema = {
|
|
1537
|
+
arrangement: 'vertical',
|
|
1538
|
+
fields: [
|
|
1539
|
+
{
|
|
1540
|
+
type: 'text-search',
|
|
1541
|
+
name: 'search',
|
|
1542
|
+
label: 'Search Products',
|
|
1543
|
+
keys: { field: 'query' },
|
|
1544
|
+
props: {
|
|
1545
|
+
placeholder: 'Enter search terms',
|
|
1546
|
+
'aria-label': 'Search products by name or description',
|
|
1547
|
+
'aria-describedby': 'search-help'
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
]
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// Add help text and ARIA attributes
|
|
1554
|
+
const helpText = {
|
|
1555
|
+
'search-help': 'Use keywords to search product names and descriptions'
|
|
1556
|
+
};
|
|
1557
|
+
```
|
|
1558
|
+
|
|
1559
|
+
This comprehensive guide provides everything needed to effectively use and extend FlexiCriteriaSet for building sophisticated search interfaces. The examples progress from basic usage to advanced patterns, showing how to create powerful search experiences with FlexiCriteriaSet's flexible architecture.
|