@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,67 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { InputText } from 'primereact/inputtext';
|
|
3
|
+
|
|
4
|
+
interface SpeechInputProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function SpeechInput({ value, onChange }: SpeechInputProps) {
|
|
10
|
+
const [isListening, setIsListening] = useState(false);
|
|
11
|
+
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) return;
|
|
15
|
+
recognitionRef.current = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
|
16
|
+
recognitionRef.current.continuous = false;
|
|
17
|
+
recognitionRef.current.interimResults = false;
|
|
18
|
+
recognitionRef.current.lang = 'en-US';
|
|
19
|
+
recognitionRef.current.onresult = (event: SpeechRecognitionEvent) => {
|
|
20
|
+
const transcript = event.results[0][0].transcript;
|
|
21
|
+
onChange(transcript);
|
|
22
|
+
setIsListening(false);
|
|
23
|
+
};
|
|
24
|
+
recognitionRef.current.onend = () => setIsListening(false);
|
|
25
|
+
recognitionRef.current.onerror = () => setIsListening(false);
|
|
26
|
+
}, [onChange]);
|
|
27
|
+
|
|
28
|
+
const handleMicClick = () => {
|
|
29
|
+
if (!recognitionRef.current) return;
|
|
30
|
+
if (isListening) {
|
|
31
|
+
recognitionRef.current.stop();
|
|
32
|
+
setIsListening(false);
|
|
33
|
+
} else {
|
|
34
|
+
onChange('');
|
|
35
|
+
setIsListening(true);
|
|
36
|
+
recognitionRef.current.start();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="tw:flex tw:flex-1 tw:items-center tw:gap-2">
|
|
42
|
+
<InputText
|
|
43
|
+
value={value}
|
|
44
|
+
onChange={e => onChange(e.target.value)}
|
|
45
|
+
placeholder="Speak or type here..."
|
|
46
|
+
className="tw:flex-1"
|
|
47
|
+
/>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={handleMicClick}
|
|
51
|
+
className={`tw:rounded-full tw:p-2 tw:border ${isListening ? 'tw:bg-red-500 tw:text-white' : 'tw:bg-gray-200 tw:text-black'} tw:transition`}
|
|
52
|
+
aria-label={isListening ? 'Stop listening' : 'Start listening'}
|
|
53
|
+
>
|
|
54
|
+
{/* Mic icon: classic microphone shape */}
|
|
55
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="tw:h-5">
|
|
56
|
+
<rect x="9" y="2" width="6" height="12" rx="3" />
|
|
57
|
+
<path d="M5 10v2a7 7 0 0 0 14 0v-2" />
|
|
58
|
+
<line x1="12" y1="19" x2="12" y2="22" />
|
|
59
|
+
<line x1="8" y1="22" x2="16" y2="22" />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
<span className="tw:text-s tw:ml-2">{isListening ? 'Listening...' : ''}</span>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default SpeechInput;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DataTable, DataTableExportFunctionEvent } from 'primereact/datatable';
|
|
3
|
+
import { Column } from 'primereact/column';
|
|
4
|
+
import { Tag } from 'primereact/tag';
|
|
5
|
+
import { ColumnDefinition } from '../framework/column-definition';
|
|
6
|
+
import { NoRowsComponent } from '../framework/view';
|
|
7
|
+
import { FlexRow, FlexColumn, DateTime } from '../framework/cell-renderer-components/LayoutHelpers';
|
|
8
|
+
import { CurrencyAmount } from '../framework/cell-renderer-components/CurrencyAmount';
|
|
9
|
+
import { majorToMinor, minorToMajor } from '../framework/currency';
|
|
10
|
+
import { Mapping } from '../framework/cell-renderer-components/Mapping';
|
|
11
|
+
import { Link } from '../framework/cell-renderer-components/Link';
|
|
12
|
+
import { FilterState, getFilterStateById, setFilterStateById } from '../framework/state';
|
|
13
|
+
import { FilterFormState } from '../framework/filter-form-state';
|
|
14
|
+
|
|
15
|
+
type TableProps = {
|
|
16
|
+
viewId: string;
|
|
17
|
+
columns: ColumnDefinition[];
|
|
18
|
+
data: Record<string, unknown>[][]; // Array of rows, each row is an array of values for the columns
|
|
19
|
+
noRowsComponent?: NoRowsComponent; // The noRowsComponent function
|
|
20
|
+
setFilterState: (filterState: FilterState) => void; // Function to update filter state
|
|
21
|
+
filterState: FilterState; // Current filter state
|
|
22
|
+
triggerRefetch: () => void; // Function to trigger data refetch
|
|
23
|
+
ref?: React.Ref<DataTable<any>>; // An outside ref to the DataTable instance
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function Table({
|
|
27
|
+
viewId,
|
|
28
|
+
columns,
|
|
29
|
+
data,
|
|
30
|
+
noRowsComponent,
|
|
31
|
+
setFilterState,
|
|
32
|
+
filterState,
|
|
33
|
+
triggerRefetch,
|
|
34
|
+
ref
|
|
35
|
+
}: TableProps) {
|
|
36
|
+
// Create wrapped setFilterState that provides current state to updater function
|
|
37
|
+
const wrappedSetFilterState = (updater: (currentState: FilterState) => FilterState) => {
|
|
38
|
+
const newState = updater(filterState);
|
|
39
|
+
setFilterState(newState);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Instantiate the noRowsComponent if it exists
|
|
43
|
+
const noDataRowsComponent = noRowsComponent
|
|
44
|
+
? noRowsComponent({
|
|
45
|
+
filterState,
|
|
46
|
+
setFilterState: wrappedSetFilterState,
|
|
47
|
+
applyFilters: triggerRefetch,
|
|
48
|
+
updateFilterById: (filterId: string, updater: (currentValue: FilterFormState) => FilterFormState) => {
|
|
49
|
+
wrappedSetFilterState(currentState => {
|
|
50
|
+
try {
|
|
51
|
+
const currentFilter = getFilterStateById(currentState, filterId);
|
|
52
|
+
const updatedFilter = updater(currentFilter);
|
|
53
|
+
if (updatedFilter === currentFilter) return currentState;
|
|
54
|
+
return setFilterStateById(currentState, filterId, updatedFilter);
|
|
55
|
+
} catch {
|
|
56
|
+
return currentState; // filter missing -> no change
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
const exportFunction = (event: DataTableExportFunctionEvent<any>) => {
|
|
64
|
+
const data = (event.rowData as [])[Number(event.field)]
|
|
65
|
+
return data[Object.keys(data)[0]]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<DataTable
|
|
70
|
+
ref={ref}
|
|
71
|
+
value={data}
|
|
72
|
+
tableStyle={{ minWidth: '50rem' }}
|
|
73
|
+
showGridlines
|
|
74
|
+
stripedRows
|
|
75
|
+
size='small'
|
|
76
|
+
emptyMessage={noDataRowsComponent}
|
|
77
|
+
exportFunction={exportFunction}
|
|
78
|
+
exportFilename={viewId}
|
|
79
|
+
>
|
|
80
|
+
{columns.map((column, columnIndex) => (
|
|
81
|
+
<Column
|
|
82
|
+
key={columnIndex}
|
|
83
|
+
field={columnIndex.toString()}
|
|
84
|
+
header={column.name}
|
|
85
|
+
body={rowData => column.cellRenderer({
|
|
86
|
+
data: rowData[columnIndex],
|
|
87
|
+
setFilterState: wrappedSetFilterState,
|
|
88
|
+
applyFilters: triggerRefetch,
|
|
89
|
+
updateFilterById: (filterId: string, updater: (currentValue: FilterFormState) => FilterFormState) => {
|
|
90
|
+
wrappedSetFilterState(currentState => {
|
|
91
|
+
try {
|
|
92
|
+
const currentFilter = getFilterStateById(currentState, filterId);
|
|
93
|
+
const updatedFilter = updater(currentFilter);
|
|
94
|
+
if (updatedFilter === currentFilter) return currentState;
|
|
95
|
+
return setFilterStateById(currentState, filterId, updatedFilter);
|
|
96
|
+
} catch {
|
|
97
|
+
return currentState;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
createElement: React.createElement,
|
|
102
|
+
components: {
|
|
103
|
+
Badge: Tag,
|
|
104
|
+
FlexRow,
|
|
105
|
+
FlexColumn,
|
|
106
|
+
Mapping,
|
|
107
|
+
DateTime,
|
|
108
|
+
CurrencyAmount,
|
|
109
|
+
Link
|
|
110
|
+
},
|
|
111
|
+
currency: { minorToMajor, majorToMinor }
|
|
112
|
+
})}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</DataTable>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default Table;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from 'primereact/button';
|
|
3
|
+
|
|
4
|
+
interface TablePaginationProps {
|
|
5
|
+
onPageChange: () => void;
|
|
6
|
+
onPrevPage: () => void;
|
|
7
|
+
hasNextPage: boolean;
|
|
8
|
+
hasPrevPage: boolean;
|
|
9
|
+
currentPage: number; // 0-based
|
|
10
|
+
rowsPerPage: number;
|
|
11
|
+
actualRows: number; // number of rows in the current page
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TablePagination: React.FC<TablePaginationProps> = ({ onPageChange, onPrevPage, hasNextPage, hasPrevPage, currentPage, rowsPerPage, actualRows }) => {
|
|
15
|
+
const start = currentPage * rowsPerPage + 1;
|
|
16
|
+
const end = start + actualRows - 1;
|
|
17
|
+
return (
|
|
18
|
+
<div className="tw:flex tw:justify-center tw:items-center tw:mt-4 tw:gap-2">
|
|
19
|
+
<Button
|
|
20
|
+
rounded
|
|
21
|
+
icon="pi pi-angle-left"
|
|
22
|
+
onClick={onPrevPage}
|
|
23
|
+
disabled={!hasPrevPage}
|
|
24
|
+
className="p-button-sm"
|
|
25
|
+
data-testid="pagination-prev"
|
|
26
|
+
/>
|
|
27
|
+
<span className="tw:text-s tw:text-gray-600 tw:mx-3" data-testid="pagination-page">{start}-{end}</span>
|
|
28
|
+
<Button
|
|
29
|
+
rounded
|
|
30
|
+
icon="pi pi-angle-right"
|
|
31
|
+
onClick={onPageChange}
|
|
32
|
+
disabled={!hasNextPage}
|
|
33
|
+
className="p-button-sm"
|
|
34
|
+
data-testid="pagination-next"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default TablePagination;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// src/components/aiAssistant.test.ts
|
|
2
|
+
import { FilterSchemasAndGroups } from '../framework/filters';
|
|
3
|
+
import { buildInitialFormState } from '../framework/state';
|
|
4
|
+
import { mergeFilterFormState } from './aiAssistant';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('aiAssistant customOperator patching', () => {
|
|
8
|
+
it('should patch customOperator values when AI returns a plain string', () => {
|
|
9
|
+
// Create a filter schema with a customOperator
|
|
10
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
11
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
12
|
+
filters: [
|
|
13
|
+
{
|
|
14
|
+
id: 'test-filter-1',
|
|
15
|
+
label: 'Test Filter',
|
|
16
|
+
group: 'test',
|
|
17
|
+
expression: {
|
|
18
|
+
type: 'equals',
|
|
19
|
+
field: 'test',
|
|
20
|
+
value: {
|
|
21
|
+
type: 'customOperator',
|
|
22
|
+
operators: [
|
|
23
|
+
{ label: 'Equals', value: '_eq' },
|
|
24
|
+
{ label: 'Not Equals', value: '_neq' }
|
|
25
|
+
],
|
|
26
|
+
valueControl: { type: 'text' }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
aiGenerated: false
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Build empty state
|
|
35
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
36
|
+
|
|
37
|
+
// AI returns a plain string instead of an object
|
|
38
|
+
const aiState = {
|
|
39
|
+
type: 'leaf',
|
|
40
|
+
field: 'test',
|
|
41
|
+
value: 'test_value'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Apply our merge function
|
|
45
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
46
|
+
|
|
47
|
+
// The result should have the string wrapped in an object with the default operator
|
|
48
|
+
expect(result.type).toBe('leaf');
|
|
49
|
+
if (result.type === 'leaf') {
|
|
50
|
+
expect(result.value).toEqual({
|
|
51
|
+
operator: '_eq',
|
|
52
|
+
value: 'test_value'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not modify values when AI returns proper objects', () => {
|
|
58
|
+
// Create a filter schema with a text field
|
|
59
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
60
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
61
|
+
filters: [
|
|
62
|
+
{
|
|
63
|
+
id: 'test-filter-2',
|
|
64
|
+
label: 'Test Filter',
|
|
65
|
+
group: 'test',
|
|
66
|
+
expression: {
|
|
67
|
+
type: 'equals',
|
|
68
|
+
field: 'test_field',
|
|
69
|
+
value: {
|
|
70
|
+
type: 'customOperator',
|
|
71
|
+
operators: [
|
|
72
|
+
{ label: 'equals', value: '_eq' },
|
|
73
|
+
{ label: 'not equals', value: '_neq' }
|
|
74
|
+
],
|
|
75
|
+
valueControl: { type: 'text' }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
aiGenerated: false
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Build initial empty state
|
|
84
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
85
|
+
|
|
86
|
+
// Simulate AI returning a proper object
|
|
87
|
+
const aiState = {
|
|
88
|
+
type: 'leaf',
|
|
89
|
+
field: 'test_field',
|
|
90
|
+
value: {
|
|
91
|
+
operator: '_neq',
|
|
92
|
+
value: 'test_value'
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Apply our merge function
|
|
97
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
98
|
+
|
|
99
|
+
// The result should preserve the original object
|
|
100
|
+
expect(result.type).toBe('leaf');
|
|
101
|
+
if (result.type === 'leaf') {
|
|
102
|
+
expect(result.value).toEqual({
|
|
103
|
+
operator: '_neq',
|
|
104
|
+
value: 'test_value'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not modify non-customOperator values', () => {
|
|
110
|
+
// Create a filter schema with a regular text filter
|
|
111
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
112
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
113
|
+
filters: [
|
|
114
|
+
{
|
|
115
|
+
id: 'test-filter-3',
|
|
116
|
+
label: 'Test Filter',
|
|
117
|
+
group: 'test',
|
|
118
|
+
expression: {
|
|
119
|
+
type: 'equals',
|
|
120
|
+
field: 'test_field',
|
|
121
|
+
value: {
|
|
122
|
+
type: 'text'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
aiGenerated: false
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Build initial empty state
|
|
131
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
132
|
+
|
|
133
|
+
// Simulate AI returning a string value for a text filter
|
|
134
|
+
const aiState = {
|
|
135
|
+
type: 'leaf',
|
|
136
|
+
field: 'test_field',
|
|
137
|
+
value: 'test_value'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Apply our merge function
|
|
141
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
142
|
+
|
|
143
|
+
// The result should preserve the original string value
|
|
144
|
+
expect(result.type).toBe('leaf');
|
|
145
|
+
if (result.type === 'leaf') {
|
|
146
|
+
expect(result.value).toBe('test_value');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should map NOT wrapped customOperator to not-equals operator', () => {
|
|
151
|
+
// Create a filter schema with a customOperator that has not-equals
|
|
152
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
153
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
154
|
+
filters: [
|
|
155
|
+
{
|
|
156
|
+
id: 'test-filter-not',
|
|
157
|
+
label: 'Test Filter',
|
|
158
|
+
group: 'test',
|
|
159
|
+
expression: {
|
|
160
|
+
type: 'equals',
|
|
161
|
+
field: 'test',
|
|
162
|
+
value: {
|
|
163
|
+
type: 'customOperator',
|
|
164
|
+
operators: [
|
|
165
|
+
{ label: 'Equals', value: '_eq' },
|
|
166
|
+
{ label: 'Not Equals', value: '_neq' }
|
|
167
|
+
],
|
|
168
|
+
valueControl: { type: 'text' }
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
aiGenerated: false
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Build empty state
|
|
177
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
178
|
+
|
|
179
|
+
// AI returns a NOT wrapped around a leaf with string value
|
|
180
|
+
const aiState = {
|
|
181
|
+
type: 'not',
|
|
182
|
+
child: {
|
|
183
|
+
type: 'leaf',
|
|
184
|
+
field: 'test',
|
|
185
|
+
value: 'test_value'
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Apply our merge function
|
|
190
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
191
|
+
|
|
192
|
+
// The result should map to not-equals operator
|
|
193
|
+
expect(result.type).toBe('leaf');
|
|
194
|
+
if (result.type === 'leaf') {
|
|
195
|
+
expect(result.value).toEqual({
|
|
196
|
+
operator: '_neq',
|
|
197
|
+
value: 'test_value'
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should collapse OR children into array for in filter', () => {
|
|
203
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
204
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
205
|
+
filters: [
|
|
206
|
+
{
|
|
207
|
+
id: 'in-filter',
|
|
208
|
+
label: 'In Filter',
|
|
209
|
+
group: 'test',
|
|
210
|
+
expression: {
|
|
211
|
+
type: 'in',
|
|
212
|
+
field: 'category',
|
|
213
|
+
value: { type: 'text' }
|
|
214
|
+
},
|
|
215
|
+
aiGenerated: false
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
221
|
+
const aiState = {
|
|
222
|
+
type: 'or',
|
|
223
|
+
children: [
|
|
224
|
+
{ type: 'leaf', field: 'category', value: 'A' },
|
|
225
|
+
{ type: 'leaf', field: 'category', value: 'B' },
|
|
226
|
+
{ type: 'leaf', field: 'category', value: 'A' } // duplicate to test uniqueness
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
231
|
+
expect(result.type).toBe('leaf');
|
|
232
|
+
if (result.type === 'leaf') {
|
|
233
|
+
expect(result.value).toEqual(['A', 'B']);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should collapse OR children into array for notIn filter', () => {
|
|
238
|
+
const filterSchema: FilterSchemasAndGroups = {
|
|
239
|
+
groups: [{ name: 'test', label: 'Test Group' }],
|
|
240
|
+
filters: [
|
|
241
|
+
{
|
|
242
|
+
id: 'nin-filter',
|
|
243
|
+
label: 'Not In Filter',
|
|
244
|
+
group: 'test',
|
|
245
|
+
expression: {
|
|
246
|
+
type: 'notIn',
|
|
247
|
+
field: 'status',
|
|
248
|
+
value: { type: 'text' }
|
|
249
|
+
},
|
|
250
|
+
aiGenerated: false
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const emptyState = buildInitialFormState(filterSchema.filters[0].expression);
|
|
256
|
+
const aiState = {
|
|
257
|
+
type: 'or',
|
|
258
|
+
children: [
|
|
259
|
+
{ type: 'leaf', field: 'status', value: 'NEW' },
|
|
260
|
+
{ type: 'leaf', field: 'status', value: 'ARCHIVED' }
|
|
261
|
+
]
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const result = mergeFilterFormState(filterSchema.filters[0].expression, emptyState, aiState);
|
|
265
|
+
expect(result.type).toBe('leaf');
|
|
266
|
+
if (result.type === 'leaf') {
|
|
267
|
+
expect(result.value).toEqual(['NEW', 'ARCHIVED']);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|