@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.
Files changed (97) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/copilot-instructions.md +64 -0
  3. package/.github/workflows/ci.yml +51 -0
  4. package/.husky/pre-commit +8 -0
  5. package/README.md +63 -0
  6. package/docs/api/README.md +32 -0
  7. package/docs/api/cell-renderers.md +121 -0
  8. package/docs/api/no-rows-component.md +71 -0
  9. package/docs/api/runtime.md +78 -0
  10. package/e2e/app.spec.ts +6 -0
  11. package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
  12. package/e2e/filter-sharing.spec.ts +113 -0
  13. package/e2e/filter-url-persistence.spec.ts +36 -0
  14. package/e2e/graphqlMock.ts +144 -0
  15. package/e2e/multi-field-filters.spec.ts +95 -0
  16. package/e2e/pagination.spec.ts +38 -0
  17. package/e2e/payment-request-email-filter.spec.ts +67 -0
  18. package/e2e/save-filter-splitbutton.spec.ts +68 -0
  19. package/e2e/simple-view-email-filter.spec.ts +67 -0
  20. package/e2e/simple-view-transforms.spec.ts +171 -0
  21. package/e2e/simple-view.spec.ts +104 -0
  22. package/e2e/transform-regression.spec.ts +108 -0
  23. package/eslint.config.js +30 -0
  24. package/index.html +17 -0
  25. package/jest.config.js +10 -0
  26. package/package.json +45 -0
  27. package/playwright.config.ts +54 -0
  28. package/public/vite.svg +1 -0
  29. package/src/App.externalRuntime.test.ts +190 -0
  30. package/src/App.tsx +540 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/components/AIAssistantForm.tsx +241 -0
  33. package/src/components/FilterForm.test.ts +82 -0
  34. package/src/components/FilterForm.tsx +375 -0
  35. package/src/components/PhoneNumberFilter.tsx +102 -0
  36. package/src/components/SavedFilterList.tsx +181 -0
  37. package/src/components/SpeechInput.tsx +67 -0
  38. package/src/components/Table.tsx +119 -0
  39. package/src/components/TablePagination.tsx +40 -0
  40. package/src/components/aiAssistant.test.ts +270 -0
  41. package/src/components/aiAssistant.ts +291 -0
  42. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
  43. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
  44. package/src/framework/cell-renderer-components/Link.tsx +28 -0
  45. package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
  46. package/src/framework/cell-renderer-components.test.ts +353 -0
  47. package/src/framework/column-definition.tsx +85 -0
  48. package/src/framework/currency.test.ts +46 -0
  49. package/src/framework/currency.ts +62 -0
  50. package/src/framework/data.staticConditions.test.ts +46 -0
  51. package/src/framework/data.test.ts +167 -0
  52. package/src/framework/data.ts +162 -0
  53. package/src/framework/filter-form-state.test.ts +189 -0
  54. package/src/framework/filter-form-state.ts +185 -0
  55. package/src/framework/filter-sharing.test.ts +135 -0
  56. package/src/framework/filter-sharing.ts +118 -0
  57. package/src/framework/filters.ts +194 -0
  58. package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
  59. package/src/framework/graphql.paginationKey.test.ts +29 -0
  60. package/src/framework/graphql.test.ts +286 -0
  61. package/src/framework/graphql.ts +462 -0
  62. package/src/framework/native-runtime/index.tsx +33 -0
  63. package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
  64. package/src/framework/runtime-reference.test.ts +172 -0
  65. package/src/framework/runtime.ts +15 -0
  66. package/src/framework/saved-filters.test.ts +422 -0
  67. package/src/framework/saved-filters.ts +293 -0
  68. package/src/framework/state.test.ts +86 -0
  69. package/src/framework/state.ts +148 -0
  70. package/src/framework/transform.test.ts +51 -0
  71. package/src/framework/view-parser-initialvalues.test.ts +228 -0
  72. package/src/framework/view-parser.ts +714 -0
  73. package/src/framework/view.test.ts +1805 -0
  74. package/src/framework/view.ts +38 -0
  75. package/src/index.css +6 -0
  76. package/src/main.tsx +99 -0
  77. package/src/views/index.ts +12 -0
  78. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
  79. package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
  80. package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
  81. package/src/views/payment-requests/index.ts +1 -0
  82. package/src/views/payment-requests/runtime.tsx +145 -0
  83. package/src/views/payment-requests/view.json +692 -0
  84. package/src/views/payment-requests-initial-values.test.ts +73 -0
  85. package/src/views/request-log/index.ts +2 -0
  86. package/src/views/request-log/runtime.tsx +47 -0
  87. package/src/views/request-log/view.json +123 -0
  88. package/src/views/simple-test-view/index.ts +3 -0
  89. package/src/views/simple-test-view/runtime.tsx +85 -0
  90. package/src/views/simple-test-view/view.json +191 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tailwind.config.js +7 -0
  93. package/tsconfig.app.json +26 -0
  94. package/tsconfig.jest.json +6 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +24 -0
  97. package/vite.config.ts +11 -0
@@ -0,0 +1,293 @@
1
+ import { serializeFilterFormStateMap, parseFilterFormState } from './filter-form-state';
2
+ import { FilterSchemasAndGroups } from './filters';
3
+ import { FilterState } from './state';
4
+
5
+ /**
6
+ * Format revisions for saved filters
7
+ */
8
+ export const OLD_ARRAY_FORMAT_REVISION = '2025-09-04T00:00:00.000Z';
9
+ export const CURRENT_FORMAT_REVISION = '2025-09-19T00:00:00.000Z';
10
+
11
+ /**
12
+ * Raw saved filter data as stored in localStorage - using unknown for type safety
13
+ */
14
+ export interface RawSavedFilter {
15
+ id: string;
16
+ name: string;
17
+ view: string;
18
+ state: unknown; // Serialized FilterState - could be object format or legacy array format
19
+ createdAt: string | Date; // Could be string from JSON or Date object
20
+ formatRevision?: string; // Optional for backwards compatibility
21
+ }
22
+
23
+ /**
24
+ * Parsed saved filter with properly typed state
25
+ */
26
+ export interface SavedFilter {
27
+ id: string;
28
+ name: string;
29
+ view: string;
30
+ state: FilterState; // Parsed FilterState as a Map
31
+ createdAt: Date;
32
+ formatRevision: string;
33
+ }
34
+
35
+ /**
36
+ * Interface for the saved filter manager
37
+ */
38
+ export interface SavedFilterManager {
39
+ loadFilters(viewName: string, schema: FilterSchemasAndGroups): SavedFilter[];
40
+ saveFilter(filter: Omit<SavedFilter, 'id' | 'createdAt' | 'formatRevision'>): SavedFilter;
41
+ updateFilter(filter: SavedFilter, updates: Partial<Pick<SavedFilter, 'name' | 'state'>>): SavedFilter | null;
42
+ deleteFilter(id: string): boolean;
43
+ }
44
+
45
+ const SAVED_FILTERS_KEY = 'dtvSavedFilters';
46
+ const LEGACY_SAVED_FILTERS_KEY = 'savedFilters';
47
+
48
+ /**
49
+ * Convert old array format to object format using schema order
50
+ */
51
+ function convertArrayToObject(state: unknown, schema: FilterSchemasAndGroups): Record<string, unknown> {
52
+ if (!Array.isArray(state)) {
53
+ console.warn('Expected array for conversion but got:', typeof state);
54
+ return {};
55
+ }
56
+
57
+ const objectState: Record<string, unknown> = {};
58
+
59
+ // Map array positions to filter IDs using schema order
60
+ state.forEach((filterState: unknown, index: number) => {
61
+ if (index < schema.filters.length) {
62
+ const filterId = schema.filters[index].id;
63
+ objectState[filterId] = filterState;
64
+ }
65
+ });
66
+
67
+ return objectState;
68
+ }
69
+
70
+ /**
71
+ * Create and return a SavedFilterManager instance
72
+ */
73
+ export function createSavedFilterManager(): SavedFilterManager {
74
+
75
+ /**
76
+ * Migrate data from legacy localStorage key to current key if needed
77
+ * Returns true if migration occurred, false otherwise
78
+ */
79
+ function migrateLegacyStorageKey(): boolean {
80
+ try {
81
+ // Check if current key already has data
82
+ if (localStorage.getItem(SAVED_FILTERS_KEY)) {
83
+ return false; // No migration needed
84
+ }
85
+
86
+ // Check if legacy key has data
87
+ const legacyRaw = localStorage.getItem(LEGACY_SAVED_FILTERS_KEY);
88
+ if (!legacyRaw) {
89
+ return false; // No legacy data to migrate
90
+ }
91
+
92
+ console.info('Found saved filters in legacy localStorage key, migrating...');
93
+
94
+ // Move data from legacy key to current key
95
+ localStorage.setItem(SAVED_FILTERS_KEY, legacyRaw);
96
+ localStorage.removeItem(LEGACY_SAVED_FILTERS_KEY);
97
+
98
+ // Parse to get count for logging
99
+ const parsed: unknown = JSON.parse(legacyRaw);
100
+ const count = Array.isArray(parsed) ? parsed.length : 0;
101
+ console.info(`Migrated ${count} filters from legacy localStorage key '${LEGACY_SAVED_FILTERS_KEY}' to '${SAVED_FILTERS_KEY}'`);
102
+
103
+ return true;
104
+ } catch (error) {
105
+ console.error('Failed to migrate legacy localStorage key:', error);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Load raw saved filters from localStorage with proper type safety
112
+ */
113
+ function loadRawSavedFilters(): RawSavedFilter[] {
114
+ try {
115
+ // First, handle legacy key migration
116
+ migrateLegacyStorageKey();
117
+
118
+ // Now load from the current key
119
+ const raw = localStorage.getItem(SAVED_FILTERS_KEY);
120
+ if (!raw) {
121
+ return [];
122
+ }
123
+
124
+ const parsed: unknown = JSON.parse(raw);
125
+ if (!Array.isArray(parsed)) {
126
+ return [];
127
+ }
128
+
129
+ return parsed.map((item: unknown) => {
130
+ // Type guard for item structure
131
+ if (typeof item !== 'object' || item === null) {
132
+ throw new Error('Invalid saved filter structure');
133
+ }
134
+
135
+ const rawItem = item as Record<string, unknown>;
136
+
137
+ return {
138
+ id: typeof rawItem.id === 'string' ? rawItem.id : crypto.randomUUID(),
139
+ name: typeof rawItem.name === 'string' ? rawItem.name : 'Unnamed Filter',
140
+ view: typeof rawItem.view === 'string' ? rawItem.view : '',
141
+ state: rawItem.state, // Keep as unknown for later parsing
142
+ createdAt: typeof rawItem.createdAt === 'string'
143
+ ? rawItem.createdAt
144
+ : new Date().toISOString(),
145
+ formatRevision: typeof rawItem.formatRevision === 'string'
146
+ ? rawItem.formatRevision
147
+ : OLD_ARRAY_FORMAT_REVISION // Default to old format for items without revision
148
+ } satisfies RawSavedFilter;
149
+ });
150
+ } catch (error) {
151
+ console.error('Failed to load saved filters from localStorage:', error);
152
+ return [];
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Load and parse saved filters for a specific view
158
+ */
159
+ function loadFilters(viewName: string, schema: FilterSchemasAndGroups): SavedFilter[] {
160
+ const allRawFilters = loadRawSavedFilters();
161
+ let hasMigrations = false;
162
+
163
+ // Process all raw filters for migration
164
+ const updatedAllRawFilters = allRawFilters.map((rawFilter) => {
165
+ // If this is old array format, convert to object format
166
+ if (rawFilter.formatRevision === OLD_ARRAY_FORMAT_REVISION) {
167
+ hasMigrations = true;
168
+ const objectState = convertArrayToObject(rawFilter.state, schema);
169
+ return {
170
+ ...rawFilter,
171
+ state: objectState,
172
+ formatRevision: CURRENT_FORMAT_REVISION
173
+ };
174
+ }
175
+
176
+ // Always ensure current revision
177
+ return {
178
+ ...rawFilter,
179
+ formatRevision: CURRENT_FORMAT_REVISION
180
+ };
181
+ });
182
+
183
+ // Filter for the specific view from the updated filters
184
+ const viewRawFilters = updatedAllRawFilters.filter(filter => filter.view === viewName);
185
+
186
+ // Parse the view-specific filters into SavedFilter format
187
+ const parsedFilters = viewRawFilters.map((rawFilter): SavedFilter => {
188
+ // Parse the object state into a Map
189
+ const parsedState = parseFilterFormState(rawFilter.state as Record<string, unknown>, schema);
190
+
191
+ return {
192
+ id: rawFilter.id,
193
+ name: rawFilter.name,
194
+ view: rawFilter.view,
195
+ state: parsedState,
196
+ createdAt: typeof rawFilter.createdAt === 'string'
197
+ ? new Date(rawFilter.createdAt)
198
+ : rawFilter.createdAt,
199
+ formatRevision: CURRENT_FORMAT_REVISION
200
+ };
201
+ });
202
+
203
+ // If we migrated anything, save all updated filters
204
+ if (hasMigrations) {
205
+ try {
206
+ localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(updatedAllRawFilters));
207
+ console.info(`Migrated filters from old array format to new object format`);
208
+ } catch (error) {
209
+ console.error('Failed to save migrated filters to localStorage:', error);
210
+ }
211
+ }
212
+
213
+ return parsedFilters;
214
+ }
215
+
216
+ /**
217
+ * Save a filter to localStorage
218
+ */
219
+ function saveFilter(filter: Omit<SavedFilter, 'id' | 'createdAt' | 'formatRevision'>): SavedFilter {
220
+ const savedFilter: SavedFilter = {
221
+ id: crypto.randomUUID(),
222
+ createdAt: new Date(),
223
+ formatRevision: CURRENT_FORMAT_REVISION,
224
+ ...filter
225
+ };
226
+
227
+ const existingFilters = loadRawSavedFilters();
228
+ const newRawFilter: RawSavedFilter = {
229
+ ...savedFilter,
230
+ createdAt: savedFilter.createdAt.toISOString(),
231
+ state: serializeFilterFormStateMap(savedFilter.state)
232
+ };
233
+
234
+ existingFilters.push(newRawFilter);
235
+ localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(existingFilters));
236
+ return savedFilter;
237
+ }
238
+
239
+ /**
240
+ * Update an existing filter
241
+ */
242
+ function updateFilter(filter: SavedFilter, updates: Partial<Pick<SavedFilter, 'name' | 'state'>>): SavedFilter | null {
243
+ const allFilters = loadRawSavedFilters();
244
+ const filterIndex = allFilters.findIndex((existingFilter: RawSavedFilter) => existingFilter.id === filter.id);
245
+
246
+ if (filterIndex === -1) {
247
+ return null;
248
+ }
249
+
250
+ const updatedFilter: SavedFilter = {
251
+ ...filter,
252
+ ...updates
253
+ };
254
+
255
+ const updatedRawFilter: RawSavedFilter = {
256
+ ...allFilters[filterIndex],
257
+ name: updatedFilter.name,
258
+ state: updates.state ? serializeFilterFormStateMap(updates.state) : allFilters[filterIndex].state
259
+ };
260
+
261
+ allFilters[filterIndex] = updatedRawFilter;
262
+ localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(allFilters));
263
+ return updatedFilter;
264
+ }
265
+
266
+ /**
267
+ * Delete a filter by ID
268
+ */
269
+ function deleteFilter(id: string): boolean {
270
+ const allFilters = loadRawSavedFilters();
271
+ const originalLength = allFilters.length;
272
+ const filteredFilters = allFilters.filter((filter: RawSavedFilter) => filter.id !== id);
273
+
274
+ if (filteredFilters.length === originalLength) {
275
+ return false; // Filter not found
276
+ }
277
+
278
+ localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(filteredFilters));
279
+ return true;
280
+ }
281
+
282
+ return {
283
+ loadFilters,
284
+ saveFilter,
285
+ updateFilter,
286
+ deleteFilter
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Default exported instance of the saved filter manager
292
+ */
293
+ export const savedFilterManager = createSavedFilterManager();
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { describe, it, expect } from '@jest/globals';
5
+ import { createDefaultAppState, setSelectedViewId, setDataRows, setFilterSchema, setFilterState, FilterState } from './state';
6
+ import { buildInitialFormState } from './state';
7
+ import { View } from './view';
8
+
9
+ // Mock view definitions
10
+ const mockViews: View[] = [
11
+ {
12
+ id: 'foo',
13
+ title: 'Foo',
14
+ filterSchema: {
15
+ groups: [{ name: 'default', label: 'Default' }],
16
+ filters: [
17
+ { id: 'filter-a', label: 'A', expression: { type: 'isNull', key: 'a', value: {} }, group: 'default' }
18
+ ]
19
+ },
20
+ columnDefinitions: [],
21
+ paginationKey: 'id',
22
+ } as any,
23
+ {
24
+ id: 'bar',
25
+ title: 'Bar',
26
+ filterSchema: {
27
+ groups: [{ name: 'default', label: 'Default' }],
28
+ filters: [
29
+ { id: 'filter-b', label: 'B', expression: { type: 'isNull', key: 'b', value: {} }, group: 'default' }
30
+ ]
31
+ },
32
+ columnDefinitions: [],
33
+ paginationKey: 'id',
34
+ } as any
35
+ ];
36
+
37
+ describe('AppState', () => {
38
+ it('creates default state with correct initial view and filter state', () => {
39
+ const state = createDefaultAppState(mockViews);
40
+ expect(state.selectedViewId).toBe('foo');
41
+ expect(state.views).toBe(mockViews);
42
+ expect(state.filterSchemasAndGroups).toEqual(mockViews[0].filterSchema);
43
+ expect(state.filterState).toEqual(
44
+ new Map(mockViews[0].filterSchema.filters.map(f => [f.id, buildInitialFormState(f.expression)]))
45
+ );
46
+ expect(state.data).toEqual({ rows: [], flattenedRows: [] });
47
+ });
48
+
49
+ it('setSelectedViewId updates selectedViewIndex, filterSchema, and filterState', () => {
50
+ let state = createDefaultAppState(mockViews);
51
+ state = setSelectedViewId(state, mockViews[1].id);
52
+ expect(state.selectedViewId).toBe('bar');
53
+ expect(state.filterSchemasAndGroups).toEqual(mockViews[1].filterSchema);
54
+ expect(state.filterState).toEqual(
55
+ new Map(mockViews[1].filterSchema.filters.map(f => [f.id, buildInitialFormState(f.expression)]))
56
+ );
57
+ });
58
+
59
+ it('setDataRows updates data and pagination', () => {
60
+ let state = createDefaultAppState(mockViews);
61
+ const data = { rows: [{ id: 1 }, { id: 2 }], flattenedRows: [[{ id: 1 }], [{ id: 2 }]] };
62
+ const pagination = { page: 2, cursors: ['a', 'b'] };
63
+ state = setDataRows(state, data, pagination);
64
+ expect(state.data).toBe(data);
65
+ expect(state.pagination).toEqual(pagination);
66
+ });
67
+
68
+ it('setFilterSchema updates filterSchema', () => {
69
+ let state = createDefaultAppState(mockViews);
70
+ const newSchema = {
71
+ groups: [{ name: 'default', label: 'Default' }],
72
+ filters: [
73
+ { id: 'filter-c', label: 'C', expression: { type: 'isNull', key: 'c', value: null }, group: 'default' }
74
+ ]
75
+ };
76
+ state = setFilterSchema(state, newSchema as any);
77
+ expect(state.filterSchemasAndGroups).toBe(newSchema);
78
+ });
79
+
80
+ it('setFilterState updates filterState', () => {
81
+ let state = createDefaultAppState(mockViews);
82
+ const newFilterState: FilterState = new Map([['filter1', { key: 'x', value: 42 } as any]]);
83
+ state = setFilterState(state, newFilterState);
84
+ expect(state.filterState).toBe(newFilterState);
85
+ });
86
+ });
@@ -0,0 +1,148 @@
1
+ import { useState } from "react";
2
+ import { FilterFormState } from "../components/FilterForm";
3
+ import { FilterSchemasAndGroups, FilterId, FilterExpr } from "./filters";
4
+ import { View, ViewId } from "./view";
5
+ import { FetchDataResult } from "./data";
6
+
7
+ export type FilterState = Map<FilterId, FilterFormState>;
8
+
9
+ export enum FormStateInitMode {
10
+ WithInitialValues = 'withInitialValues',
11
+ Empty = 'empty'
12
+ }
13
+
14
+ // Helper to build form state from FilterExpr
15
+ export function buildInitialFormState(expr: FilterExpr, mode: FormStateInitMode = FormStateInitMode.WithInitialValues): FilterFormState {
16
+ if (expr.type === 'and' || expr.type === 'or') {
17
+ return {
18
+ type: expr.type,
19
+ children: expr.filters.map(child => buildInitialFormState(child, mode))
20
+ };
21
+ } else if (expr.type === 'not') {
22
+ return {
23
+ type: 'not',
24
+ child: buildInitialFormState(expr.filter, mode)
25
+ };
26
+ } else {
27
+ return {
28
+ type: 'leaf',
29
+ value: mode === FormStateInitMode.Empty ? '' : ('initialValue' in expr.value && expr.value.initialValue !== undefined ? expr.value.initialValue : '')
30
+ };
31
+ }
32
+ }
33
+
34
+ export function getFilterStateById(state: FilterState, id: FilterId): FilterFormState {
35
+ const filter = state.get(id);
36
+ if (!filter) {
37
+ throw new Error(`Inconsistent state: Filter with id ${id} not found`);
38
+ }
39
+ return filter;
40
+ }
41
+
42
+ export function setFilterStateById(state: FilterState, id: FilterId, newFilterState: FilterFormState): FilterState {
43
+ if (!state.has(id)) {
44
+ throw new Error(`Inconsistent state: Filter with id ${id} not found`);
45
+ }
46
+ const newState = new Map(state);
47
+ newState.set(id, newFilterState);
48
+ return newState;
49
+ }
50
+
51
+ // AppState data structure for app state
52
+ export interface AppState {
53
+ selectedViewId: ViewId
54
+ views: View[]
55
+ filterSchemasAndGroups: FilterSchemasAndGroups
56
+ data: FetchDataResult
57
+ filterState: FilterState
58
+ pagination: PaginationState
59
+ }
60
+
61
+ export interface PaginationState {
62
+ page: number;
63
+ cursors: (string | number | null)[];
64
+ }
65
+
66
+ const defaultPagination: PaginationState = {
67
+ page: 0,
68
+ cursors: []
69
+ };
70
+
71
+ export function createDefaultFilterState(filterSchema: FilterSchemasAndGroups, mode: FormStateInitMode = FormStateInitMode.WithInitialValues): FilterState {
72
+ return new Map(filterSchema.filters.map(filter => [filter.id, buildInitialFormState(filter.expression, mode)]));
73
+ }
74
+
75
+ export function createDefaultAppState(views: View[]): AppState {
76
+ const selectedViewId = views[0]?.id;
77
+ const view = views.find(v => v.id === selectedViewId) as View;
78
+ const filterSchema: FilterSchemasAndGroups = view.filterSchema;
79
+ const initialFilterState = createDefaultFilterState(filterSchema);
80
+ return {
81
+ views,
82
+ selectedViewId,
83
+ filterSchemasAndGroups: filterSchema,
84
+ data: { flattenedRows: [], rows: [] },
85
+ filterState: initialFilterState,
86
+ pagination: defaultPagination
87
+ };
88
+ }
89
+
90
+ // Update selectedViewId
91
+ function setSelectedViewId(state: AppState, newId: ViewId): AppState {
92
+ const view = state.views.find(v => v.id === newId);
93
+ const filterSchema = view?.filterSchema || { groups: [], filters: [] };
94
+ return {
95
+ ...state,
96
+ selectedViewId: newId,
97
+ filterSchemasAndGroups: filterSchema,
98
+ filterState: createDefaultFilterState(filterSchema),
99
+ pagination: defaultPagination
100
+ };
101
+ }
102
+
103
+ function getSelectedView(state: AppState): View {
104
+ return state.views.find(v => v.id === state.selectedViewId) as View;
105
+ }
106
+
107
+ function setDataRows(state: AppState, newRows: FetchDataResult, pagination: PaginationState = defaultPagination): AppState {
108
+ return {
109
+ ...state,
110
+ data: newRows,
111
+ pagination
112
+ };
113
+ }
114
+
115
+ function setFilterSchema(state: AppState, newSchema: FilterSchemasAndGroups): AppState {
116
+ return {
117
+ ...state,
118
+ filterSchemasAndGroups: newSchema
119
+ };
120
+ }
121
+
122
+ function setFilterState(state: AppState, newFilterState: FilterState): AppState {
123
+ return {
124
+ ...state,
125
+ filterState: newFilterState,
126
+ pagination: defaultPagination
127
+ };
128
+ }
129
+
130
+ export const useAppState = (views: View[], initialFilterStateOverride?: FilterState) => {
131
+ const [appState, setAppState] = useState<AppState>(() => {
132
+ const base = createDefaultAppState(views);
133
+ if (initialFilterStateOverride) {
134
+ return { ...base, filterState: initialFilterStateOverride };
135
+ }
136
+ return base;
137
+ });
138
+ return {
139
+ state: appState,
140
+ selectedView: getSelectedView(appState),
141
+ setSelectedViewId: (id: ViewId) => setAppState(prev => setSelectedViewId(prev, id)),
142
+ setDataRows: (rows: FetchDataResult, pagination?: PaginationState) => setAppState(prev => setDataRows(prev, rows, pagination)),
143
+ setFilterSchema: (schema: FilterSchemasAndGroups) => setAppState(prev => setFilterSchema(prev, schema)),
144
+ setFilterState: (filterState: FilterState) => setAppState(prev => setFilterState(prev, filterState))
145
+ };
146
+ }
147
+
148
+ export { setSelectedViewId, setDataRows, setFilterSchema, setFilterState };
@@ -0,0 +1,51 @@
1
+ import { TransformResult } from './filters';
2
+
3
+ describe('TransformResult functionality', () => {
4
+
5
+ describe('new object-based transform behavior', () => {
6
+ it('should handle object returns with value only', () => {
7
+ const objectTransform = (input: any): TransformResult => ({ value: input?.toString() || "" });
8
+
9
+ const result = objectTransform(42);
10
+ expect(result).toEqual({ value: "42" });
11
+
12
+ const emptyResult = objectTransform(null);
13
+ expect(emptyResult).toEqual({ value: "" });
14
+ });
15
+
16
+ it('should handle object returns with both field and value', () => {
17
+ const keyValueTransform = (input: any): TransformResult => ({
18
+ field: "transformedField",
19
+ value: `prefix_${input}`
20
+ });
21
+
22
+ const result = keyValueTransform("test");
23
+ expect(result).toEqual({
24
+ field: "transformedField",
25
+ value: "prefix_test"
26
+ });
27
+ });
28
+
29
+ it('should handle conditional transform returns', () => {
30
+ const conditionalTransform = (input: any): TransformResult => {
31
+ if (!input || input === '') {
32
+ return input; // Return simple value for empty input
33
+ }
34
+ return { field: "transformedField", value: `prefix_${input}` };
35
+ };
36
+
37
+ // Empty input returns simple value
38
+ expect(conditionalTransform("")).toBe("");
39
+ expect(conditionalTransform(null)).toBe(null);
40
+
41
+ // Non-empty input returns object
42
+ expect(conditionalTransform("test")).toEqual({
43
+ field: "transformedField",
44
+ value: "prefix_test"
45
+ });
46
+ });
47
+
48
+
49
+ });
50
+
51
+ });