@kronor/dtv 0.2.9
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/.editorconfig +12 -0
- package/.github/copilot-instructions.md +64 -0
- package/.github/workflows/ci.yml +51 -0
- package/.husky/pre-commit +8 -0
- package/README.md +63 -0
- package/docs/api/README.md +32 -0
- package/docs/api/cell-renderers.md +121 -0
- package/docs/api/no-rows-component.md +71 -0
- package/docs/api/runtime.md +78 -0
- package/e2e/app.spec.ts +6 -0
- package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
- package/e2e/filter-sharing.spec.ts +113 -0
- package/e2e/filter-url-persistence.spec.ts +36 -0
- package/e2e/graphqlMock.ts +144 -0
- package/e2e/multi-field-filters.spec.ts +95 -0
- package/e2e/pagination.spec.ts +38 -0
- package/e2e/payment-request-email-filter.spec.ts +67 -0
- package/e2e/save-filter-splitbutton.spec.ts +68 -0
- package/e2e/simple-view-email-filter.spec.ts +67 -0
- package/e2e/simple-view-transforms.spec.ts +171 -0
- package/e2e/simple-view.spec.ts +104 -0
- package/e2e/transform-regression.spec.ts +108 -0
- package/eslint.config.js +30 -0
- package/index.html +17 -0
- package/jest.config.js +10 -0
- package/package.json +45 -0
- package/playwright.config.ts +54 -0
- package/public/vite.svg +1 -0
- package/src/App.externalRuntime.test.ts +190 -0
- package/src/App.tsx +540 -0
- package/src/assets/react.svg +1 -0
- package/src/components/AIAssistantForm.tsx +241 -0
- package/src/components/FilterForm.test.ts +82 -0
- package/src/components/FilterForm.tsx +375 -0
- package/src/components/PhoneNumberFilter.tsx +102 -0
- package/src/components/SavedFilterList.tsx +181 -0
- package/src/components/SpeechInput.tsx +67 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/TablePagination.tsx +40 -0
- package/src/components/aiAssistant.test.ts +270 -0
- package/src/components/aiAssistant.ts +291 -0
- package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
- package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
- package/src/framework/cell-renderer-components/Link.tsx +28 -0
- package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
- package/src/framework/cell-renderer-components.test.ts +353 -0
- package/src/framework/column-definition.tsx +85 -0
- package/src/framework/currency.test.ts +46 -0
- package/src/framework/currency.ts +62 -0
- package/src/framework/data.staticConditions.test.ts +46 -0
- package/src/framework/data.test.ts +167 -0
- package/src/framework/data.ts +162 -0
- package/src/framework/filter-form-state.test.ts +189 -0
- package/src/framework/filter-form-state.ts +185 -0
- package/src/framework/filter-sharing.test.ts +135 -0
- package/src/framework/filter-sharing.ts +118 -0
- package/src/framework/filters.ts +194 -0
- package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
- package/src/framework/graphql.paginationKey.test.ts +29 -0
- package/src/framework/graphql.test.ts +286 -0
- package/src/framework/graphql.ts +462 -0
- package/src/framework/native-runtime/index.tsx +33 -0
- package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
- package/src/framework/runtime-reference.test.ts +172 -0
- package/src/framework/runtime.ts +15 -0
- package/src/framework/saved-filters.test.ts +422 -0
- package/src/framework/saved-filters.ts +293 -0
- package/src/framework/state.test.ts +86 -0
- package/src/framework/state.ts +148 -0
- package/src/framework/transform.test.ts +51 -0
- package/src/framework/view-parser-initialvalues.test.ts +228 -0
- package/src/framework/view-parser.ts +714 -0
- package/src/framework/view.test.ts +1805 -0
- package/src/framework/view.ts +38 -0
- package/src/index.css +6 -0
- package/src/main.tsx +99 -0
- package/src/views/index.ts +12 -0
- package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
- package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
- package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
- package/src/views/payment-requests/index.ts +1 -0
- package/src/views/payment-requests/runtime.tsx +145 -0
- package/src/views/payment-requests/view.json +692 -0
- package/src/views/payment-requests-initial-values.test.ts +73 -0
- package/src/views/request-log/index.ts +2 -0
- package/src/views/request-log/runtime.tsx +47 -0
- package/src/views/request-log/view.json +123 -0
- package/src/views/simple-test-view/index.ts +3 -0
- package/src/views/simple-test-view/runtime.tsx +85 -0
- package/src/views/simple-test-view/view.json +191 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +7 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.jest.json +6 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { FilterState } from './state';
|
|
2
|
+
import { buildHasuraConditions } from './graphql';
|
|
3
|
+
import { FilterSchemasAndGroups, filterExpr, filterControl } from './filters';
|
|
4
|
+
|
|
5
|
+
describe('buildHasuraConditions', () => {
|
|
6
|
+
// Helper function to create a simple filter schema for testing
|
|
7
|
+
const createFilterSchema = (filterId: string, expression: any): FilterSchemasAndGroups => ({
|
|
8
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
9
|
+
filters: [
|
|
10
|
+
{
|
|
11
|
+
id: filterId,
|
|
12
|
+
label: 'Test Filter',
|
|
13
|
+
expression,
|
|
14
|
+
group: 'basic',
|
|
15
|
+
aiGenerated: false
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return an empty object for no conditions', () => {
|
|
21
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
22
|
+
groups: [],
|
|
23
|
+
filters: []
|
|
24
|
+
};
|
|
25
|
+
expect(buildHasuraConditions(new Map(), filterSchema)).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle a single condition', () => {
|
|
29
|
+
const filterSchema = createFilterSchema(
|
|
30
|
+
'name-filter',
|
|
31
|
+
filterExpr.equals('name', filterControl.text())
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const formState: FilterState = new Map([
|
|
35
|
+
['name-filter', { type: 'leaf', field: 'name', filterType: 'equals', value: 'test', control: { type: 'text' } }]
|
|
36
|
+
]);
|
|
37
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({ name: { _eq: 'test' } });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle multiple conditions with an implicit AND', () => {
|
|
41
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
42
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
43
|
+
filters: [
|
|
44
|
+
{
|
|
45
|
+
id: 'name-filter',
|
|
46
|
+
label: 'Name Filter',
|
|
47
|
+
expression: filterExpr.equals('name', filterControl.text()),
|
|
48
|
+
group: 'basic',
|
|
49
|
+
aiGenerated: false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'age-filter',
|
|
53
|
+
label: 'Age Filter',
|
|
54
|
+
expression: filterExpr.greaterThan('age', filterControl.number()),
|
|
55
|
+
group: 'basic',
|
|
56
|
+
aiGenerated: false
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const formState: FilterState = new Map([
|
|
62
|
+
['name-filter', { type: 'leaf', field: 'name', filterType: 'equals', value: 'test', control: { type: 'text' } }],
|
|
63
|
+
['age-filter', { type: 'leaf', field: 'age', filterType: 'greaterThan', value: 20, control: { type: 'number' } }]
|
|
64
|
+
]);
|
|
65
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
66
|
+
_and: [
|
|
67
|
+
{ name: { _eq: 'test' } },
|
|
68
|
+
{ age: { _gt: 20 } }
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle nested fields', () => {
|
|
74
|
+
const filterSchema = createFilterSchema(
|
|
75
|
+
'user-name-filter',
|
|
76
|
+
filterExpr.equals('user.name', filterControl.text())
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const formState: FilterState = new Map([
|
|
80
|
+
['user-name-filter', { type: 'leaf', field: 'user.name', filterType: 'equals', value: 'test', control: { type: 'text' } }]
|
|
81
|
+
]);
|
|
82
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
83
|
+
user: { name: { _eq: 'test' } }
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle deeply nested fields', () => {
|
|
88
|
+
const filterSchema = createFilterSchema(
|
|
89
|
+
'deep-filter',
|
|
90
|
+
filterExpr.equals('a.b.c.d', filterControl.text())
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const formState: FilterState = new Map([
|
|
94
|
+
['deep-filter', { type: 'leaf', field: 'a.b.c.d', filterType: 'equals', value: 'deep', control: { type: 'text' } }]
|
|
95
|
+
]);
|
|
96
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
97
|
+
a: { b: { c: { d: { _eq: 'deep' } } } }
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle explicit AND/OR conditions', () => {
|
|
102
|
+
const filterSchema = createFilterSchema(
|
|
103
|
+
'or-filter',
|
|
104
|
+
filterExpr.or([
|
|
105
|
+
filterExpr.equals('name', filterControl.text()),
|
|
106
|
+
filterExpr.equals('name', filterControl.text())
|
|
107
|
+
])
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const formState: FilterState = new Map([
|
|
111
|
+
['or-filter', {
|
|
112
|
+
type: 'or',
|
|
113
|
+
filterType: 'or',
|
|
114
|
+
children: [
|
|
115
|
+
{ type: 'leaf', field: 'name', filterType: 'equals', value: 'test', control: { type: 'text' } },
|
|
116
|
+
{ type: 'leaf', field: 'name', filterType: 'equals', value: 'another', control: { type: 'text' } }
|
|
117
|
+
]
|
|
118
|
+
}]
|
|
119
|
+
]);
|
|
120
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
121
|
+
_or: [
|
|
122
|
+
{ name: { _eq: 'test' } },
|
|
123
|
+
{ name: { _eq: 'another' } }
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle NOT conditions', () => {
|
|
129
|
+
const filterSchema = createFilterSchema(
|
|
130
|
+
'not-filter',
|
|
131
|
+
filterExpr.not(filterExpr.equals('name', filterControl.text()))
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const formState: FilterState = new Map([
|
|
135
|
+
['not-filter', {
|
|
136
|
+
type: 'not',
|
|
137
|
+
filterType: 'not',
|
|
138
|
+
child: { type: 'leaf', field: 'name', filterType: 'equals', value: 'test', control: { type: 'text' } }
|
|
139
|
+
}]
|
|
140
|
+
]);
|
|
141
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
142
|
+
_not: { name: { _eq: 'test' } }
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle complex nested structures', () => {
|
|
147
|
+
const filterSchema = createFilterSchema(
|
|
148
|
+
'complex-filter',
|
|
149
|
+
filterExpr.and([
|
|
150
|
+
filterExpr.greaterThan('age', filterControl.number()),
|
|
151
|
+
filterExpr.or([
|
|
152
|
+
filterExpr.iLike('user.name', filterControl.text()),
|
|
153
|
+
filterExpr.not(filterExpr.equals('user.role', filterControl.text()))
|
|
154
|
+
])
|
|
155
|
+
])
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const formState: FilterState = new Map([
|
|
159
|
+
['complex-filter', {
|
|
160
|
+
type: 'and',
|
|
161
|
+
filterType: 'and',
|
|
162
|
+
children: [
|
|
163
|
+
{ type: 'leaf', field: 'age', filterType: 'greaterThan', value: 20, control: { type: 'number' } },
|
|
164
|
+
{
|
|
165
|
+
type: 'or',
|
|
166
|
+
filterType: 'or',
|
|
167
|
+
children: [
|
|
168
|
+
{ type: 'leaf', field: 'user.name', filterType: 'iLike', value: '%test%', control: { type: 'text' } },
|
|
169
|
+
{
|
|
170
|
+
type: 'not',
|
|
171
|
+
filterType: 'not',
|
|
172
|
+
child: { type: 'leaf', field: 'user.role', filterType: 'equals', value: 'admin', control: { type: 'text' } }
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}]
|
|
178
|
+
]);
|
|
179
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
180
|
+
_and: [
|
|
181
|
+
{ age: { _gt: 20 } },
|
|
182
|
+
{
|
|
183
|
+
_or: [
|
|
184
|
+
{ user: { name: { _ilike: '%test%' } } },
|
|
185
|
+
{ _not: { user: { role: { _eq: 'admin' } } } }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should ignore empty or invalid values', () => {
|
|
193
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
194
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
195
|
+
filters: [
|
|
196
|
+
{
|
|
197
|
+
id: 'name-filter',
|
|
198
|
+
label: 'Name Filter',
|
|
199
|
+
expression: filterExpr.equals('name', filterControl.text()),
|
|
200
|
+
group: 'basic',
|
|
201
|
+
aiGenerated: false
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'age-filter',
|
|
205
|
+
label: 'Age Filter',
|
|
206
|
+
expression: filterExpr.greaterThan('age', filterControl.number()),
|
|
207
|
+
group: 'basic',
|
|
208
|
+
aiGenerated: false
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'tags-filter',
|
|
212
|
+
label: 'Tags Filter',
|
|
213
|
+
expression: filterExpr.in('tags', filterControl.multiselect({ items: [] })),
|
|
214
|
+
group: 'basic',
|
|
215
|
+
aiGenerated: false
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'valid-filter',
|
|
219
|
+
label: 'Valid Filter',
|
|
220
|
+
expression: filterExpr.equals('valid', filterControl.text()),
|
|
221
|
+
group: 'basic',
|
|
222
|
+
aiGenerated: false
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const formState: FilterState = new Map([
|
|
228
|
+
['name-filter', { type: 'leaf', field: 'name', filterType: 'equals', value: '', control: { type: 'text' } }],
|
|
229
|
+
['age-filter', { type: 'leaf', field: 'age', filterType: 'greaterThan', value: undefined, control: { type: 'number' } }],
|
|
230
|
+
['tags-filter', { type: 'leaf', field: 'tags', filterType: 'in', value: [], control: { type: 'multiselect', items: [] } }],
|
|
231
|
+
['valid-filter', { type: 'leaf', field: 'valid', filterType: 'equals', value: 'good', control: { type: 'text' } }]
|
|
232
|
+
]);
|
|
233
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({ valid: { _eq: 'good' } });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle custom operators', () => {
|
|
237
|
+
const filterSchema = createFilterSchema(
|
|
238
|
+
'custom-filter',
|
|
239
|
+
filterExpr.equals('custom_field', filterControl.customOperator({
|
|
240
|
+
operators: [{ label: 'Custom Op', value: '_custom_op' }],
|
|
241
|
+
valueControl: filterControl.text()
|
|
242
|
+
}))
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const formState: FilterState = new Map([
|
|
246
|
+
['custom-filter', {
|
|
247
|
+
type: 'leaf',
|
|
248
|
+
field: 'custom_field',
|
|
249
|
+
filterType: 'equals', // This is not used for custom operators, but required by the type
|
|
250
|
+
value: { operator: '_custom_op', value: 'some_value' },
|
|
251
|
+
control: { type: 'customOperator', operators: [{ label: 'Custom Op', value: '_custom_op' }], valueControl: { type: 'text' } }
|
|
252
|
+
}]
|
|
253
|
+
]);
|
|
254
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
255
|
+
custom_field: { _custom_op: 'some_value' }
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should handle custom operators with nested fields', () => {
|
|
260
|
+
const filterSchema = createFilterSchema(
|
|
261
|
+
'user-name-custom-filter',
|
|
262
|
+
filterExpr.equals('user.name', filterControl.customOperator({
|
|
263
|
+
operators: [{ label: 'iLike', value: '_ilike' }],
|
|
264
|
+
valueControl: filterControl.text()
|
|
265
|
+
}))
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const formState: FilterState = new Map([
|
|
269
|
+
['user-name-custom-filter', {
|
|
270
|
+
type: 'leaf',
|
|
271
|
+
field: 'user.name',
|
|
272
|
+
filterType: 'equals', // This is not used for custom operators, but required by the type
|
|
273
|
+
value: { operator: '_ilike', value: '%test%' },
|
|
274
|
+
control: { type: 'customOperator', operators: [{ label: 'iLike', value: '_ilike' }], valueControl: { type: 'text' } }
|
|
275
|
+
}]
|
|
276
|
+
]);
|
|
277
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
278
|
+
user: { name: { _ilike: '%test%' } }
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("Multi-field Filter Support with buildHasuraConditions", () => {
|
|
284
|
+
it("should handle object format with and", () => {
|
|
285
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
286
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
287
|
+
filters: [
|
|
288
|
+
{
|
|
289
|
+
id: 'multi-and-filter',
|
|
290
|
+
label: 'Multi AND Filter',
|
|
291
|
+
expression: filterExpr.equals({ and: ['name', 'title'] }, filterControl.text()),
|
|
292
|
+
group: 'basic',
|
|
293
|
+
aiGenerated: false
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const formState: FilterState = new Map([
|
|
299
|
+
['multi-and-filter', {
|
|
300
|
+
type: 'leaf',
|
|
301
|
+
field: { and: ['name', 'title'] },
|
|
302
|
+
filterType: 'equals',
|
|
303
|
+
value: 'test',
|
|
304
|
+
control: { type: 'text' }
|
|
305
|
+
}]
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
const result = buildHasuraConditions(formState, filterSchema);
|
|
309
|
+
|
|
310
|
+
expect(result).toEqual({
|
|
311
|
+
_and: [
|
|
312
|
+
{ name: { _eq: 'test' } },
|
|
313
|
+
{ title: { _eq: 'test' } }
|
|
314
|
+
]
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should handle object format with or", () => {
|
|
319
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
320
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
321
|
+
filters: [
|
|
322
|
+
{
|
|
323
|
+
id: 'multi-or-filter',
|
|
324
|
+
label: 'Multi OR Filter',
|
|
325
|
+
expression: filterExpr.equals({ or: ['name', 'title'] }, filterControl.text()),
|
|
326
|
+
group: 'basic',
|
|
327
|
+
aiGenerated: false
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const formState: FilterState = new Map([
|
|
333
|
+
['multi-or-filter', {
|
|
334
|
+
type: 'leaf',
|
|
335
|
+
field: { or: ['name', 'title'] },
|
|
336
|
+
filterType: 'equals',
|
|
337
|
+
value: 'test',
|
|
338
|
+
control: { type: 'text' }
|
|
339
|
+
}]
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
const result = buildHasuraConditions(formState, filterSchema);
|
|
343
|
+
|
|
344
|
+
expect(result).toEqual({
|
|
345
|
+
_or: [
|
|
346
|
+
{ name: { _eq: 'test' } },
|
|
347
|
+
{ title: { _eq: 'test' } }
|
|
348
|
+
]
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should handle nested fields with object format", () => {
|
|
353
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
354
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
355
|
+
filters: [
|
|
356
|
+
{
|
|
357
|
+
id: 'nested-multi-filter',
|
|
358
|
+
label: 'Nested Multi Filter',
|
|
359
|
+
expression: filterExpr.iLike({ or: ['user.email', 'user.username'] }, filterControl.text()),
|
|
360
|
+
group: 'basic',
|
|
361
|
+
aiGenerated: false
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const formState: FilterState = new Map([
|
|
367
|
+
['nested-multi-filter', {
|
|
368
|
+
type: 'leaf',
|
|
369
|
+
field: { or: ['user.email', 'user.username'] },
|
|
370
|
+
filterType: 'iLike',
|
|
371
|
+
value: '%john%',
|
|
372
|
+
control: { type: 'text' }
|
|
373
|
+
}]
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
const result = buildHasuraConditions(formState, filterSchema);
|
|
377
|
+
|
|
378
|
+
expect(result).toEqual({
|
|
379
|
+
_or: [
|
|
380
|
+
{ user: { email: { _ilike: '%john%' } } },
|
|
381
|
+
{ user: { username: { _ilike: '%john%' } } }
|
|
382
|
+
]
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should handle mixed multi-field and single field expressions", () => {
|
|
387
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
388
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
389
|
+
filters: [
|
|
390
|
+
{
|
|
391
|
+
id: 'search-filter',
|
|
392
|
+
label: 'Search Filter',
|
|
393
|
+
expression: filterExpr.iLike({ or: ['name', 'title'] }, filterControl.text()),
|
|
394
|
+
group: 'basic',
|
|
395
|
+
aiGenerated: false
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: 'category-filter',
|
|
399
|
+
label: 'Category Filter',
|
|
400
|
+
expression: filterExpr.equals('category', filterControl.text()),
|
|
401
|
+
group: 'basic',
|
|
402
|
+
aiGenerated: false
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const formState: FilterState = new Map([
|
|
408
|
+
['search-filter', {
|
|
409
|
+
type: 'leaf',
|
|
410
|
+
field: { or: ['name', 'title'] },
|
|
411
|
+
filterType: 'iLike',
|
|
412
|
+
value: '%search%',
|
|
413
|
+
control: { type: 'text' }
|
|
414
|
+
}],
|
|
415
|
+
['category-filter', {
|
|
416
|
+
type: 'leaf',
|
|
417
|
+
field: 'category',
|
|
418
|
+
filterType: 'equals',
|
|
419
|
+
value: 'tech',
|
|
420
|
+
control: { type: 'text' }
|
|
421
|
+
}]
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
const result = buildHasuraConditions(formState, filterSchema);
|
|
425
|
+
|
|
426
|
+
expect(result).toEqual({
|
|
427
|
+
_and: [
|
|
428
|
+
{
|
|
429
|
+
_or: [
|
|
430
|
+
{ name: { _ilike: '%search%' } },
|
|
431
|
+
{ title: { _ilike: '%search%' } }
|
|
432
|
+
]
|
|
433
|
+
},
|
|
434
|
+
{ category: { _eq: 'tech' } }
|
|
435
|
+
]
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should handle transforms in filter schema', () => {
|
|
440
|
+
// Create a filter schema with transform
|
|
441
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
442
|
+
groups: [{ name: 'basic', label: 'Basic Filters' }],
|
|
443
|
+
filters: [
|
|
444
|
+
{
|
|
445
|
+
id: 'email-filter',
|
|
446
|
+
label: 'Email Filter',
|
|
447
|
+
expression: filterExpr.equals('user.email', filterControl.text(), {
|
|
448
|
+
toQuery: (input: unknown) => ({
|
|
449
|
+
field: 'user.email_address', // transform field name
|
|
450
|
+
value: String(input).toLowerCase() // transform value
|
|
451
|
+
})
|
|
452
|
+
}),
|
|
453
|
+
group: 'basic',
|
|
454
|
+
aiGenerated: false
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// Create filter state with original value
|
|
460
|
+
const formState = new Map();
|
|
461
|
+
formState.set('email-filter', {
|
|
462
|
+
type: 'leaf' as const,
|
|
463
|
+
field: 'user.email', // This should be ignored in favor of schema
|
|
464
|
+
value: 'TEST@EXAMPLE.COM',
|
|
465
|
+
control: { type: 'text' as const }, // This should be ignored in favor of schema
|
|
466
|
+
filterType: 'equals' // This should be ignored in favor of schema
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(buildHasuraConditions(formState, filterSchema)).toEqual({
|
|
470
|
+
user: { email_address: { _eq: 'test@example.com' } }
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { generateGraphQLQuery } from './graphql';
|
|
2
|
+
import { ColumnDefinition, field } from './column-definition';
|
|
3
|
+
|
|
4
|
+
describe('generateGraphQLQuery paginationKey inclusion', () => {
|
|
5
|
+
it('includes paginationKey field when not present in column definitions', () => {
|
|
6
|
+
const columns: ColumnDefinition[] = [
|
|
7
|
+
{ name: 'Name', data: [field('name')], cellRenderer: () => null }
|
|
8
|
+
];
|
|
9
|
+
const query = generateGraphQLQuery('users', columns, 'UserBoolExp', 'UserOrderBy', 'id');
|
|
10
|
+
// Expect both name and id to appear. The selection set is at the end of the query string.
|
|
11
|
+
// We specifically test that id appears as a standalone field even though not in columns.
|
|
12
|
+
// Naively check for newline followed by two spaces then id (rendering style in renderGraphQLQuery)
|
|
13
|
+
expect(query).toMatch(/\bid\b/); // id should appear somewhere
|
|
14
|
+
// Ensure name also exists
|
|
15
|
+
expect(query).toContain('name');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not duplicate paginationKey if already present', () => {
|
|
19
|
+
const columns: ColumnDefinition[] = [
|
|
20
|
+
{ name: 'ID', data: [field('id')], cellRenderer: () => null },
|
|
21
|
+
{ name: 'Name', data: [field('name')], cellRenderer: () => null }
|
|
22
|
+
];
|
|
23
|
+
const query = generateGraphQLQuery('users', columns, 'UserBoolExp', 'UserOrderBy', 'id');
|
|
24
|
+
// Count occurrences of id field (rough heuristic). Should not exceed 1 meaningful occurrence inside selection set excluding variable definitions etc.
|
|
25
|
+
const idMatches = query.match(/\bid\b/g) || [];
|
|
26
|
+
// Should be at least one, but not more than 2 (one might appear in orderBy variable name or types in some schemas). We just assert not > 3 to be safe.
|
|
27
|
+
expect(idMatches.length).toBeGreaterThanOrEqual(1);
|
|
28
|
+
});
|
|
29
|
+
});
|