@rkosafo/cai.components 0.0.75 → 0.0.78
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/README.md +8 -8
- package/dist/baseEditor/index.svelte +32 -32
- package/dist/builders/filters/FilterBuilder.svelte +641 -641
- package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
- package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
- package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
- package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
- package/dist/forms/FormInput/FormInput.svelte +87 -87
- package/dist/forms/FormRadio/FormRadio.svelte +53 -53
- package/dist/forms/FormSelect/FormSelect.svelte +88 -88
- package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
- package/dist/forms/button-toggle/ButtonToggle.svelte +119 -0
- package/dist/forms/button-toggle/ButtonToggle.svelte.d.ts +139 -0
- package/dist/forms/button-toggle/ButtonToggleGroup.svelte +0 -0
- package/dist/forms/button-toggle/ButtonToggleGroup.svelte.d.ts +26 -0
- package/dist/forms/button-toggle/CheckIcon.svelte +28 -0
- package/dist/forms/button-toggle/CheckIcon.svelte.d.ts +4 -0
- package/dist/forms/button-toggle/index.d.ts +4 -0
- package/dist/forms/button-toggle/index.js +4 -0
- package/dist/forms/button-toggle/theme.d.ts +347 -0
- package/dist/forms/button-toggle/theme.js +129 -0
- package/dist/forms/checkbox/Checkbox.svelte +82 -82
- package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
- package/dist/forms/datepicker/Datepicker.svelte +707 -707
- package/dist/forms/form/Form.svelte +69 -69
- package/dist/forms/input/Input.svelte +363 -363
- package/dist/forms/label/Label.svelte +38 -38
- package/dist/forms/radio/Radio.svelte +48 -48
- package/dist/forms/radio/RadioButton.svelte +22 -22
- package/dist/forms/select/Select.svelte +56 -56
- package/dist/forms/textarea/Textarea.svelte +165 -165
- package/dist/forms/toggle/Toggle.svelte +70 -0
- package/dist/forms/toggle/Toggle.svelte.d.ts +3 -0
- package/dist/forms/toggle/index.d.ts +2 -0
- package/dist/forms/toggle/index.js +2 -0
- package/dist/forms/toggle/theme.d.ts +280 -0
- package/dist/forms/toggle/theme.js +97 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/layout/Chat/CategorySelector.svelte +52 -52
- package/dist/layout/Chat/ChatEntry.svelte +246 -246
- package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
- package/dist/layout/Chat/ChatHeader.svelte +172 -172
- package/dist/layout/Chat/ChatInput.svelte +207 -207
- package/dist/layout/Chat/DraggableWindow.svelte +230 -230
- package/dist/layout/Chat/PreviewPage.svelte +182 -182
- package/dist/layout/Chat/RichText.svelte +216 -216
- package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
- package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
- package/dist/layout/TF/Content/Content.svelte +21 -21
- package/dist/layout/TF/Header/Header.svelte +166 -166
- package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
- package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
- package/dist/layout/mailing/MailPaginator.svelte +36 -36
- package/dist/layout/mailing/MailSidebar.svelte +39 -39
- package/dist/layout/mailing/MailToolBar.svelte +174 -174
- package/dist/layout/mailing/MailingContent.svelte +10 -10
- package/dist/layout/mailing/MailingHeader.svelte +55 -55
- package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
- package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
- package/dist/layout/mailing/MailingModule.svelte +448 -448
- package/dist/styles/docs.css +615 -615
- package/dist/styles/tf-layout.css +185 -185
- package/dist/themes/ThemeProvider.svelte +20 -20
- package/dist/themes/themes.d.ts +3 -0
- package/dist/themes/themes.js +3 -0
- package/dist/types/index.d.ts +57 -1
- package/dist/typography/heading/Heading.svelte +35 -35
- package/dist/ui/accordion/Accordion.svelte +49 -49
- package/dist/ui/accordion/AccordionItem.svelte +173 -173
- package/dist/ui/alert/Alert.svelte +83 -83
- package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
- package/dist/ui/avatar/Avatar.svelte +77 -77
- package/dist/ui/box/Box.svelte +28 -28
- package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
- package/dist/ui/buttons/ActionButton.svelte +234 -234
- package/dist/ui/buttons/Button.svelte +102 -102
- package/dist/ui/buttons/GradientButton.svelte +59 -59
- package/dist/ui/datatable/Datatable.svelte +525 -525
- package/dist/ui/drawer/Drawer.svelte +300 -300
- package/dist/ui/dropdown/Dropdown.svelte +36 -36
- package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
- package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
- package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
- package/dist/ui/dropdown/DropdownItem.svelte +52 -52
- package/dist/ui/footer/Footer.svelte +15 -15
- package/dist/ui/footer/FooterBrand.svelte +37 -37
- package/dist/ui/footer/FooterCopyright.svelte +45 -45
- package/dist/ui/footer/FooterIcon.svelte +22 -22
- package/dist/ui/footer/FooterLink.svelte +33 -33
- package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
- package/dist/ui/icons/IconifyIcon.svelte +7 -7
- package/dist/ui/indicator/Indicator.svelte +42 -42
- package/dist/ui/modal/Modal.svelte +265 -265
- package/dist/ui/modal/theme.d.ts +26 -26
- package/dist/ui/modal/theme.js +25 -25
- package/dist/ui/notificationList/NotificationList.svelte +123 -123
- package/dist/ui/pageLoader/PageLoader.svelte +14 -14
- package/dist/ui/paginate/Paginate.svelte +96 -96
- package/dist/ui/speedDial/SpeedDial.svelte +77 -0
- package/dist/ui/speedDial/SpeedDial.svelte.d.ts +21 -0
- package/dist/ui/speedDial/SpeedDialButton.svelte +75 -0
- package/dist/ui/speedDial/SpeedDialButton.svelte.d.ts +20 -0
- package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -0
- package/dist/ui/speedDial/SpeedDialTrigger.svelte.d.ts +18 -0
- package/dist/ui/speedDial/index.d.ts +4 -0
- package/dist/ui/speedDial/index.js +4 -0
- package/dist/ui/speedDial/theme.d.ts +75 -0
- package/dist/ui/speedDial/theme.js +35 -0
- package/dist/ui/tab/Tab.svelte +67 -67
- package/dist/ui/table/Table.svelte +396 -396
- package/dist/ui/tableLoader/TableLoader.svelte +24 -24
- package/dist/ui/toast/Toast.svelte +337 -337
- package/dist/ui/toast/Toast.svelte.d.ts +10 -10
- package/dist/ui/toast/index.d.ts +1 -2
- package/dist/ui/toast/index.js +3 -1
- package/dist/ui/toolbar/Toolbar.svelte +59 -59
- package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
- package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
- package/dist/ui/tooltip/Tooltip.svelte +51 -51
- package/dist/utils/Popper.svelte +257 -257
- package/dist/utils/closeButton/CloseButton.svelte +88 -88
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.js +13 -3
- package/dist/utils/singleSelection.svelte.js +48 -48
- package/dist/youtube/index.svelte +12 -12
- package/package.json +2 -1
|
@@ -1,641 +1,641 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
function getDefaultValue(type: FilterBuilderColumnConfig['type']): any {
|
|
3
|
-
switch (type) {
|
|
4
|
-
case 'string':
|
|
5
|
-
return '';
|
|
6
|
-
case 'number':
|
|
7
|
-
return null;
|
|
8
|
-
case 'date':
|
|
9
|
-
return '';
|
|
10
|
-
case 'boolean':
|
|
11
|
-
return true;
|
|
12
|
-
default:
|
|
13
|
-
return '';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isFilterValid(filter: FilterBuilderItem): boolean {
|
|
18
|
-
return !!(filter.column && filter.operator && filter.value !== null && filter.value !== '');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getUniqueFilters(...filterArrays: FilterBuilderItem[][]): FilterBuilderItem[] {
|
|
22
|
-
const combinedFilters = filterArrays.flat();
|
|
23
|
-
const uniqueFilters: FilterBuilderItem[] = [];
|
|
24
|
-
const seen = new Set<string>();
|
|
25
|
-
|
|
26
|
-
combinedFilters.forEach((filter) => {
|
|
27
|
-
// Create a unique identifier based on all filter properties
|
|
28
|
-
const identifier = `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
|
|
29
|
-
|
|
30
|
-
if (!seen.has(identifier)) {
|
|
31
|
-
seen.add(identifier);
|
|
32
|
-
uniqueFilters.push(filter);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
return uniqueFilters;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Alternative version with configurable uniqueness criteria
|
|
40
|
-
function getUniqueFiltersAdvanced(
|
|
41
|
-
filterArrays: FilterBuilderItem[][],
|
|
42
|
-
uniquenessKey: (filter: FilterBuilderItem) => string = defaultUniquenessKey
|
|
43
|
-
): FilterBuilderItem[] {
|
|
44
|
-
const combinedFilters = filterArrays.flat();
|
|
45
|
-
const uniqueFilters: FilterBuilderItem[] = [];
|
|
46
|
-
const seen = new Set<string>();
|
|
47
|
-
|
|
48
|
-
combinedFilters.forEach((filter) => {
|
|
49
|
-
const key = uniquenessKey(filter);
|
|
50
|
-
|
|
51
|
-
if (!seen.has(key)) {
|
|
52
|
-
seen.add(key);
|
|
53
|
-
uniqueFilters.push(filter);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return uniqueFilters;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Default uniqueness key function
|
|
61
|
-
function defaultUniquenessKey(filter: FilterBuilderItem): string {
|
|
62
|
-
return `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
|
|
63
|
-
}
|
|
64
|
-
</script>
|
|
65
|
-
|
|
66
|
-
<script lang="ts">
|
|
67
|
-
import { browser } from '$app/environment';
|
|
68
|
-
|
|
69
|
-
import {
|
|
70
|
-
Button,
|
|
71
|
-
Datepicker,
|
|
72
|
-
Input,
|
|
73
|
-
Label,
|
|
74
|
-
Select,
|
|
75
|
-
type FilterBuilderColumnConfig,
|
|
76
|
-
type FilterBuilderItem,
|
|
77
|
-
type FilterBuilderProps
|
|
78
|
-
} from '../../index.js';
|
|
79
|
-
import { nanoid } from 'nanoid';
|
|
80
|
-
import { onMount } from 'svelte';
|
|
81
|
-
import { fade } from 'svelte/transition';
|
|
82
|
-
|
|
83
|
-
let { columns = [], filters = $bindable([]), onRunQuery }: FilterBuilderProps = $props();
|
|
84
|
-
|
|
85
|
-
// Operator mappings for each type
|
|
86
|
-
const operatorsByType = {
|
|
87
|
-
string: [
|
|
88
|
-
{ value: 'contains', label: 'contains', sqlOp: 'ILIKE' },
|
|
89
|
-
{ value: '=', label: 'equals', sqlOp: '=' },
|
|
90
|
-
{ value: '!=', label: 'does not equal', sqlOp: '!=' },
|
|
91
|
-
{ value: 'starts_with', label: 'starts with', sqlOp: 'ILIKE' }
|
|
92
|
-
],
|
|
93
|
-
number: [
|
|
94
|
-
{ value: '=', label: 'equals', sqlOp: '=' },
|
|
95
|
-
{ value: '>', label: 'is greater than', sqlOp: '>' },
|
|
96
|
-
{ value: '<', label: 'is less than', sqlOp: '<' },
|
|
97
|
-
{ value: '>=', label: 'is at least', sqlOp: '>=' },
|
|
98
|
-
{ value: '<=', label: 'is at most', sqlOp: '<=' },
|
|
99
|
-
{ value: '!=', label: 'does not equal', sqlOp: '!=' }
|
|
100
|
-
],
|
|
101
|
-
date: [
|
|
102
|
-
{ value: '=', label: 'is on', sqlOp: '=' },
|
|
103
|
-
{ value: '>', label: 'is after', sqlOp: '>' },
|
|
104
|
-
{ value: '<', label: 'is before', sqlOp: '<' },
|
|
105
|
-
{ value: '>=', label: 'is on or after', sqlOp: '>=' },
|
|
106
|
-
{ value: '<=', label: 'is on or before', sqlOp: '<=' },
|
|
107
|
-
{ value: '!=', label: 'is not on', sqlOp: '!=' }
|
|
108
|
-
],
|
|
109
|
-
boolean: [
|
|
110
|
-
{ value: '=', label: 'is', sqlOp: '=' },
|
|
111
|
-
{ value: '!=', label: 'is not', sqlOp: '!=' }
|
|
112
|
-
]
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
let activeTab = $state('SQL');
|
|
116
|
-
let completedFilters = $state<FilterBuilderItem[]>([]);
|
|
117
|
-
|
|
118
|
-
function addFilter() {
|
|
119
|
-
const newFilter: FilterBuilderItem = {
|
|
120
|
-
id: nanoid(),
|
|
121
|
-
column: columns[0]?.name || '',
|
|
122
|
-
operator: operatorsByType[columns[0]?.type || 'string'][0].value,
|
|
123
|
-
value: getDefaultValue(columns[0]?.type || 'string'),
|
|
124
|
-
logicalOperator: completedFilters.length > 0 ? 'AND' : undefined
|
|
125
|
-
// label: getColumnLabel(columns[0]?.name || '')
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
filters = [...filters, newFilter];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function clearAllFilters() {
|
|
132
|
-
filters = [];
|
|
133
|
-
completedFilters = [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getColumnType(columnName: string): FilterBuilderColumnConfig['type'] {
|
|
137
|
-
return columns.find((col) => col.name === columnName)?.type || 'string';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getColumnLabel(columnName: string): string {
|
|
141
|
-
return columns.find((col) => col.name === columnName)?.label || columnName;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function updateFilter(filterId: string, field: keyof FilterBuilderItem, value: any) {
|
|
145
|
-
const filterIndex = filters.findIndex((f) => f.id === filterId);
|
|
146
|
-
if (filterIndex === -1) return;
|
|
147
|
-
|
|
148
|
-
const updatedFilter = { ...filters[filterIndex], [field]: value };
|
|
149
|
-
|
|
150
|
-
// Reset operator and value when column changes
|
|
151
|
-
if (field === 'column') {
|
|
152
|
-
const columnType = getColumnType(value);
|
|
153
|
-
updatedFilter.operator = operatorsByType[columnType][0].value;
|
|
154
|
-
updatedFilter.value = getDefaultValue(columnType);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
filters[filterIndex] = updatedFilter;
|
|
158
|
-
filters = [...filters];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function completeFilter(filterId: string) {
|
|
162
|
-
const filter = filters.find((f) => f.id === filterId);
|
|
163
|
-
if (filter && isFilterValid(filter) && !completedFilters.some((f) => f.id === filterId)) {
|
|
164
|
-
completedFilters = [...completedFilters, filter];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// console.log('Completed Filters:', completedFilters);
|
|
168
|
-
|
|
169
|
-
removeFilter(filterId);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function removeFilter(filterId: string) {
|
|
173
|
-
const filterIndex = filters.findIndex((f) => f.id === filterId);
|
|
174
|
-
if (filterIndex === -1) return;
|
|
175
|
-
|
|
176
|
-
// If removing the first filter and there's a second one, remove the logical operator from the second
|
|
177
|
-
if (filterIndex === 0 && filters.length > 1) {
|
|
178
|
-
filters[1].logicalOperator = undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
filters = filters.filter((f) => f.id !== filterId);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function removeCompleteFilter(filterId: string) {
|
|
185
|
-
completedFilters = completedFilters.filter((f) => f.id !== filterId);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function toggleLogicalOperator(filterId: string) {
|
|
189
|
-
const filter = filters.find((f) => f.id === filterId);
|
|
190
|
-
if (filter && filter.logicalOperator) {
|
|
191
|
-
filter.logicalOperator = filter.logicalOperator === 'AND' ? 'OR' : 'AND';
|
|
192
|
-
filters = [...filters];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function generateSQLQuery(): string {
|
|
197
|
-
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
198
|
-
const validFilterList = uniqueFilters.filter(isFilterValid);
|
|
199
|
-
|
|
200
|
-
if (validFilterList.length === 0) return '';
|
|
201
|
-
|
|
202
|
-
const conditions = validFilterList.map((filter, index) => {
|
|
203
|
-
let value = filter.value;
|
|
204
|
-
const columnType = getColumnType(filter.column);
|
|
205
|
-
const operatorConfig = operatorsByType[columnType].find((op) => op.value === filter.operator);
|
|
206
|
-
const sqlOperator = operatorConfig?.sqlOp || filter.operator;
|
|
207
|
-
|
|
208
|
-
// Handle string values based on operator type
|
|
209
|
-
if (typeof value === 'string') {
|
|
210
|
-
if (filter.operator === 'contains') {
|
|
211
|
-
value = `'%${value}%'`;
|
|
212
|
-
} else if (filter.operator === 'starts_with') {
|
|
213
|
-
value = `'${value}%'`;
|
|
214
|
-
} else {
|
|
215
|
-
value = `'${value}'`;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const condition = `${filter.column} ${sqlOperator} ${value}`;
|
|
220
|
-
|
|
221
|
-
if (index === 0) {
|
|
222
|
-
return condition;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return `${filter.logicalOperator} ${condition}`;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
return `WHERE ${conditions.join(' ')}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function generateJSONQuery(): any[] {
|
|
232
|
-
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
233
|
-
return uniqueFilters.filter(isFilterValid).map((filter) => ({
|
|
234
|
-
column: filter.column,
|
|
235
|
-
operator: filter.operator,
|
|
236
|
-
value: filter.value,
|
|
237
|
-
...(filter.logicalOperator && { logicalOperator: filter.logicalOperator })
|
|
238
|
-
}));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getLogicalOperatorCounts() {
|
|
242
|
-
const counts = { OR: 0, AND: 0 };
|
|
243
|
-
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
244
|
-
|
|
245
|
-
uniqueFilters.forEach((filter) => {
|
|
246
|
-
if (filter.logicalOperator) {
|
|
247
|
-
counts[filter.logicalOperator]++;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
return counts;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function getColumnTypeCounts() {
|
|
254
|
-
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
255
|
-
|
|
256
|
-
const usedTypes = new Set(
|
|
257
|
-
uniqueFilters.filter(isFilterValid).map((filter) => getColumnType(filter.column))
|
|
258
|
-
);
|
|
259
|
-
return usedTypes.size;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function loadValidFilters() {
|
|
263
|
-
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
264
|
-
const validFilterList = uniqueFilters.filter(isFilterValid);
|
|
265
|
-
|
|
266
|
-
return validFilterList;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function buildDecisionTree() {
|
|
270
|
-
if (completedFilters.length === 0) return null;
|
|
271
|
-
|
|
272
|
-
const tree = {
|
|
273
|
-
type: 'root',
|
|
274
|
-
children: []
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
let currentNode = tree;
|
|
278
|
-
|
|
279
|
-
completedFilters.forEach((filter, index) => {
|
|
280
|
-
const filterNode = {
|
|
281
|
-
type: 'condition',
|
|
282
|
-
filter: filter,
|
|
283
|
-
children: []
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
if (index === 0) {
|
|
287
|
-
currentNode.children.push(filterNode);
|
|
288
|
-
} else {
|
|
289
|
-
const operatorNode = {
|
|
290
|
-
type: 'operator',
|
|
291
|
-
operator: filter.logicalOperator,
|
|
292
|
-
children: [filterNode]
|
|
293
|
-
};
|
|
294
|
-
currentNode.children.push(operatorNode);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
return tree;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function renderDecisionNode(node, level = 0) {
|
|
302
|
-
if (!node) return '';
|
|
303
|
-
|
|
304
|
-
const indent = level * 40;
|
|
305
|
-
|
|
306
|
-
if (node.type === 'condition') {
|
|
307
|
-
return `
|
|
308
|
-
|
|
309
|
-
<div class="relative flex items-center">
|
|
310
|
-
<div class="relative">
|
|
311
|
-
<div class="flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-lg text-sm gap-0.5 w-fit h-fit">
|
|
312
|
-
${node.filter.column}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.operator}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.value}
|
|
313
|
-
</div>
|
|
314
|
-
<div class="absolute right-1 top-1">
|
|
315
|
-
<button onclick="removeCompleteFilter('${node.filter.id}')" class="p-1 text-gray-400 hover:text-red-500">
|
|
316
|
-
<iconify-icon class="absolute -top-2 -right-2 text-gray-400 hover:text-red-500" icon="ic:round-close"></iconify-icon>
|
|
317
|
-
</button>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
<div class="w-5 h-0.5 bg-gray-300"></div>
|
|
321
|
-
</div>
|
|
322
|
-
`;
|
|
323
|
-
} else if (node.type === 'operator') {
|
|
324
|
-
const operatorColor =
|
|
325
|
-
node.operator === 'AND' ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800';
|
|
326
|
-
return `
|
|
327
|
-
<div class="flex flex-wrap items-center h-fit">
|
|
328
|
-
<div class="flex items-center justify-center ${operatorColor} px-2 py-1 rounded text-xs font-semibold mb-2 w-fit">
|
|
329
|
-
${node.operator}
|
|
330
|
-
</div>
|
|
331
|
-
<div class="w-5 h-0.5 bg-gray-300"></div>
|
|
332
|
-
${node.children.map((child) => renderDecisionNode(child, level + 1)).join('')}
|
|
333
|
-
</div>
|
|
334
|
-
`;
|
|
335
|
-
} else {
|
|
336
|
-
return node.children.map((child) => renderDecisionNode(child, level)).join('');
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Generate SQL WHERE clause
|
|
341
|
-
let sqlQuery = $derived(generateSQLQuery());
|
|
342
|
-
let jsonQuery = $derived(generateJSONQuery());
|
|
343
|
-
let validFilters = $derived(loadValidFilters());
|
|
344
|
-
let logicalOperatorCounts = $derived(getLogicalOperatorCounts());
|
|
345
|
-
let columnTypeCounts = $derived(getColumnTypeCounts());
|
|
346
|
-
let decisionTree = $derived(buildDecisionTree());
|
|
347
|
-
|
|
348
|
-
onMount(() => {
|
|
349
|
-
if (browser) {
|
|
350
|
-
(window as any).removeCompleteFilter = removeCompleteFilter;
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
</script>
|
|
354
|
-
|
|
355
|
-
<div class="flex h-screen bg-gray-50">
|
|
356
|
-
<!-- Left Panel - Filter Builder -->
|
|
357
|
-
<div class="flex w-3/5 flex-col border-r border-gray-200 bg-white">
|
|
358
|
-
<!-- Header -->
|
|
359
|
-
<div class="border-b border-gray-200 p-6">
|
|
360
|
-
<h2 class="mb-2 text-lg font-semibold text-gray-900">Build Filters</h2>
|
|
361
|
-
<p class="text-sm text-gray-600">Add conditions to filter your data</p>
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<!-- Filter Controls -->
|
|
365
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
366
|
-
<div class="mb-6 flex items-center justify-between">
|
|
367
|
-
<Button onclick={addFilter}>
|
|
368
|
-
{#snippet leadingIcon()}
|
|
369
|
-
<iconify-icon class="me-1.5" style="font-size: 20px;" icon="ei:plus"></iconify-icon>
|
|
370
|
-
{/snippet}
|
|
371
|
-
Add Filter
|
|
372
|
-
</Button>
|
|
373
|
-
|
|
374
|
-
<Button color="alternative" onclick={clearAllFilters}>
|
|
375
|
-
{#snippet leadingIcon()}
|
|
376
|
-
<iconify-icon class="me-1.5" icon="mi:delete"></iconify-icon>
|
|
377
|
-
{/snippet}
|
|
378
|
-
Clear All
|
|
379
|
-
</Button>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
<!-- Filters List -->
|
|
383
|
-
<div class="space-y-4">
|
|
384
|
-
{#each filters as filter, index}
|
|
385
|
-
<div class="relative rounded-lg bg-gray-50 p-4">
|
|
386
|
-
<!-- Logical Operator (for filters after the first) -->
|
|
387
|
-
{#if completedFilters.length > 0}
|
|
388
|
-
<div class="mt-4 mb-2 flex justify-center">
|
|
389
|
-
<div class="flex overflow-hidden rounded-md border border-gray-300 bg-white">
|
|
390
|
-
<button
|
|
391
|
-
onclick={() => toggleLogicalOperator(filter.id)}
|
|
392
|
-
class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
|
|
393
|
-
'AND'
|
|
394
|
-
? 'bg-blue-600 text-white'
|
|
395
|
-
: 'text-gray-600 hover:bg-gray-50'}"
|
|
396
|
-
>
|
|
397
|
-
AND
|
|
398
|
-
</button>
|
|
399
|
-
<button
|
|
400
|
-
onclick={() => toggleLogicalOperator(filter.id)}
|
|
401
|
-
class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
|
|
402
|
-
'OR'
|
|
403
|
-
? 'bg-orange-500 text-white'
|
|
404
|
-
: 'text-gray-600 hover:bg-gray-50'}"
|
|
405
|
-
>
|
|
406
|
-
OR
|
|
407
|
-
</button>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
{/if}
|
|
411
|
-
|
|
412
|
-
<!-- Column, Operator, Value Row -->
|
|
413
|
-
<div class="mb-3 grid grid-cols-3 gap-3">
|
|
414
|
-
<div>
|
|
415
|
-
<Label class="mb-2 block text-xs font-medium text-gray-700">Column</Label>
|
|
416
|
-
|
|
417
|
-
<Select
|
|
418
|
-
clearable={false}
|
|
419
|
-
minHeight={40}
|
|
420
|
-
options={columns}
|
|
421
|
-
onChange={(e) => {
|
|
422
|
-
updateFilter(filter.id, 'column', e.name);
|
|
423
|
-
}}
|
|
424
|
-
/>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
<div>
|
|
428
|
-
<Label class="mb-2 block text-xs font-medium text-gray-700">Operator</Label>
|
|
429
|
-
|
|
430
|
-
<Select
|
|
431
|
-
clearable={false}
|
|
432
|
-
minHeight={40}
|
|
433
|
-
onChange={(e) => {
|
|
434
|
-
updateFilter(filter.id, 'operator', e.value);
|
|
435
|
-
}}
|
|
436
|
-
options={operatorsByType[getColumnType(filter.column)]}
|
|
437
|
-
/>
|
|
438
|
-
</div>
|
|
439
|
-
|
|
440
|
-
<div>
|
|
441
|
-
<Label class="mb-2 block text-xs font-medium text-gray-700">Value</Label>
|
|
442
|
-
|
|
443
|
-
{#if getColumnType(filter.column) === 'string'}
|
|
444
|
-
<Input
|
|
445
|
-
type="text"
|
|
446
|
-
oninput={(e) => {
|
|
447
|
-
updateFilter(filter.id, 'value', e.target.value);
|
|
448
|
-
}}
|
|
449
|
-
/>
|
|
450
|
-
{:else if getColumnType(filter.column) === 'number'}
|
|
451
|
-
<Input
|
|
452
|
-
type="number"
|
|
453
|
-
oninput={(e) =>
|
|
454
|
-
updateFilter(filter.id, 'value', parseFloat(e.target.value) || 0)}
|
|
455
|
-
/>
|
|
456
|
-
{:else if getColumnType(filter.column) === 'date'}
|
|
457
|
-
<Datepicker
|
|
458
|
-
onselect={(e) => {
|
|
459
|
-
updateFilter(filter.id, 'value', e?.toLocaleString());
|
|
460
|
-
}}
|
|
461
|
-
/>
|
|
462
|
-
{:else if getColumnType(filter.column) === 'boolean'}
|
|
463
|
-
<Select
|
|
464
|
-
clearable={false}
|
|
465
|
-
minHeight={40}
|
|
466
|
-
onChange={(e) => updateFilter(filter.id, 'value', e.value)}
|
|
467
|
-
options={[
|
|
468
|
-
{ value: true, label: 'True' },
|
|
469
|
-
{ value: false, label: 'False' }
|
|
470
|
-
]}
|
|
471
|
-
/>
|
|
472
|
-
{/if}
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
|
|
476
|
-
<!-- Filter Validation -->
|
|
477
|
-
{#if isFilterValid(filter)}
|
|
478
|
-
<div transition:fade class="flex justify-between">
|
|
479
|
-
<div class="flex items-center text-xs text-green-600">
|
|
480
|
-
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
|
481
|
-
<path
|
|
482
|
-
fill-rule="evenodd"
|
|
483
|
-
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
484
|
-
clip-rule="evenodd"
|
|
485
|
-
></path>
|
|
486
|
-
</svg>
|
|
487
|
-
Filter is valid
|
|
488
|
-
</div>
|
|
489
|
-
<Button color="green" size="xs" onclick={() => completeFilter(filter.id)}
|
|
490
|
-
>Done</Button
|
|
491
|
-
>
|
|
492
|
-
</div>
|
|
493
|
-
{/if}
|
|
494
|
-
|
|
495
|
-
<!-- Remove Button -->
|
|
496
|
-
<button
|
|
497
|
-
aria-label="removeFilter"
|
|
498
|
-
onclick={() => removeFilter(filter.id)}
|
|
499
|
-
class="absolute top-2 right-2 p-1 text-gray-400 transition-colors hover:text-red-500"
|
|
500
|
-
>
|
|
501
|
-
<iconify-icon icon="ic:round-close"></iconify-icon>
|
|
502
|
-
</button>
|
|
503
|
-
</div>
|
|
504
|
-
{/each}
|
|
505
|
-
</div>
|
|
506
|
-
{#if decisionTree}
|
|
507
|
-
<div class="space-y-4">
|
|
508
|
-
<div class="rounded-lg bg-green-50 p-4">
|
|
509
|
-
<div class="rounded-lg border bg-white p-4">
|
|
510
|
-
{#if decisionTree}
|
|
511
|
-
<div class="flex flex-wrap space-y-3">
|
|
512
|
-
{@html renderDecisionNode(decisionTree)}
|
|
513
|
-
</div>
|
|
514
|
-
{:else}
|
|
515
|
-
<div class="flex h-32 items-center justify-center text-gray-500">
|
|
516
|
-
<p class="text-sm">No decision tree to display</p>
|
|
517
|
-
</div>
|
|
518
|
-
{/if}
|
|
519
|
-
</div>
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
{/if}
|
|
523
|
-
</div>
|
|
524
|
-
</div>
|
|
525
|
-
|
|
526
|
-
<!-- Right Panel - Generated Query -->
|
|
527
|
-
<div class="flex w-2/5 flex-col bg-white">
|
|
528
|
-
<!-- Header -->
|
|
529
|
-
<div class="flex justify-between border-b border-gray-200 p-6">
|
|
530
|
-
<div class="flex flex-col">
|
|
531
|
-
<div class="mb-2 flex items-center justify-between">
|
|
532
|
-
<div class="flex items-center gap-2">
|
|
533
|
-
<h2 class="text-lg font-semibold text-gray-900">Generated Query</h2>
|
|
534
|
-
<div class="flex items-center text-sm text-gray-600">
|
|
535
|
-
<span
|
|
536
|
-
class="mr-2 inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
|
|
537
|
-
>
|
|
538
|
-
{validFilters.length} active filters
|
|
539
|
-
</span>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
</div>
|
|
543
|
-
<p class="text-sm text-gray-600">Live preview of your filter conditions</p>
|
|
544
|
-
</div>
|
|
545
|
-
<div class="flex items-center">
|
|
546
|
-
<Button
|
|
547
|
-
color="blue"
|
|
548
|
-
onclick={() => onRunQuery({ jsonBuild: jsonQuery, sqlBuild: sqlQuery })}>Run Query</Button
|
|
549
|
-
>
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
|
|
553
|
-
<!-- Tabs -->
|
|
554
|
-
<div class="border-b border-gray-200">
|
|
555
|
-
<div class="flex">
|
|
556
|
-
<button
|
|
557
|
-
onclick={() => (activeTab = 'SQL')}
|
|
558
|
-
class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'SQL'
|
|
559
|
-
? 'border-blue-500 text-blue-600'
|
|
560
|
-
: 'border-transparent text-gray-600 hover:text-gray-800'}"
|
|
561
|
-
>
|
|
562
|
-
SQL
|
|
563
|
-
</button>
|
|
564
|
-
<button
|
|
565
|
-
onclick={() => (activeTab = 'JSON')}
|
|
566
|
-
class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'JSON'
|
|
567
|
-
? 'border-blue-500 text-blue-600'
|
|
568
|
-
: 'border-transparent text-gray-600 hover:text-gray-800'}"
|
|
569
|
-
>
|
|
570
|
-
JSON
|
|
571
|
-
</button>
|
|
572
|
-
</div>
|
|
573
|
-
</div>
|
|
574
|
-
|
|
575
|
-
<!-- Query Content -->
|
|
576
|
-
<div class="flex-1 overflow-y-auto p-6">
|
|
577
|
-
{#if validFilters.length === 0}
|
|
578
|
-
<div class="flex h-32 items-center justify-center text-gray-500">
|
|
579
|
-
<div class="text-center">
|
|
580
|
-
<svg
|
|
581
|
-
class="mx-auto mb-2 h-8 w-8 opacity-50"
|
|
582
|
-
fill="none"
|
|
583
|
-
stroke="currentColor"
|
|
584
|
-
viewBox="0 0 24 24"
|
|
585
|
-
>
|
|
586
|
-
<path
|
|
587
|
-
stroke-linecap="round"
|
|
588
|
-
stroke-linejoin="round"
|
|
589
|
-
stroke-width="2"
|
|
590
|
-
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
|
591
|
-
></path>
|
|
592
|
-
</svg>
|
|
593
|
-
<p class="text-sm">No active filters</p>
|
|
594
|
-
</div>
|
|
595
|
-
</div>
|
|
596
|
-
{:else if activeTab === 'SQL'}
|
|
597
|
-
<div class="space-y-4">
|
|
598
|
-
<div class="rounded-lg bg-gray-50 p-4">
|
|
599
|
-
<h3 class="mb-2 text-sm font-medium text-gray-900">SQL WHERE Clause</h3>
|
|
600
|
-
<div class="rounded border bg-white p-3 font-mono text-sm">
|
|
601
|
-
<span class="text-blue-600">WHERE</span>
|
|
602
|
-
{sqlQuery.replace('WHERE ', '')}
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
</div>
|
|
606
|
-
{:else}
|
|
607
|
-
<div class="space-y-4">
|
|
608
|
-
<div class="rounded-lg bg-gray-50 p-4">
|
|
609
|
-
<h3 class="mb-2 text-sm font-medium text-gray-900">Filter Objects</h3>
|
|
610
|
-
<pre
|
|
611
|
-
class="overflow-x-auto rounded border bg-white p-3 text-xs text-gray-800">{JSON.stringify(
|
|
612
|
-
jsonQuery,
|
|
613
|
-
null,
|
|
614
|
-
2
|
|
615
|
-
)}</pre>
|
|
616
|
-
</div>
|
|
617
|
-
</div>
|
|
618
|
-
{/if}
|
|
619
|
-
|
|
620
|
-
<!-- Statistics -->
|
|
621
|
-
<div class="mt-6 grid grid-cols-2 gap-4">
|
|
622
|
-
<div class="rounded-lg bg-blue-50 p-4">
|
|
623
|
-
<h4 class="mb-2 text-sm font-medium text-blue-900">Logical Operators</h4>
|
|
624
|
-
<div class="space-y-1">
|
|
625
|
-
<div class="text-xl font-bold text-blue-600">{logicalOperatorCounts.OR}</div>
|
|
626
|
-
<div class="text-xs text-blue-800">{logicalOperatorCounts.OR} OR</div>
|
|
627
|
-
<div class="text-xs text-blue-800">{logicalOperatorCounts.AND} AND</div>
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
|
|
631
|
-
<div class="rounded-lg bg-green-50 p-4">
|
|
632
|
-
<h4 class="mb-2 text-sm font-medium text-green-900">Column Types</h4>
|
|
633
|
-
<div class="space-y-1">
|
|
634
|
-
<div class="text-xl font-bold text-green-600">{columnTypeCounts}</div>
|
|
635
|
-
<div class="text-xs text-green-800">types used</div>
|
|
636
|
-
</div>
|
|
637
|
-
</div>
|
|
638
|
-
</div>
|
|
639
|
-
</div>
|
|
640
|
-
</div>
|
|
641
|
-
</div>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
function getDefaultValue(type: FilterBuilderColumnConfig['type']): any {
|
|
3
|
+
switch (type) {
|
|
4
|
+
case 'string':
|
|
5
|
+
return '';
|
|
6
|
+
case 'number':
|
|
7
|
+
return null;
|
|
8
|
+
case 'date':
|
|
9
|
+
return '';
|
|
10
|
+
case 'boolean':
|
|
11
|
+
return true;
|
|
12
|
+
default:
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isFilterValid(filter: FilterBuilderItem): boolean {
|
|
18
|
+
return !!(filter.column && filter.operator && filter.value !== null && filter.value !== '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getUniqueFilters(...filterArrays: FilterBuilderItem[][]): FilterBuilderItem[] {
|
|
22
|
+
const combinedFilters = filterArrays.flat();
|
|
23
|
+
const uniqueFilters: FilterBuilderItem[] = [];
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
|
|
26
|
+
combinedFilters.forEach((filter) => {
|
|
27
|
+
// Create a unique identifier based on all filter properties
|
|
28
|
+
const identifier = `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
|
|
29
|
+
|
|
30
|
+
if (!seen.has(identifier)) {
|
|
31
|
+
seen.add(identifier);
|
|
32
|
+
uniqueFilters.push(filter);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return uniqueFilters;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Alternative version with configurable uniqueness criteria
|
|
40
|
+
function getUniqueFiltersAdvanced(
|
|
41
|
+
filterArrays: FilterBuilderItem[][],
|
|
42
|
+
uniquenessKey: (filter: FilterBuilderItem) => string = defaultUniquenessKey
|
|
43
|
+
): FilterBuilderItem[] {
|
|
44
|
+
const combinedFilters = filterArrays.flat();
|
|
45
|
+
const uniqueFilters: FilterBuilderItem[] = [];
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
|
|
48
|
+
combinedFilters.forEach((filter) => {
|
|
49
|
+
const key = uniquenessKey(filter);
|
|
50
|
+
|
|
51
|
+
if (!seen.has(key)) {
|
|
52
|
+
seen.add(key);
|
|
53
|
+
uniqueFilters.push(filter);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return uniqueFilters;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Default uniqueness key function
|
|
61
|
+
function defaultUniquenessKey(filter: FilterBuilderItem): string {
|
|
62
|
+
return `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<script lang="ts">
|
|
67
|
+
import { browser } from '$app/environment';
|
|
68
|
+
|
|
69
|
+
import {
|
|
70
|
+
Button,
|
|
71
|
+
Datepicker,
|
|
72
|
+
Input,
|
|
73
|
+
Label,
|
|
74
|
+
Select,
|
|
75
|
+
type FilterBuilderColumnConfig,
|
|
76
|
+
type FilterBuilderItem,
|
|
77
|
+
type FilterBuilderProps
|
|
78
|
+
} from '../../index.js';
|
|
79
|
+
import { nanoid } from 'nanoid';
|
|
80
|
+
import { onMount } from 'svelte';
|
|
81
|
+
import { fade } from 'svelte/transition';
|
|
82
|
+
|
|
83
|
+
let { columns = [], filters = $bindable([]), onRunQuery }: FilterBuilderProps = $props();
|
|
84
|
+
|
|
85
|
+
// Operator mappings for each type
|
|
86
|
+
const operatorsByType = {
|
|
87
|
+
string: [
|
|
88
|
+
{ value: 'contains', label: 'contains', sqlOp: 'ILIKE' },
|
|
89
|
+
{ value: '=', label: 'equals', sqlOp: '=' },
|
|
90
|
+
{ value: '!=', label: 'does not equal', sqlOp: '!=' },
|
|
91
|
+
{ value: 'starts_with', label: 'starts with', sqlOp: 'ILIKE' }
|
|
92
|
+
],
|
|
93
|
+
number: [
|
|
94
|
+
{ value: '=', label: 'equals', sqlOp: '=' },
|
|
95
|
+
{ value: '>', label: 'is greater than', sqlOp: '>' },
|
|
96
|
+
{ value: '<', label: 'is less than', sqlOp: '<' },
|
|
97
|
+
{ value: '>=', label: 'is at least', sqlOp: '>=' },
|
|
98
|
+
{ value: '<=', label: 'is at most', sqlOp: '<=' },
|
|
99
|
+
{ value: '!=', label: 'does not equal', sqlOp: '!=' }
|
|
100
|
+
],
|
|
101
|
+
date: [
|
|
102
|
+
{ value: '=', label: 'is on', sqlOp: '=' },
|
|
103
|
+
{ value: '>', label: 'is after', sqlOp: '>' },
|
|
104
|
+
{ value: '<', label: 'is before', sqlOp: '<' },
|
|
105
|
+
{ value: '>=', label: 'is on or after', sqlOp: '>=' },
|
|
106
|
+
{ value: '<=', label: 'is on or before', sqlOp: '<=' },
|
|
107
|
+
{ value: '!=', label: 'is not on', sqlOp: '!=' }
|
|
108
|
+
],
|
|
109
|
+
boolean: [
|
|
110
|
+
{ value: '=', label: 'is', sqlOp: '=' },
|
|
111
|
+
{ value: '!=', label: 'is not', sqlOp: '!=' }
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let activeTab = $state('SQL');
|
|
116
|
+
let completedFilters = $state<FilterBuilderItem[]>([]);
|
|
117
|
+
|
|
118
|
+
function addFilter() {
|
|
119
|
+
const newFilter: FilterBuilderItem = {
|
|
120
|
+
id: nanoid(),
|
|
121
|
+
column: columns[0]?.name || '',
|
|
122
|
+
operator: operatorsByType[columns[0]?.type || 'string'][0].value,
|
|
123
|
+
value: getDefaultValue(columns[0]?.type || 'string'),
|
|
124
|
+
logicalOperator: completedFilters.length > 0 ? 'AND' : undefined
|
|
125
|
+
// label: getColumnLabel(columns[0]?.name || '')
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
filters = [...filters, newFilter];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function clearAllFilters() {
|
|
132
|
+
filters = [];
|
|
133
|
+
completedFilters = [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getColumnType(columnName: string): FilterBuilderColumnConfig['type'] {
|
|
137
|
+
return columns.find((col) => col.name === columnName)?.type || 'string';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getColumnLabel(columnName: string): string {
|
|
141
|
+
return columns.find((col) => col.name === columnName)?.label || columnName;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function updateFilter(filterId: string, field: keyof FilterBuilderItem, value: any) {
|
|
145
|
+
const filterIndex = filters.findIndex((f) => f.id === filterId);
|
|
146
|
+
if (filterIndex === -1) return;
|
|
147
|
+
|
|
148
|
+
const updatedFilter = { ...filters[filterIndex], [field]: value };
|
|
149
|
+
|
|
150
|
+
// Reset operator and value when column changes
|
|
151
|
+
if (field === 'column') {
|
|
152
|
+
const columnType = getColumnType(value);
|
|
153
|
+
updatedFilter.operator = operatorsByType[columnType][0].value;
|
|
154
|
+
updatedFilter.value = getDefaultValue(columnType);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
filters[filterIndex] = updatedFilter;
|
|
158
|
+
filters = [...filters];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function completeFilter(filterId: string) {
|
|
162
|
+
const filter = filters.find((f) => f.id === filterId);
|
|
163
|
+
if (filter && isFilterValid(filter) && !completedFilters.some((f) => f.id === filterId)) {
|
|
164
|
+
completedFilters = [...completedFilters, filter];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// console.log('Completed Filters:', completedFilters);
|
|
168
|
+
|
|
169
|
+
removeFilter(filterId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function removeFilter(filterId: string) {
|
|
173
|
+
const filterIndex = filters.findIndex((f) => f.id === filterId);
|
|
174
|
+
if (filterIndex === -1) return;
|
|
175
|
+
|
|
176
|
+
// If removing the first filter and there's a second one, remove the logical operator from the second
|
|
177
|
+
if (filterIndex === 0 && filters.length > 1) {
|
|
178
|
+
filters[1].logicalOperator = undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
filters = filters.filter((f) => f.id !== filterId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function removeCompleteFilter(filterId: string) {
|
|
185
|
+
completedFilters = completedFilters.filter((f) => f.id !== filterId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toggleLogicalOperator(filterId: string) {
|
|
189
|
+
const filter = filters.find((f) => f.id === filterId);
|
|
190
|
+
if (filter && filter.logicalOperator) {
|
|
191
|
+
filter.logicalOperator = filter.logicalOperator === 'AND' ? 'OR' : 'AND';
|
|
192
|
+
filters = [...filters];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function generateSQLQuery(): string {
|
|
197
|
+
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
198
|
+
const validFilterList = uniqueFilters.filter(isFilterValid);
|
|
199
|
+
|
|
200
|
+
if (validFilterList.length === 0) return '';
|
|
201
|
+
|
|
202
|
+
const conditions = validFilterList.map((filter, index) => {
|
|
203
|
+
let value = filter.value;
|
|
204
|
+
const columnType = getColumnType(filter.column);
|
|
205
|
+
const operatorConfig = operatorsByType[columnType].find((op) => op.value === filter.operator);
|
|
206
|
+
const sqlOperator = operatorConfig?.sqlOp || filter.operator;
|
|
207
|
+
|
|
208
|
+
// Handle string values based on operator type
|
|
209
|
+
if (typeof value === 'string') {
|
|
210
|
+
if (filter.operator === 'contains') {
|
|
211
|
+
value = `'%${value}%'`;
|
|
212
|
+
} else if (filter.operator === 'starts_with') {
|
|
213
|
+
value = `'${value}%'`;
|
|
214
|
+
} else {
|
|
215
|
+
value = `'${value}'`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const condition = `${filter.column} ${sqlOperator} ${value}`;
|
|
220
|
+
|
|
221
|
+
if (index === 0) {
|
|
222
|
+
return condition;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `${filter.logicalOperator} ${condition}`;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return `WHERE ${conditions.join(' ')}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function generateJSONQuery(): any[] {
|
|
232
|
+
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
233
|
+
return uniqueFilters.filter(isFilterValid).map((filter) => ({
|
|
234
|
+
column: filter.column,
|
|
235
|
+
operator: filter.operator,
|
|
236
|
+
value: filter.value,
|
|
237
|
+
...(filter.logicalOperator && { logicalOperator: filter.logicalOperator })
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getLogicalOperatorCounts() {
|
|
242
|
+
const counts = { OR: 0, AND: 0 };
|
|
243
|
+
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
244
|
+
|
|
245
|
+
uniqueFilters.forEach((filter) => {
|
|
246
|
+
if (filter.logicalOperator) {
|
|
247
|
+
counts[filter.logicalOperator]++;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return counts;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getColumnTypeCounts() {
|
|
254
|
+
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
255
|
+
|
|
256
|
+
const usedTypes = new Set(
|
|
257
|
+
uniqueFilters.filter(isFilterValid).map((filter) => getColumnType(filter.column))
|
|
258
|
+
);
|
|
259
|
+
return usedTypes.size;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function loadValidFilters() {
|
|
263
|
+
const uniqueFilters = getUniqueFilters(filters, completedFilters);
|
|
264
|
+
const validFilterList = uniqueFilters.filter(isFilterValid);
|
|
265
|
+
|
|
266
|
+
return validFilterList;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildDecisionTree() {
|
|
270
|
+
if (completedFilters.length === 0) return null;
|
|
271
|
+
|
|
272
|
+
const tree = {
|
|
273
|
+
type: 'root',
|
|
274
|
+
children: []
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
let currentNode = tree;
|
|
278
|
+
|
|
279
|
+
completedFilters.forEach((filter, index) => {
|
|
280
|
+
const filterNode = {
|
|
281
|
+
type: 'condition',
|
|
282
|
+
filter: filter,
|
|
283
|
+
children: []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (index === 0) {
|
|
287
|
+
currentNode.children.push(filterNode);
|
|
288
|
+
} else {
|
|
289
|
+
const operatorNode = {
|
|
290
|
+
type: 'operator',
|
|
291
|
+
operator: filter.logicalOperator,
|
|
292
|
+
children: [filterNode]
|
|
293
|
+
};
|
|
294
|
+
currentNode.children.push(operatorNode);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return tree;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderDecisionNode(node, level = 0) {
|
|
302
|
+
if (!node) return '';
|
|
303
|
+
|
|
304
|
+
const indent = level * 40;
|
|
305
|
+
|
|
306
|
+
if (node.type === 'condition') {
|
|
307
|
+
return `
|
|
308
|
+
|
|
309
|
+
<div class="relative flex items-center">
|
|
310
|
+
<div class="relative">
|
|
311
|
+
<div class="flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-lg text-sm gap-0.5 w-fit h-fit">
|
|
312
|
+
${node.filter.column}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.operator}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.value}
|
|
313
|
+
</div>
|
|
314
|
+
<div class="absolute right-1 top-1">
|
|
315
|
+
<button onclick="removeCompleteFilter('${node.filter.id}')" class="p-1 text-gray-400 hover:text-red-500">
|
|
316
|
+
<iconify-icon class="absolute -top-2 -right-2 text-gray-400 hover:text-red-500" icon="ic:round-close"></iconify-icon>
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="w-5 h-0.5 bg-gray-300"></div>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
} else if (node.type === 'operator') {
|
|
324
|
+
const operatorColor =
|
|
325
|
+
node.operator === 'AND' ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800';
|
|
326
|
+
return `
|
|
327
|
+
<div class="flex flex-wrap items-center h-fit">
|
|
328
|
+
<div class="flex items-center justify-center ${operatorColor} px-2 py-1 rounded text-xs font-semibold mb-2 w-fit">
|
|
329
|
+
${node.operator}
|
|
330
|
+
</div>
|
|
331
|
+
<div class="w-5 h-0.5 bg-gray-300"></div>
|
|
332
|
+
${node.children.map((child) => renderDecisionNode(child, level + 1)).join('')}
|
|
333
|
+
</div>
|
|
334
|
+
`;
|
|
335
|
+
} else {
|
|
336
|
+
return node.children.map((child) => renderDecisionNode(child, level)).join('');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Generate SQL WHERE clause
|
|
341
|
+
let sqlQuery = $derived(generateSQLQuery());
|
|
342
|
+
let jsonQuery = $derived(generateJSONQuery());
|
|
343
|
+
let validFilters = $derived(loadValidFilters());
|
|
344
|
+
let logicalOperatorCounts = $derived(getLogicalOperatorCounts());
|
|
345
|
+
let columnTypeCounts = $derived(getColumnTypeCounts());
|
|
346
|
+
let decisionTree = $derived(buildDecisionTree());
|
|
347
|
+
|
|
348
|
+
onMount(() => {
|
|
349
|
+
if (browser) {
|
|
350
|
+
(window as any).removeCompleteFilter = removeCompleteFilter;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<div class="flex h-screen bg-gray-50">
|
|
356
|
+
<!-- Left Panel - Filter Builder -->
|
|
357
|
+
<div class="flex w-3/5 flex-col border-r border-gray-200 bg-white">
|
|
358
|
+
<!-- Header -->
|
|
359
|
+
<div class="border-b border-gray-200 p-6">
|
|
360
|
+
<h2 class="mb-2 text-lg font-semibold text-gray-900">Build Filters</h2>
|
|
361
|
+
<p class="text-sm text-gray-600">Add conditions to filter your data</p>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Filter Controls -->
|
|
365
|
+
<div class="flex-1 overflow-y-auto p-6">
|
|
366
|
+
<div class="mb-6 flex items-center justify-between">
|
|
367
|
+
<Button onclick={addFilter}>
|
|
368
|
+
{#snippet leadingIcon()}
|
|
369
|
+
<iconify-icon class="me-1.5" style="font-size: 20px;" icon="ei:plus"></iconify-icon>
|
|
370
|
+
{/snippet}
|
|
371
|
+
Add Filter
|
|
372
|
+
</Button>
|
|
373
|
+
|
|
374
|
+
<Button color="alternative" onclick={clearAllFilters}>
|
|
375
|
+
{#snippet leadingIcon()}
|
|
376
|
+
<iconify-icon class="me-1.5" icon="mi:delete"></iconify-icon>
|
|
377
|
+
{/snippet}
|
|
378
|
+
Clear All
|
|
379
|
+
</Button>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<!-- Filters List -->
|
|
383
|
+
<div class="space-y-4">
|
|
384
|
+
{#each filters as filter, index}
|
|
385
|
+
<div class="relative rounded-lg bg-gray-50 p-4">
|
|
386
|
+
<!-- Logical Operator (for filters after the first) -->
|
|
387
|
+
{#if completedFilters.length > 0}
|
|
388
|
+
<div class="mt-4 mb-2 flex justify-center">
|
|
389
|
+
<div class="flex overflow-hidden rounded-md border border-gray-300 bg-white">
|
|
390
|
+
<button
|
|
391
|
+
onclick={() => toggleLogicalOperator(filter.id)}
|
|
392
|
+
class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
|
|
393
|
+
'AND'
|
|
394
|
+
? 'bg-blue-600 text-white'
|
|
395
|
+
: 'text-gray-600 hover:bg-gray-50'}"
|
|
396
|
+
>
|
|
397
|
+
AND
|
|
398
|
+
</button>
|
|
399
|
+
<button
|
|
400
|
+
onclick={() => toggleLogicalOperator(filter.id)}
|
|
401
|
+
class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
|
|
402
|
+
'OR'
|
|
403
|
+
? 'bg-orange-500 text-white'
|
|
404
|
+
: 'text-gray-600 hover:bg-gray-50'}"
|
|
405
|
+
>
|
|
406
|
+
OR
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
{/if}
|
|
411
|
+
|
|
412
|
+
<!-- Column, Operator, Value Row -->
|
|
413
|
+
<div class="mb-3 grid grid-cols-3 gap-3">
|
|
414
|
+
<div>
|
|
415
|
+
<Label class="mb-2 block text-xs font-medium text-gray-700">Column</Label>
|
|
416
|
+
|
|
417
|
+
<Select
|
|
418
|
+
clearable={false}
|
|
419
|
+
minHeight={40}
|
|
420
|
+
options={columns}
|
|
421
|
+
onChange={(e) => {
|
|
422
|
+
updateFilter(filter.id, 'column', e.name);
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<div>
|
|
428
|
+
<Label class="mb-2 block text-xs font-medium text-gray-700">Operator</Label>
|
|
429
|
+
|
|
430
|
+
<Select
|
|
431
|
+
clearable={false}
|
|
432
|
+
minHeight={40}
|
|
433
|
+
onChange={(e) => {
|
|
434
|
+
updateFilter(filter.id, 'operator', e.value);
|
|
435
|
+
}}
|
|
436
|
+
options={operatorsByType[getColumnType(filter.column)]}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div>
|
|
441
|
+
<Label class="mb-2 block text-xs font-medium text-gray-700">Value</Label>
|
|
442
|
+
|
|
443
|
+
{#if getColumnType(filter.column) === 'string'}
|
|
444
|
+
<Input
|
|
445
|
+
type="text"
|
|
446
|
+
oninput={(e) => {
|
|
447
|
+
updateFilter(filter.id, 'value', e.target.value);
|
|
448
|
+
}}
|
|
449
|
+
/>
|
|
450
|
+
{:else if getColumnType(filter.column) === 'number'}
|
|
451
|
+
<Input
|
|
452
|
+
type="number"
|
|
453
|
+
oninput={(e) =>
|
|
454
|
+
updateFilter(filter.id, 'value', parseFloat(e.target.value) || 0)}
|
|
455
|
+
/>
|
|
456
|
+
{:else if getColumnType(filter.column) === 'date'}
|
|
457
|
+
<Datepicker
|
|
458
|
+
onselect={(e) => {
|
|
459
|
+
updateFilter(filter.id, 'value', e?.toLocaleString());
|
|
460
|
+
}}
|
|
461
|
+
/>
|
|
462
|
+
{:else if getColumnType(filter.column) === 'boolean'}
|
|
463
|
+
<Select
|
|
464
|
+
clearable={false}
|
|
465
|
+
minHeight={40}
|
|
466
|
+
onChange={(e) => updateFilter(filter.id, 'value', e.value)}
|
|
467
|
+
options={[
|
|
468
|
+
{ value: true, label: 'True' },
|
|
469
|
+
{ value: false, label: 'False' }
|
|
470
|
+
]}
|
|
471
|
+
/>
|
|
472
|
+
{/if}
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<!-- Filter Validation -->
|
|
477
|
+
{#if isFilterValid(filter)}
|
|
478
|
+
<div transition:fade class="flex justify-between">
|
|
479
|
+
<div class="flex items-center text-xs text-green-600">
|
|
480
|
+
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
|
481
|
+
<path
|
|
482
|
+
fill-rule="evenodd"
|
|
483
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
484
|
+
clip-rule="evenodd"
|
|
485
|
+
></path>
|
|
486
|
+
</svg>
|
|
487
|
+
Filter is valid
|
|
488
|
+
</div>
|
|
489
|
+
<Button color="green" size="xs" onclick={() => completeFilter(filter.id)}
|
|
490
|
+
>Done</Button
|
|
491
|
+
>
|
|
492
|
+
</div>
|
|
493
|
+
{/if}
|
|
494
|
+
|
|
495
|
+
<!-- Remove Button -->
|
|
496
|
+
<button
|
|
497
|
+
aria-label="removeFilter"
|
|
498
|
+
onclick={() => removeFilter(filter.id)}
|
|
499
|
+
class="absolute top-2 right-2 p-1 text-gray-400 transition-colors hover:text-red-500"
|
|
500
|
+
>
|
|
501
|
+
<iconify-icon icon="ic:round-close"></iconify-icon>
|
|
502
|
+
</button>
|
|
503
|
+
</div>
|
|
504
|
+
{/each}
|
|
505
|
+
</div>
|
|
506
|
+
{#if decisionTree}
|
|
507
|
+
<div class="space-y-4">
|
|
508
|
+
<div class="rounded-lg bg-green-50 p-4">
|
|
509
|
+
<div class="rounded-lg border bg-white p-4">
|
|
510
|
+
{#if decisionTree}
|
|
511
|
+
<div class="flex flex-wrap space-y-3">
|
|
512
|
+
{@html renderDecisionNode(decisionTree)}
|
|
513
|
+
</div>
|
|
514
|
+
{:else}
|
|
515
|
+
<div class="flex h-32 items-center justify-center text-gray-500">
|
|
516
|
+
<p class="text-sm">No decision tree to display</p>
|
|
517
|
+
</div>
|
|
518
|
+
{/if}
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
{/if}
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<!-- Right Panel - Generated Query -->
|
|
527
|
+
<div class="flex w-2/5 flex-col bg-white">
|
|
528
|
+
<!-- Header -->
|
|
529
|
+
<div class="flex justify-between border-b border-gray-200 p-6">
|
|
530
|
+
<div class="flex flex-col">
|
|
531
|
+
<div class="mb-2 flex items-center justify-between">
|
|
532
|
+
<div class="flex items-center gap-2">
|
|
533
|
+
<h2 class="text-lg font-semibold text-gray-900">Generated Query</h2>
|
|
534
|
+
<div class="flex items-center text-sm text-gray-600">
|
|
535
|
+
<span
|
|
536
|
+
class="mr-2 inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
|
|
537
|
+
>
|
|
538
|
+
{validFilters.length} active filters
|
|
539
|
+
</span>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<p class="text-sm text-gray-600">Live preview of your filter conditions</p>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="flex items-center">
|
|
546
|
+
<Button
|
|
547
|
+
color="blue"
|
|
548
|
+
onclick={() => onRunQuery({ jsonBuild: jsonQuery, sqlBuild: sqlQuery })}>Run Query</Button
|
|
549
|
+
>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<!-- Tabs -->
|
|
554
|
+
<div class="border-b border-gray-200">
|
|
555
|
+
<div class="flex">
|
|
556
|
+
<button
|
|
557
|
+
onclick={() => (activeTab = 'SQL')}
|
|
558
|
+
class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'SQL'
|
|
559
|
+
? 'border-blue-500 text-blue-600'
|
|
560
|
+
: 'border-transparent text-gray-600 hover:text-gray-800'}"
|
|
561
|
+
>
|
|
562
|
+
SQL
|
|
563
|
+
</button>
|
|
564
|
+
<button
|
|
565
|
+
onclick={() => (activeTab = 'JSON')}
|
|
566
|
+
class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'JSON'
|
|
567
|
+
? 'border-blue-500 text-blue-600'
|
|
568
|
+
: 'border-transparent text-gray-600 hover:text-gray-800'}"
|
|
569
|
+
>
|
|
570
|
+
JSON
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<!-- Query Content -->
|
|
576
|
+
<div class="flex-1 overflow-y-auto p-6">
|
|
577
|
+
{#if validFilters.length === 0}
|
|
578
|
+
<div class="flex h-32 items-center justify-center text-gray-500">
|
|
579
|
+
<div class="text-center">
|
|
580
|
+
<svg
|
|
581
|
+
class="mx-auto mb-2 h-8 w-8 opacity-50"
|
|
582
|
+
fill="none"
|
|
583
|
+
stroke="currentColor"
|
|
584
|
+
viewBox="0 0 24 24"
|
|
585
|
+
>
|
|
586
|
+
<path
|
|
587
|
+
stroke-linecap="round"
|
|
588
|
+
stroke-linejoin="round"
|
|
589
|
+
stroke-width="2"
|
|
590
|
+
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
|
591
|
+
></path>
|
|
592
|
+
</svg>
|
|
593
|
+
<p class="text-sm">No active filters</p>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
{:else if activeTab === 'SQL'}
|
|
597
|
+
<div class="space-y-4">
|
|
598
|
+
<div class="rounded-lg bg-gray-50 p-4">
|
|
599
|
+
<h3 class="mb-2 text-sm font-medium text-gray-900">SQL WHERE Clause</h3>
|
|
600
|
+
<div class="rounded border bg-white p-3 font-mono text-sm">
|
|
601
|
+
<span class="text-blue-600">WHERE</span>
|
|
602
|
+
{sqlQuery.replace('WHERE ', '')}
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
{:else}
|
|
607
|
+
<div class="space-y-4">
|
|
608
|
+
<div class="rounded-lg bg-gray-50 p-4">
|
|
609
|
+
<h3 class="mb-2 text-sm font-medium text-gray-900">Filter Objects</h3>
|
|
610
|
+
<pre
|
|
611
|
+
class="overflow-x-auto rounded border bg-white p-3 text-xs text-gray-800">{JSON.stringify(
|
|
612
|
+
jsonQuery,
|
|
613
|
+
null,
|
|
614
|
+
2
|
|
615
|
+
)}</pre>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
{/if}
|
|
619
|
+
|
|
620
|
+
<!-- Statistics -->
|
|
621
|
+
<div class="mt-6 grid grid-cols-2 gap-4">
|
|
622
|
+
<div class="rounded-lg bg-blue-50 p-4">
|
|
623
|
+
<h4 class="mb-2 text-sm font-medium text-blue-900">Logical Operators</h4>
|
|
624
|
+
<div class="space-y-1">
|
|
625
|
+
<div class="text-xl font-bold text-blue-600">{logicalOperatorCounts.OR}</div>
|
|
626
|
+
<div class="text-xs text-blue-800">{logicalOperatorCounts.OR} OR</div>
|
|
627
|
+
<div class="text-xs text-blue-800">{logicalOperatorCounts.AND} AND</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div class="rounded-lg bg-green-50 p-4">
|
|
632
|
+
<h4 class="mb-2 text-sm font-medium text-green-900">Column Types</h4>
|
|
633
|
+
<div class="space-y-1">
|
|
634
|
+
<div class="text-xl font-bold text-green-600">{columnTypeCounts}</div>
|
|
635
|
+
<div class="text-xs text-green-800">types used</div>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|