@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,353 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Tag } from 'primereact/tag';
|
|
3
|
+
import { CellRenderer } from './column-definition';
|
|
4
|
+
import { FlexRow, FlexColumn, DateTime } from './cell-renderer-components/LayoutHelpers';
|
|
5
|
+
import { CurrencyAmount } from './cell-renderer-components/CurrencyAmount';
|
|
6
|
+
import { Mapping } from './cell-renderer-components/Mapping';
|
|
7
|
+
import { Link } from './cell-renderer-components/Link';
|
|
8
|
+
|
|
9
|
+
describe('Cell Renderer Components', () => {
|
|
10
|
+
it('should provide Badge component to cell renderers', () => {
|
|
11
|
+
// Create a test cell renderer that uses the Badge component
|
|
12
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
13
|
+
const { Badge } = components;
|
|
14
|
+
return createElement(Badge, {
|
|
15
|
+
value: `Test: ${data.value}`,
|
|
16
|
+
severity: 'success' as any,
|
|
17
|
+
style: { fontSize: '.8rem' }
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Create mock props with the components property
|
|
22
|
+
const mockProps = {
|
|
23
|
+
data: { value: 'Hello' },
|
|
24
|
+
setFilterState: jest.fn(),
|
|
25
|
+
applyFilters: jest.fn(),
|
|
26
|
+
updateFilterById: jest.fn(),
|
|
27
|
+
createElement: React.createElement,
|
|
28
|
+
components: {
|
|
29
|
+
Badge: Tag,
|
|
30
|
+
FlexRow,
|
|
31
|
+
FlexColumn,
|
|
32
|
+
Mapping,
|
|
33
|
+
DateTime,
|
|
34
|
+
CurrencyAmount,
|
|
35
|
+
Link
|
|
36
|
+
},
|
|
37
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Test that the cell renderer can access and use the Badge component
|
|
41
|
+
expect(() => {
|
|
42
|
+
const result = testCellRenderer(mockProps);
|
|
43
|
+
expect(result).toBeDefined();
|
|
44
|
+
}).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should provide the correct Badge component type', () => {
|
|
48
|
+
const testCellRenderer: CellRenderer = ({ components }) => {
|
|
49
|
+
const { Badge } = components;
|
|
50
|
+
// Verify Badge is the PrimeReact Tag component
|
|
51
|
+
expect(Badge).toBe(Tag);
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockProps = {
|
|
56
|
+
data: {},
|
|
57
|
+
setFilterState: jest.fn(),
|
|
58
|
+
applyFilters: jest.fn(),
|
|
59
|
+
updateFilterById: jest.fn(),
|
|
60
|
+
createElement: React.createElement,
|
|
61
|
+
components: {
|
|
62
|
+
Badge: Tag,
|
|
63
|
+
FlexRow,
|
|
64
|
+
FlexColumn,
|
|
65
|
+
Mapping,
|
|
66
|
+
DateTime,
|
|
67
|
+
CurrencyAmount,
|
|
68
|
+
Link
|
|
69
|
+
},
|
|
70
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
testCellRenderer(mockProps);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should allow creating Badge elements with typical PrimeReact Tag props', () => {
|
|
77
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
78
|
+
const { Badge } = components;
|
|
79
|
+
|
|
80
|
+
// Create a Badge using React.createElement (similar to JSX)
|
|
81
|
+
return createElement(Badge, {
|
|
82
|
+
value: data.status,
|
|
83
|
+
severity: 'warning' as any,
|
|
84
|
+
style: { fontSize: '0.8rem', padding: '0.3em 1em' }
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const mockProps = {
|
|
89
|
+
data: { status: 'Pending' },
|
|
90
|
+
setFilterState: jest.fn(),
|
|
91
|
+
applyFilters: jest.fn(),
|
|
92
|
+
updateFilterById: jest.fn(),
|
|
93
|
+
createElement: React.createElement,
|
|
94
|
+
components: {
|
|
95
|
+
Badge: Tag,
|
|
96
|
+
FlexRow,
|
|
97
|
+
FlexColumn,
|
|
98
|
+
Mapping,
|
|
99
|
+
DateTime,
|
|
100
|
+
CurrencyAmount,
|
|
101
|
+
Link
|
|
102
|
+
},
|
|
103
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(() => {
|
|
107
|
+
const result = testCellRenderer(mockProps);
|
|
108
|
+
expect(result).toBeDefined();
|
|
109
|
+
}).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should provide FlexRow and FlexColumn components to cell renderers', () => {
|
|
113
|
+
// Create a test cell renderer that uses FlexRow and FlexColumn components
|
|
114
|
+
const testCellRenderer: CellRenderer = ({ components, createElement }) => {
|
|
115
|
+
const { FlexRow, FlexColumn } = components;
|
|
116
|
+
return createElement(FlexRow, {
|
|
117
|
+
children: [
|
|
118
|
+
createElement(FlexColumn, { children: 'Vertical Layout' }),
|
|
119
|
+
createElement(FlexColumn, { children: 'Another Column' })
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const mockProps = {
|
|
125
|
+
data: { value: 'Layout Test' },
|
|
126
|
+
setFilterState: jest.fn(),
|
|
127
|
+
applyFilters: jest.fn(),
|
|
128
|
+
updateFilterById: jest.fn(),
|
|
129
|
+
createElement: React.createElement,
|
|
130
|
+
components: {
|
|
131
|
+
Badge: Tag,
|
|
132
|
+
FlexRow,
|
|
133
|
+
FlexColumn,
|
|
134
|
+
Mapping,
|
|
135
|
+
DateTime,
|
|
136
|
+
CurrencyAmount,
|
|
137
|
+
Link
|
|
138
|
+
},
|
|
139
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Test that the cell renderer can access and use FlexRow/FlexColumn components
|
|
143
|
+
expect(() => {
|
|
144
|
+
const result = testCellRenderer(mockProps);
|
|
145
|
+
expect(result).toBeDefined();
|
|
146
|
+
}).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should support flex-wrap property on FlexRow component', () => {
|
|
150
|
+
// Create a test cell renderer that uses FlexRow with wrap property
|
|
151
|
+
const testCellRenderer: CellRenderer = ({ components, createElement }) => {
|
|
152
|
+
const { FlexRow } = components;
|
|
153
|
+
return createElement(FlexRow, {
|
|
154
|
+
wrap: true,
|
|
155
|
+
children: ['Item 1', 'Item 2', 'Item 3']
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const mockProps = {
|
|
160
|
+
data: { value: 'Wrap Test' },
|
|
161
|
+
setFilterState: jest.fn(),
|
|
162
|
+
applyFilters: jest.fn(),
|
|
163
|
+
updateFilterById: jest.fn(),
|
|
164
|
+
createElement: React.createElement,
|
|
165
|
+
components: {
|
|
166
|
+
Badge: Tag,
|
|
167
|
+
FlexRow,
|
|
168
|
+
FlexColumn,
|
|
169
|
+
Mapping,
|
|
170
|
+
DateTime,
|
|
171
|
+
CurrencyAmount,
|
|
172
|
+
Link
|
|
173
|
+
},
|
|
174
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Test that FlexRow can handle wrap property
|
|
178
|
+
expect(() => {
|
|
179
|
+
const result = testCellRenderer(mockProps);
|
|
180
|
+
expect(result).toBeDefined();
|
|
181
|
+
}).not.toThrow();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should support different wrap values on FlexRow component', () => {
|
|
185
|
+
const wrapValues = ['wrap', 'nowrap', 'wrap-reverse'];
|
|
186
|
+
|
|
187
|
+
wrapValues.forEach(wrapValue => {
|
|
188
|
+
const testCellRenderer: CellRenderer = ({ components, createElement }) => {
|
|
189
|
+
const { FlexRow } = components;
|
|
190
|
+
return createElement(FlexRow, {
|
|
191
|
+
wrap: wrapValue,
|
|
192
|
+
children: ['Test Item']
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const mockProps = {
|
|
197
|
+
data: { value: `Wrap Test ${wrapValue}` },
|
|
198
|
+
setFilterState: jest.fn(),
|
|
199
|
+
applyFilters: jest.fn(),
|
|
200
|
+
updateFilterById: jest.fn(),
|
|
201
|
+
createElement: React.createElement,
|
|
202
|
+
components: {
|
|
203
|
+
Badge: Tag,
|
|
204
|
+
FlexRow,
|
|
205
|
+
FlexColumn,
|
|
206
|
+
Mapping,
|
|
207
|
+
DateTime,
|
|
208
|
+
CurrencyAmount,
|
|
209
|
+
Link
|
|
210
|
+
},
|
|
211
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
expect(() => {
|
|
215
|
+
const result = testCellRenderer(mockProps);
|
|
216
|
+
expect(result).toBeDefined();
|
|
217
|
+
}).not.toThrow();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should provide Mapping component to cell renderers', () => {
|
|
222
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
223
|
+
const { Mapping } = components;
|
|
224
|
+
const statusMap = { 'pending': 'Pending', 'approved': 'Approved', 'rejected': 'Rejected' };
|
|
225
|
+
return createElement(Mapping, { value: data.status, map: statusMap });
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const mockProps = {
|
|
229
|
+
data: { status: 'pending' },
|
|
230
|
+
setFilterState: jest.fn(),
|
|
231
|
+
applyFilters: jest.fn(),
|
|
232
|
+
updateFilterById: jest.fn(),
|
|
233
|
+
createElement: React.createElement,
|
|
234
|
+
components: {
|
|
235
|
+
Badge: Tag,
|
|
236
|
+
FlexRow,
|
|
237
|
+
FlexColumn,
|
|
238
|
+
Mapping,
|
|
239
|
+
DateTime,
|
|
240
|
+
CurrencyAmount,
|
|
241
|
+
Link
|
|
242
|
+
},
|
|
243
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Test that the cell renderer can access and use the Mapping component
|
|
247
|
+
expect(() => {
|
|
248
|
+
const result = testCellRenderer(mockProps);
|
|
249
|
+
expect(result).toBeDefined();
|
|
250
|
+
}).not.toThrow();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should provide DateTime component to cell renderers', () => {
|
|
254
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
255
|
+
const { DateTime } = components;
|
|
256
|
+
return createElement(DateTime, { date: data.createdAt, options: { dateStyle: 'short' } });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const mockProps = {
|
|
260
|
+
data: { createdAt: '2023-01-01T12:00:00Z' },
|
|
261
|
+
setFilterState: jest.fn(),
|
|
262
|
+
applyFilters: jest.fn(),
|
|
263
|
+
updateFilterById: jest.fn(),
|
|
264
|
+
createElement: React.createElement,
|
|
265
|
+
components: {
|
|
266
|
+
Badge: Tag,
|
|
267
|
+
FlexRow,
|
|
268
|
+
FlexColumn,
|
|
269
|
+
Mapping,
|
|
270
|
+
DateTime,
|
|
271
|
+
CurrencyAmount,
|
|
272
|
+
Link
|
|
273
|
+
},
|
|
274
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Test that the cell renderer can access and use the DateTime component
|
|
278
|
+
expect(() => {
|
|
279
|
+
const result = testCellRenderer(mockProps);
|
|
280
|
+
expect(result).toBeDefined();
|
|
281
|
+
}).not.toThrow();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should provide CurrencyAmount component to cell renderers', () => {
|
|
285
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
286
|
+
const { CurrencyAmount } = components;
|
|
287
|
+
return createElement(CurrencyAmount, {
|
|
288
|
+
amount: data.amount,
|
|
289
|
+
currency: data.currency || 'USD',
|
|
290
|
+
options: { minimumFractionDigits: 2 }
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const mockProps = {
|
|
295
|
+
data: { amount: 12345, currency: 'EUR' },
|
|
296
|
+
setFilterState: jest.fn(),
|
|
297
|
+
applyFilters: jest.fn(),
|
|
298
|
+
updateFilterById: jest.fn(),
|
|
299
|
+
createElement: React.createElement,
|
|
300
|
+
components: {
|
|
301
|
+
Badge: Tag,
|
|
302
|
+
FlexRow,
|
|
303
|
+
FlexColumn,
|
|
304
|
+
Mapping,
|
|
305
|
+
DateTime,
|
|
306
|
+
CurrencyAmount,
|
|
307
|
+
Link
|
|
308
|
+
},
|
|
309
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Test that the cell renderer can access and use the CurrencyAmount component
|
|
313
|
+
expect(() => {
|
|
314
|
+
const result = testCellRenderer(mockProps);
|
|
315
|
+
expect(result).toBeDefined();
|
|
316
|
+
}).not.toThrow();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should provide Link component to cell renderers', () => {
|
|
320
|
+
const testCellRenderer: CellRenderer = ({ data, components, createElement }) => {
|
|
321
|
+
const { Link } = components;
|
|
322
|
+
return createElement(Link, {
|
|
323
|
+
text: data.linkText || 'Click here',
|
|
324
|
+
href: data.url || '#',
|
|
325
|
+
className: 'custom-link-class'
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const mockProps = {
|
|
330
|
+
data: { linkText: 'Visit Example', url: 'https://example.com' },
|
|
331
|
+
setFilterState: jest.fn(),
|
|
332
|
+
applyFilters: jest.fn(),
|
|
333
|
+
updateFilterById: jest.fn(),
|
|
334
|
+
createElement: React.createElement,
|
|
335
|
+
components: {
|
|
336
|
+
Badge: Tag,
|
|
337
|
+
FlexRow,
|
|
338
|
+
FlexColumn,
|
|
339
|
+
Mapping,
|
|
340
|
+
DateTime,
|
|
341
|
+
CurrencyAmount,
|
|
342
|
+
Link
|
|
343
|
+
},
|
|
344
|
+
currency: { majorToMinor: jest.fn(), minorToMajor: jest.fn() }
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Test that the cell renderer can access and use the Link component
|
|
348
|
+
expect(() => {
|
|
349
|
+
const result = testCellRenderer(mockProps);
|
|
350
|
+
expect(result).toBeDefined();
|
|
351
|
+
}).not.toThrow();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ReactNode, createElement } from "react";
|
|
2
|
+
import { FlexRow, FlexColumn, DateTime } from "./cell-renderer-components/LayoutHelpers";
|
|
3
|
+
import { CurrencyAmount } from './cell-renderer-components/CurrencyAmount';
|
|
4
|
+
import { majorToMinor, minorToMajor } from './currency';
|
|
5
|
+
import { Mapping } from "./cell-renderer-components/Mapping";
|
|
6
|
+
import { Link } from "./cell-renderer-components/Link";
|
|
7
|
+
import { Tag } from 'primereact/tag';
|
|
8
|
+
import { FilterState } from "./state";
|
|
9
|
+
import { FilterId } from "./filters";
|
|
10
|
+
|
|
11
|
+
export type CellRendererProps = {
|
|
12
|
+
data: Record<string, any>;
|
|
13
|
+
setFilterState: (updater: (currentState: FilterState) => FilterState) => void; // Function to update filter state
|
|
14
|
+
applyFilters: () => void; // Function to trigger data fetch with current filter state
|
|
15
|
+
updateFilterById: (filterId: FilterId, updater: (currentValue: any) => any) => void; // Narrow helper to update a specific filter by id
|
|
16
|
+
createElement: typeof createElement; // React createElement function
|
|
17
|
+
components: {
|
|
18
|
+
Badge: typeof Tag; // PrimeReact Tag component exposed as Badge for user convenience
|
|
19
|
+
FlexRow: typeof FlexRow; // Horizontal layout component
|
|
20
|
+
FlexColumn: typeof FlexColumn; // Vertical layout component
|
|
21
|
+
Mapping: typeof Mapping; // Generic mapping component for displaying mapped values
|
|
22
|
+
DateTime: typeof DateTime; // Date formatting component
|
|
23
|
+
CurrencyAmount: typeof CurrencyAmount; // Currency formatting component
|
|
24
|
+
Link: typeof Link; // Link component for creating hyperlinks
|
|
25
|
+
};
|
|
26
|
+
currency: {
|
|
27
|
+
minorToMajor: typeof minorToMajor;
|
|
28
|
+
majorToMinor: typeof majorToMinor;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CellRenderer = (props: CellRendererProps) => ReactNode;
|
|
33
|
+
|
|
34
|
+
export type OrderByConfig = {
|
|
35
|
+
key: string; // data key to order by
|
|
36
|
+
direction: 'ASC' | 'DESC';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Tagged ADT for QueryConfig: either a path or a config group
|
|
40
|
+
export type Field = {
|
|
41
|
+
type: 'field';
|
|
42
|
+
path: string; // dot-separated data path
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type QueryConfig = {
|
|
46
|
+
field: string
|
|
47
|
+
path?: string; // path for querying inside JSON columns
|
|
48
|
+
orderBy?: OrderByConfig | OrderByConfig[];
|
|
49
|
+
limit?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type QueryConfigs = {
|
|
53
|
+
type: 'queryConfigs'
|
|
54
|
+
configs: QueryConfig[]
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Field alias support - wraps any FieldQuery with an alias name
|
|
58
|
+
export type FieldAlias = {
|
|
59
|
+
type: 'fieldAlias';
|
|
60
|
+
alias: string; // the alias name to use in GraphQL
|
|
61
|
+
field: FieldQuery; // the underlying field query
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type FieldQuery = Field | QueryConfigs | FieldAlias;
|
|
65
|
+
|
|
66
|
+
// Helper to create a Field
|
|
67
|
+
export function field(path: string): FieldQuery {
|
|
68
|
+
return { type: 'field', path };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper to create QueryConfigs
|
|
72
|
+
export function queryConfigs(configs: QueryConfig[]): FieldQuery {
|
|
73
|
+
return { type: 'queryConfigs', configs };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Helper to create a FieldAlias
|
|
77
|
+
export function fieldAlias(alias: string, fieldQuery: FieldQuery): FieldQuery {
|
|
78
|
+
return { type: 'fieldAlias', alias, field: fieldQuery };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ColumnDefinition = {
|
|
82
|
+
data: FieldQuery[];
|
|
83
|
+
name: string; // column display name
|
|
84
|
+
cellRenderer: CellRenderer;
|
|
85
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { majorToMinor, minorToMajor, getCurrencyMajorUnitScale } from './currency';
|
|
2
|
+
|
|
3
|
+
describe('currency unit conversion', () => {
|
|
4
|
+
it('converts USD major to minor and back (2 decimals)', () => {
|
|
5
|
+
const scale = getCurrencyMajorUnitScale('USD');
|
|
6
|
+
expect(scale).toBe(100);
|
|
7
|
+
const minor = majorToMinor(12.34, 'USD');
|
|
8
|
+
expect(minor).toBe(1234);
|
|
9
|
+
const major = minorToMajor(minor, 'USD');
|
|
10
|
+
expect(major).toBeCloseTo(12.34);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('handles rounding correctly (USD)', () => {
|
|
14
|
+
// 0.015 * 100 = 1.5 -> rounds to 2 cents
|
|
15
|
+
const minor = majorToMinor(0.015, 'USD');
|
|
16
|
+
expect(minor).toBe(2);
|
|
17
|
+
expect(minorToMajor(minor, 'USD')).toBeCloseTo(0.02);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('supports zero-decimal currency (JPY)', () => {
|
|
21
|
+
const scale = getCurrencyMajorUnitScale('JPY');
|
|
22
|
+
expect(scale).toBe(1);
|
|
23
|
+
const minor = majorToMinor(1234, 'JPY');
|
|
24
|
+
expect(minor).toBe(1234);
|
|
25
|
+
const major = minorToMajor(minor, 'JPY');
|
|
26
|
+
expect(major).toBe(1234);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('supports three-decimal currency (KWD)', () => {
|
|
30
|
+
const scale = getCurrencyMajorUnitScale('KWD');
|
|
31
|
+
expect(scale).toBe(1000);
|
|
32
|
+
const minor = majorToMinor(1.234, 'KWD');
|
|
33
|
+
expect(minor).toBe(1234);
|
|
34
|
+
const major = minorToMajor(minor, 'KWD');
|
|
35
|
+
expect(major).toBeCloseTo(1.234);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('throws on non-finite major', () => {
|
|
39
|
+
expect(() => majorToMinor(Number.NaN, 'USD')).toThrow();
|
|
40
|
+
expect(() => majorToMinor(Number.POSITIVE_INFINITY, 'USD')).toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws on non-integer minor', () => {
|
|
44
|
+
expect(() => minorToMajor(12.34 as unknown as number, 'USD')).toThrow();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Currency helper utilities extracted from the former Currency component file.
|
|
2
|
+
// These functions provide locale resolution, fraction digit discovery, and
|
|
3
|
+
// conversion helpers between major (display) and minor (integer) currency units.
|
|
4
|
+
|
|
5
|
+
// Cache for currency fraction digit lookups
|
|
6
|
+
const currencyFractionDigitCache: Map<string, number> = new Map();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a locale to use for currency formatting.
|
|
10
|
+
* Priority:
|
|
11
|
+
* 1. Explicitly provided locale argument
|
|
12
|
+
* 2. navigator.language (browser default)
|
|
13
|
+
* 3. First entry in navigator.languages
|
|
14
|
+
* 4. undefined (lets Intl use implementation default)
|
|
15
|
+
*/
|
|
16
|
+
export function resolveLocale(locale?: string): string | undefined {
|
|
17
|
+
if (locale) return locale;
|
|
18
|
+
if (typeof navigator !== 'undefined') {
|
|
19
|
+
const lang = (navigator as any).language || (Array.isArray((navigator as any).languages) && (navigator as any).languages[0]);
|
|
20
|
+
return typeof lang === 'string' ? lang : undefined;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getCurrencyFractionDigits(currency: string, locale: string | undefined = undefined): number {
|
|
26
|
+
const code = currency.toUpperCase();
|
|
27
|
+
if (currencyFractionDigitCache.has(code)) return currencyFractionDigitCache.get(code)!;
|
|
28
|
+
try {
|
|
29
|
+
const { maximumFractionDigits } = new Intl.NumberFormat(locale, { style: 'currency', currency: code }).resolvedOptions();
|
|
30
|
+
const value: number = typeof maximumFractionDigits === 'number' ? maximumFractionDigits : 2;
|
|
31
|
+
currencyFractionDigitCache.set(code, value);
|
|
32
|
+
return value;
|
|
33
|
+
} catch {
|
|
34
|
+
currencyFractionDigitCache.set(code, 2);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCurrencyMajorUnitScale(currency: string, locale: string | undefined = undefined): number {
|
|
40
|
+
return Math.pow(10, getCurrencyFractionDigits(currency, locale));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function majorToMinor(major: number, currency: string, locale?: string): number {
|
|
44
|
+
if (typeof major !== 'number' || !isFinite(major)) throw new Error('major must be a finite number');
|
|
45
|
+
const scale = getCurrencyMajorUnitScale(currency, locale);
|
|
46
|
+
// Use rounding to nearest minor unit to avoid FP drift
|
|
47
|
+
return Math.round(major * scale);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function minorToMajor(minor: number, currency: string, locale?: string): number {
|
|
51
|
+
if (typeof minor !== 'number' || !Number.isInteger(minor)) throw new Error('minor must be an integer number of minor units');
|
|
52
|
+
const scale = getCurrencyMajorUnitScale(currency, locale);
|
|
53
|
+
return minor / scale;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
resolveLocale,
|
|
58
|
+
getCurrencyFractionDigits,
|
|
59
|
+
getCurrencyMajorUnitScale,
|
|
60
|
+
majorToMinor,
|
|
61
|
+
minorToMajor
|
|
62
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fetchData } from './data';
|
|
2
|
+
import { View } from './view';
|
|
3
|
+
import { ColumnDefinition } from './column-definition';
|
|
4
|
+
import { FilterSchemasAndGroups } from './filters';
|
|
5
|
+
|
|
6
|
+
// We only test merging logic; GraphQL call will be mocked.
|
|
7
|
+
|
|
8
|
+
describe('fetchData staticConditions merging', () => {
|
|
9
|
+
const mockClient: any = {
|
|
10
|
+
request: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
let capturedVariables: any = null;
|
|
13
|
+
const requestSpy = mockClient.request.mockImplementation((_query: string, _vars: any) => {
|
|
14
|
+
capturedVariables = _vars;
|
|
15
|
+
return Promise.resolve({ testCollection: [] });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const view: View = {
|
|
19
|
+
title: 'Test',
|
|
20
|
+
id: 'test',
|
|
21
|
+
collectionName: 'testCollection',
|
|
22
|
+
columnDefinitions: [{ data: [{ type: 'field', path: 'id' }] } as ColumnDefinition],
|
|
23
|
+
filterSchema: { groups: [], filters: [] } as FilterSchemasAndGroups,
|
|
24
|
+
boolExpType: 'BoolExp',
|
|
25
|
+
orderByType: '[OrderBy!]',
|
|
26
|
+
paginationKey: 'id',
|
|
27
|
+
staticConditions: [{ status: { _eq: 'ACTIVE' } }, { deleted_at: { _is_null: true } }]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('merges staticConditions when no user filters', async () => {
|
|
35
|
+
const result = await fetchData({ client: mockClient, view, query: 'query', filterState: new Map(), rows: 10, cursor: null });
|
|
36
|
+
expect(requestSpy).toHaveBeenCalled();
|
|
37
|
+
expect(capturedVariables.conditions).toEqual({ _and: [{}, { status: { _eq: 'ACTIVE' } }, { deleted_at: { _is_null: true } }] });
|
|
38
|
+
expect(result.rows).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('merges pagination condition efficiently by appending', async () => {
|
|
42
|
+
await fetchData({ client: mockClient, view, query: 'query', filterState: new Map(), rows: 10, cursor: 50 });
|
|
43
|
+
expect(capturedVariables.conditions._and.length).toBe(4); // {}, two static, pagination
|
|
44
|
+
expect(capturedVariables.conditions._and[3]).toEqual({ id: { _lt: 50 } });
|
|
45
|
+
});
|
|
46
|
+
});
|