@cccsaurora/howler-ui 2.16.0-dev.378 → 2.16.0-dev.381

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 (57) hide show
  1. package/components/app/App.js +2 -0
  2. package/components/app/hooks/useMatchers.js +0 -4
  3. package/components/app/providers/FavouritesProvider.js +2 -1
  4. package/components/app/providers/FieldProvider.d.ts +2 -2
  5. package/components/app/providers/HitProvider.d.ts +3 -3
  6. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  7. package/components/app/providers/HitSearchProvider.js +64 -39
  8. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.test.js +505 -0
  10. package/components/app/providers/ParameterProvider.d.ts +13 -5
  11. package/components/app/providers/ParameterProvider.js +240 -84
  12. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  13. package/components/app/providers/ParameterProvider.test.js +1041 -0
  14. package/components/app/providers/ViewProvider.d.ts +3 -2
  15. package/components/app/providers/ViewProvider.js +21 -14
  16. package/components/app/providers/ViewProvider.test.js +19 -29
  17. package/components/elements/display/ChipPopper.d.ts +21 -0
  18. package/components/elements/display/ChipPopper.js +36 -0
  19. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  20. package/components/elements/display/ChipPopper.test.js +309 -0
  21. package/components/elements/hit/HitActions.js +3 -3
  22. package/components/elements/hit/HitSummary.d.ts +0 -1
  23. package/components/elements/hit/HitSummary.js +11 -21
  24. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  25. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  26. package/components/routes/dossiers/DossierCard.test.js +0 -2
  27. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  28. package/components/routes/hits/search/HitBrowser.js +7 -48
  29. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  30. package/components/routes/hits/search/InformationPane.js +1 -1
  31. package/components/routes/hits/search/QuerySettings.js +30 -0
  32. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  33. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  34. package/components/routes/hits/search/SearchPane.js +8 -10
  35. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  36. package/components/routes/hits/search/ViewLink.js +37 -19
  37. package/components/routes/hits/search/ViewLink.test.js +349 -303
  38. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  39. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  40. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  41. package/components/routes/hits/search/shared/HitSort.js +16 -8
  42. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  43. package/components/routes/views/ViewComposer.js +7 -6
  44. package/components/routes/views/Views.js +2 -1
  45. package/locales/en/translation.json +6 -0
  46. package/locales/fr/translation.json +6 -0
  47. package/package.json +2 -2
  48. package/setupTests.js +4 -1
  49. package/tests/mocks.d.ts +18 -0
  50. package/tests/mocks.js +65 -0
  51. package/tests/server-handlers.js +10 -28
  52. package/utils/viewUtils.d.ts +2 -0
  53. package/utils/viewUtils.js +11 -0
  54. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  55. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  56. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -0,0 +1,553 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent, {} from '@testing-library/user-event';
4
+ import { act } from 'react';
5
+ import { setupContextSelectorMock } from '@cccsaurora/howler-ui/tests/mocks';
6
+ import { vi } from 'vitest';
7
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
8
+ setupContextSelectorMock();
9
+ // Mock child components
10
+ vi.mock('./shared/HitFilter', () => ({
11
+ default: ({ id, value }) => (_jsxs("div", { id: `hit-filter-${id}`, "data-value": value, children: ["HitFilter ", id, ": ", value] }))
12
+ }));
13
+ vi.mock('./shared/HitSort', () => ({
14
+ default: () => _jsx("div", { id: "hit-sort", children: "HitSort" })
15
+ }));
16
+ vi.mock('./shared/SearchSpan', () => ({
17
+ default: () => _jsx("div", { id: "search-span", children: "SearchSpan" })
18
+ }));
19
+ vi.mock('./ViewLink', () => ({
20
+ default: ({ id, viewId }) => (_jsxs("div", { id: `view-link-${id}`, "data-view-id": viewId, children: ["ViewLink ", id, ": ", viewId] }))
21
+ }));
22
+ // Import component after mocks
23
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
24
+ import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
25
+ import i18n from '@cccsaurora/howler-ui/i18n';
26
+ import { I18nextProvider } from 'react-i18next';
27
+ import { createMockView } from '@cccsaurora/howler-ui/tests/utils';
28
+ import QuerySettings from './QuerySettings';
29
+ // Mock contexts
30
+ const mockAddFilter = vi.fn();
31
+ const mockAddView = vi.fn();
32
+ const mockFetchViews = vi.fn();
33
+ let mockParameterContext = {
34
+ filters: [],
35
+ views: [],
36
+ addFilter: mockAddFilter,
37
+ addView: mockAddView
38
+ };
39
+ let mockViewContext = {
40
+ views: {},
41
+ fetchViews: mockFetchViews
42
+ };
43
+ // Test wrapper
44
+ const Wrapper = ({ children }) => {
45
+ return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: _jsx(ViewContext.Provider, { value: mockViewContext, children: children }) }) }));
46
+ };
47
+ describe('QuerySettings', () => {
48
+ let user;
49
+ beforeEach(() => {
50
+ user = userEvent.setup();
51
+ vi.clearAllMocks();
52
+ // Reset mock contexts to defaults
53
+ mockParameterContext.filters = [];
54
+ mockParameterContext.views = [];
55
+ mockParameterContext.addFilter = mockAddFilter;
56
+ mockParameterContext.addView = mockAddView;
57
+ mockViewContext.views = {};
58
+ mockViewContext.fetchViews = mockFetchViews;
59
+ mockFetchViews.mockResolvedValue(undefined);
60
+ });
61
+ describe('Rendering Conditions', () => {
62
+ it('should render all core components when no filters or views exist', () => {
63
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
64
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
65
+ expect(screen.getByTestId('search-span')).toBeInTheDocument();
66
+ expect(screen.queryByTestId(/^view-link-/)).not.toBeInTheDocument();
67
+ });
68
+ it('should render with default boxSx when not provided', () => {
69
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
70
+ const box = container.firstChild;
71
+ expect(box).toBeInTheDocument();
72
+ });
73
+ it('should render with custom boxSx when provided', () => {
74
+ const customSx = { maxWidth: '800px', backgroundColor: 'red' };
75
+ render(_jsx(QuerySettings, { boxSx: customSx }), { wrapper: Wrapper });
76
+ // Component should render without errors
77
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
78
+ });
79
+ it('should render Add buttons in ChipPopper', async () => {
80
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
81
+ // Open the ChipPopper to see buttons
82
+ const chipButton = screen.getByRole('button');
83
+ await user.click(chipButton);
84
+ expect(screen.getByLabelText(i18n.t('hit.search.filter.add'))).toBeInTheDocument();
85
+ expect(screen.getByLabelText(i18n.t('hit.search.view.add'))).toBeInTheDocument();
86
+ });
87
+ });
88
+ describe('Views Display', () => {
89
+ it('should render no ViewLink components when views array is empty', () => {
90
+ mockParameterContext.views = [];
91
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
92
+ expect(screen.queryByTestId(/^view-link-/)).not.toBeInTheDocument();
93
+ });
94
+ it('should render single ViewLink component when one view exists', () => {
95
+ mockParameterContext.views = ['view-1'];
96
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
97
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
98
+ expect(screen.getByTestId('view-link-0')).toHaveAttribute('data-view-id', 'view-1');
99
+ });
100
+ it('should render multiple ViewLink components when multiple views exist', () => {
101
+ mockParameterContext.views = ['view-1', 'view-2', 'view-3'];
102
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
103
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
104
+ expect(screen.getByTestId('view-link-1')).toBeInTheDocument();
105
+ expect(screen.getByTestId('view-link-2')).toBeInTheDocument();
106
+ expect(screen.getByTestId('view-link-0')).toHaveAttribute('data-view-id', 'view-1');
107
+ expect(screen.getByTestId('view-link-1')).toHaveAttribute('data-view-id', 'view-2');
108
+ expect(screen.getByTestId('view-link-2')).toHaveAttribute('data-view-id', 'view-3');
109
+ });
110
+ it('should maintain view order in display', () => {
111
+ mockParameterContext.views = ['view-z', 'view-a', 'view-m'];
112
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
113
+ expect(screen.getByTestId('view-link-0')).toHaveAttribute('data-view-id', 'view-z');
114
+ expect(screen.getByTestId('view-link-1')).toHaveAttribute('data-view-id', 'view-a');
115
+ expect(screen.getByTestId('view-link-2')).toHaveAttribute('data-view-id', 'view-m');
116
+ });
117
+ it('should pass correct id prop to each ViewLink', () => {
118
+ mockParameterContext.views = ['view1', 'view2', 'view3'];
119
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
120
+ expect(screen.getByText('ViewLink 0: view1')).toBeInTheDocument();
121
+ expect(screen.getByText('ViewLink 1: view2')).toBeInTheDocument();
122
+ expect(screen.getByText('ViewLink 2: view3')).toBeInTheDocument();
123
+ });
124
+ it('should handle empty string view (selection mode)', () => {
125
+ mockParameterContext.views = ['', 'valid-view'];
126
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
127
+ expect(screen.getByTestId('view-link-0')).toHaveAttribute('data-view-id', '');
128
+ expect(screen.getByTestId('view-link-1')).toHaveAttribute('data-view-id', 'valid-view');
129
+ });
130
+ });
131
+ describe('Filter Display', () => {
132
+ it('should render no HitFilter components when filters array is empty', () => {
133
+ mockParameterContext.filters = [];
134
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
135
+ expect(screen.queryByTestId(/^hit-filter-/)).not.toBeInTheDocument();
136
+ });
137
+ it('should render single HitFilter component when one filter exists', () => {
138
+ mockParameterContext.filters = ['howler.status:open'];
139
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
140
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
141
+ expect(screen.getByTestId('hit-filter-0')).toHaveAttribute('data-value', 'howler.status:open');
142
+ });
143
+ it('should render multiple HitFilter components when multiple filters exist', () => {
144
+ mockParameterContext.filters = ['howler.status:open', 'howler.assignment:user1', 'howler.escalation:hit'];
145
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
146
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
147
+ expect(screen.getByTestId('hit-filter-1')).toBeInTheDocument();
148
+ expect(screen.getByTestId('hit-filter-2')).toBeInTheDocument();
149
+ expect(screen.getByTestId('hit-filter-0')).toHaveAttribute('data-value', 'howler.status:open');
150
+ expect(screen.getByTestId('hit-filter-1')).toHaveAttribute('data-value', 'howler.assignment:user1');
151
+ expect(screen.getByTestId('hit-filter-2')).toHaveAttribute('data-value', 'howler.escalation:hit');
152
+ });
153
+ it('should maintain filter order in display', () => {
154
+ mockParameterContext.filters = ['filter-z', 'filter-a', 'filter-m'];
155
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
156
+ expect(screen.getByTestId('hit-filter-0')).toHaveAttribute('data-value', 'filter-z');
157
+ expect(screen.getByTestId('hit-filter-1')).toHaveAttribute('data-value', 'filter-a');
158
+ expect(screen.getByTestId('hit-filter-2')).toHaveAttribute('data-value', 'filter-m');
159
+ });
160
+ it('should pass correct id prop to each HitFilter', () => {
161
+ mockParameterContext.filters = ['filter1', 'filter2', 'filter3'];
162
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
163
+ expect(screen.getByText('HitFilter 0: filter1')).toBeInTheDocument();
164
+ expect(screen.getByText('HitFilter 1: filter2')).toBeInTheDocument();
165
+ expect(screen.getByText('HitFilter 2: filter3')).toBeInTheDocument();
166
+ });
167
+ it('should handle empty string filters', () => {
168
+ mockParameterContext.filters = ['', 'valid:filter'];
169
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
170
+ expect(screen.getByTestId('hit-filter-0')).toHaveAttribute('data-value', '');
171
+ expect(screen.getByTestId('hit-filter-1')).toHaveAttribute('data-value', 'valid:filter');
172
+ });
173
+ });
174
+ describe('Add Filter and View Buttons', () => {
175
+ it('should call addFilter when Add Filter button clicked', async () => {
176
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
177
+ const chipButton = screen.getByRole('button');
178
+ await user.click(chipButton);
179
+ const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
180
+ await user.click(addFilterButton);
181
+ expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*');
182
+ expect(mockAddFilter).toHaveBeenCalledTimes(1);
183
+ });
184
+ it('should call fetchViews and addView when Add View button clicked', async () => {
185
+ mockViewContext.views = {
186
+ 'view-1': createMockView({ view_id: 'view-1' })
187
+ };
188
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
189
+ const chipButton = screen.getByRole('button');
190
+ await user.click(chipButton);
191
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
192
+ await user.click(addViewButton);
193
+ expect(mockFetchViews).toHaveBeenCalledTimes(1);
194
+ await vi.waitFor(() => {
195
+ expect(mockAddView).toHaveBeenCalledWith('');
196
+ });
197
+ });
198
+ it('should disable Add View button when no available views', async () => {
199
+ mockViewContext.views = {};
200
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
201
+ const chipButton = screen.getByRole('button');
202
+ await user.click(chipButton);
203
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
204
+ expect(addViewButton).toBeDisabled();
205
+ });
206
+ it('should disable Add View button when all views are in currentViews', async () => {
207
+ mockViewContext.views = {
208
+ 'view-1': createMockView({ view_id: 'view-1' }),
209
+ 'view-2': createMockView({ view_id: 'view-2' })
210
+ };
211
+ mockParameterContext.views = ['view-1', 'view-2'];
212
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
213
+ const chipButton = screen.getByRole('button');
214
+ await user.click(chipButton);
215
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
216
+ expect(addViewButton).toBeDisabled();
217
+ });
218
+ it('should disable Add View button when empty string already in currentViews', async () => {
219
+ mockViewContext.views = {
220
+ 'view-1': createMockView({ view_id: 'view-1' })
221
+ };
222
+ mockParameterContext.views = [''];
223
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
224
+ const chipButton = screen.getByRole('button');
225
+ await user.click(chipButton);
226
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
227
+ expect(addViewButton).toBeDisabled();
228
+ });
229
+ it('should enable Add View button when available views exist', async () => {
230
+ mockViewContext.views = {
231
+ 'view-1': createMockView({ view_id: 'view-1' }),
232
+ 'view-2': createMockView({ view_id: 'view-2' })
233
+ };
234
+ mockParameterContext.views = ['view-1'];
235
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
236
+ const chipButton = screen.getByRole('button');
237
+ await user.click(chipButton);
238
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
239
+ expect(addViewButton).not.toBeDisabled();
240
+ });
241
+ it('should display both Add buttons in ChipPopper', async () => {
242
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
243
+ const chipButton = screen.getByRole('button');
244
+ await user.click(chipButton);
245
+ expect(screen.getByLabelText(i18n.t('hit.search.filter.add'))).toBeInTheDocument();
246
+ expect(screen.getByLabelText(i18n.t('hit.search.view.add'))).toBeInTheDocument();
247
+ });
248
+ it('should allow multiple clicks to add multiple filters', async () => {
249
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
250
+ const chipButton = screen.getByRole('button');
251
+ await user.click(chipButton);
252
+ const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
253
+ await act(async () => {
254
+ await user.click(addFilterButton);
255
+ await user.click(addFilterButton);
256
+ await user.click(addFilterButton);
257
+ });
258
+ expect(mockAddFilter).toHaveBeenCalledTimes(3);
259
+ expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*');
260
+ });
261
+ });
262
+ describe('Grid Layout', () => {
263
+ it('should render components in Grid container', () => {
264
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
265
+ const gridContainer = container.querySelector('[class*="MuiGrid-container"]');
266
+ expect(gridContainer).toBeInTheDocument();
267
+ });
268
+ it('should render each component in separate Grid item', () => {
269
+ mockParameterContext.filters = ['filter1', 'filter2'];
270
+ mockParameterContext.views = ['view-1'];
271
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
272
+ const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
273
+ // HitSort + SearchSpan + 1 view + 2 filters = 5 items
274
+ expect(gridItems.length).toBe(5);
275
+ });
276
+ it('should render correct number of items with multiple views', () => {
277
+ mockParameterContext.filters = ['filter1'];
278
+ mockParameterContext.views = ['view-1', 'view-2', 'view-3'];
279
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
280
+ const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
281
+ // HitSort + SearchSpan + 3 views + 1 filter = 6 items
282
+ expect(gridItems.length).toBe(6);
283
+ });
284
+ });
285
+ describe('Edge Cases', () => {
286
+ it('should handle undefined filters gracefully', () => {
287
+ mockParameterContext.filters = undefined;
288
+ expect(() => render(_jsx(QuerySettings, {}), { wrapper: Wrapper })).not.toThrow();
289
+ });
290
+ it('should handle very long filter arrays', () => {
291
+ const manyFilters = Array.from({ length: 100 }, (_, i) => `filter${i}`);
292
+ mockParameterContext.filters = manyFilters;
293
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
294
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
295
+ expect(screen.getByTestId('hit-filter-99')).toBeInTheDocument();
296
+ });
297
+ it('should handle filters with special characters', () => {
298
+ mockParameterContext.filters = ['filter:with:colons', 'filter&with&ampersands', 'filter with spaces'];
299
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
300
+ expect(screen.getByTestId('hit-filter-0')).toHaveAttribute('data-value', 'filter:with:colons');
301
+ expect(screen.getByTestId('hit-filter-1')).toHaveAttribute('data-value', 'filter&with&ampersands');
302
+ expect(screen.getByTestId('hit-filter-2')).toHaveAttribute('data-value', 'filter with spaces');
303
+ });
304
+ it('should handle rapid context changes', () => {
305
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
306
+ mockParameterContext = { ...mockParameterContext, filters: ['filter1'] };
307
+ rerender(_jsx(QuerySettings, {}));
308
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
309
+ mockParameterContext = { ...mockParameterContext, filters: ['filter1', 'filter2'] };
310
+ rerender(_jsx(QuerySettings, {}));
311
+ expect(screen.getByTestId('hit-filter-1')).toBeInTheDocument();
312
+ mockParameterContext = { ...mockParameterContext, filters: [] };
313
+ rerender(_jsx(QuerySettings, {}));
314
+ expect(screen.queryByTestId(/^hit-filter-/)).not.toBeInTheDocument();
315
+ });
316
+ it('should handle verticalSorters prop (even though unused)', () => {
317
+ const { container } = render(_jsx(QuerySettings, { verticalSorters: true }), { wrapper: Wrapper });
318
+ expect(container).toBeInTheDocument();
319
+ });
320
+ });
321
+ describe('Integration Tests', () => {
322
+ it('should work with all contexts simultaneously', () => {
323
+ mockParameterContext.filters = ['filter1', 'filter2'];
324
+ mockParameterContext.views = ['view-1', 'view-2'];
325
+ mockViewContext.views = {
326
+ 'view-1': createMockView({ view_id: 'view-1' }),
327
+ 'view-2': createMockView({ view_id: 'view-2' })
328
+ };
329
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
330
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
331
+ expect(screen.getByTestId('search-span')).toBeInTheDocument();
332
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
333
+ expect(screen.getByTestId('view-link-1')).toBeInTheDocument();
334
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
335
+ expect(screen.getByTestId('hit-filter-1')).toBeInTheDocument();
336
+ expect(screen.getByRole('button')).toBeInTheDocument();
337
+ });
338
+ it('should update filters when context changes', () => {
339
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
340
+ expect(screen.queryByTestId(/^hit-filter-/)).not.toBeInTheDocument();
341
+ mockParameterContext = { ...mockParameterContext, filters: ['new:filter'] };
342
+ rerender(_jsx(QuerySettings, {}));
343
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
344
+ });
345
+ it('should update views when context changes', () => {
346
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
347
+ expect(screen.queryByTestId(/^view-link-/)).not.toBeInTheDocument();
348
+ mockParameterContext = { ...mockParameterContext, views: ['view-1'] };
349
+ rerender(_jsx(QuerySettings, {}));
350
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
351
+ });
352
+ it('should handle adding filter and updating filters array', async () => {
353
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
354
+ const chipButton = screen.getByRole('button');
355
+ await user.click(chipButton);
356
+ const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
357
+ await user.click(addFilterButton);
358
+ expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*');
359
+ // Simulate the filter being added to the array
360
+ mockParameterContext = { ...mockParameterContext, filters: ['howler.assessment:*'] };
361
+ rerender(_jsx(QuerySettings, {}));
362
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
363
+ });
364
+ it('should handle adding view and updating views array', async () => {
365
+ mockViewContext.views = {
366
+ 'view-1': createMockView({ view_id: 'view-1' })
367
+ };
368
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
369
+ const chipButton = screen.getByRole('button');
370
+ await user.click(chipButton);
371
+ const addViewButton = screen.getByLabelText(i18n.t('hit.search.view.add'));
372
+ await user.click(addViewButton);
373
+ expect(mockFetchViews).toHaveBeenCalled();
374
+ await vi.waitFor(() => {
375
+ expect(mockAddView).toHaveBeenCalledWith('');
376
+ });
377
+ // Simulate the view being added to the array
378
+ mockParameterContext = { ...mockParameterContext, views: [''] };
379
+ rerender(_jsx(QuerySettings, {}));
380
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
381
+ });
382
+ it('should handle multiple views and filters together', () => {
383
+ mockParameterContext.filters = ['filter1', 'filter2', 'filter3'];
384
+ mockParameterContext.views = ['view-1', 'view-2'];
385
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
386
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
387
+ expect(screen.getByTestId('view-link-1')).toBeInTheDocument();
388
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
389
+ expect(screen.getByTestId('hit-filter-1')).toBeInTheDocument();
390
+ expect(screen.getByTestId('hit-filter-2')).toBeInTheDocument();
391
+ });
392
+ });
393
+ describe('Accessibility', () => {
394
+ it('should have accessible Add Filter and Add View buttons', async () => {
395
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
396
+ const chipButton = screen.getByRole('button');
397
+ await user.click(chipButton);
398
+ expect(screen.getByLabelText(i18n.t('hit.search.filter.add'))).toBeInTheDocument();
399
+ expect(screen.getByLabelText(i18n.t('hit.search.view.add'))).toBeInTheDocument();
400
+ });
401
+ it('should maintain logical tab order', () => {
402
+ mockParameterContext.filters = ['filter1', 'filter2'];
403
+ mockParameterContext.views = ['view-1'];
404
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
405
+ // All interactive elements should be in the document
406
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
407
+ expect(screen.getByTestId('search-span')).toBeInTheDocument();
408
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
409
+ expect(screen.getByRole('button')).toBeInTheDocument();
410
+ });
411
+ it('should be keyboard accessible for Add buttons', async () => {
412
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
413
+ const chipButton = screen.getByRole('button');
414
+ await user.click(chipButton);
415
+ await waitFor(() => expect(screen.findByLabelText(i18n.t('hit.search.filter.add'))));
416
+ const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
417
+ act(() => {
418
+ addFilterButton.focus();
419
+ });
420
+ await waitFor(() => expect(addFilterButton).toHaveFocus());
421
+ await user.keyboard('{Enter}');
422
+ await waitFor(() => expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*'));
423
+ });
424
+ it('should maintain focus when filters are added', async () => {
425
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
426
+ const chipButton = screen.getByRole('button');
427
+ await user.click(chipButton);
428
+ const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
429
+ await user.click(addFilterButton);
430
+ mockParameterContext = { ...mockParameterContext, filters: ['howler.id:*'] };
431
+ rerender(_jsx(QuerySettings, {}));
432
+ // ChipPopper button should still be in document
433
+ expect(screen.queryByText(i18n.t('hit.search.filter.add'))).toBeInTheDocument();
434
+ });
435
+ it('should have semantic HTML structure', () => {
436
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
437
+ // Should use proper Box and Grid components
438
+ expect(container.querySelector('[class*="MuiBox"]')).toBeInTheDocument();
439
+ expect(container.querySelector('[class*="MuiGrid-container"]')).toBeInTheDocument();
440
+ });
441
+ it('should support screen reader navigation', () => {
442
+ mockParameterContext.filters = ['filter1', 'filter2'];
443
+ mockParameterContext.views = ['view-1'];
444
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
445
+ // All major components should be identifiable
446
+ const hitSort = screen.getByTestId('hit-sort');
447
+ const searchSpan = screen.getByTestId('search-span');
448
+ const viewLink = screen.getByTestId('view-link-0');
449
+ const filter1 = screen.getByTestId('hit-filter-0');
450
+ const filter2 = screen.getByTestId('hit-filter-1');
451
+ const chipButton = screen.getByRole('button');
452
+ expect(hitSort).toBeInTheDocument();
453
+ expect(searchSpan).toBeInTheDocument();
454
+ expect(viewLink).toBeInTheDocument();
455
+ expect(filter1).toBeInTheDocument();
456
+ expect(filter2).toBeInTheDocument();
457
+ expect(chipButton).toBeInTheDocument();
458
+ });
459
+ });
460
+ describe('Memoization', () => {
461
+ it('should not re-render unnecessarily when unrelated props change', () => {
462
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
463
+ // Mock component should be rendered once
464
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
465
+ // Re-render with same props
466
+ rerender(_jsx(QuerySettings, {}));
467
+ // Components should still be present (memo prevents unnecessary re-renders)
468
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
469
+ });
470
+ it('should update when filters change', () => {
471
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
472
+ expect(screen.queryByTestId(/^hit-filter-/)).not.toBeInTheDocument();
473
+ mockParameterContext = { ...mockParameterContext, filters: ['new:filter'] };
474
+ rerender(_jsx(QuerySettings, {}));
475
+ expect(screen.getByTestId('hit-filter-0')).toBeInTheDocument();
476
+ });
477
+ it('should update when views change', () => {
478
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
479
+ expect(screen.queryByTestId(/^view-link-/)).not.toBeInTheDocument();
480
+ mockParameterContext = { ...mockParameterContext, views: ['view-1'] };
481
+ rerender(_jsx(QuerySettings, {}));
482
+ expect(screen.getByTestId('view-link-0')).toBeInTheDocument();
483
+ });
484
+ it('should update when boxSx prop changes', () => {
485
+ const { rerender } = render(_jsx(QuerySettings, { boxSx: { maxWidth: '800px' } }), { wrapper: Wrapper });
486
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
487
+ rerender(_jsx(QuerySettings, { boxSx: { maxWidth: '1000px' } }));
488
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
489
+ });
490
+ it('should update allowAddViews when available views change', () => {
491
+ mockViewContext.views = {};
492
+ const { rerender } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
493
+ // No available views initially
494
+ mockViewContext = {
495
+ ...mockViewContext,
496
+ views: {
497
+ 'view-1': createMockView({ view_id: 'view-1' })
498
+ }
499
+ };
500
+ rerender(_jsx(QuerySettings, {}));
501
+ // Component should respond to view context changes
502
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
503
+ });
504
+ });
505
+ describe('Component Composition', () => {
506
+ it('should render all child components in correct order', () => {
507
+ mockParameterContext.filters = ['filter1'];
508
+ mockParameterContext.views = ['view-1'];
509
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
510
+ const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
511
+ // Order: HitSort, SearchSpan, ViewLink(s), HitFilter(s)
512
+ expect(gridItems[0]).toContainElement(screen.getByTestId('hit-sort'));
513
+ expect(gridItems[1]).toContainElement(screen.getByTestId('search-span'));
514
+ expect(gridItems[2]).toContainElement(screen.getByTestId('view-link-0'));
515
+ expect(gridItems[3]).toContainElement(screen.getByTestId('hit-filter-0'));
516
+ });
517
+ it('should pass correct props to ViewLink components', () => {
518
+ mockParameterContext.views = ['view-1', 'view-2'];
519
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
520
+ const viewLink0 = screen.getByTestId('view-link-0');
521
+ expect(viewLink0).toHaveAttribute('data-view-id', 'view-1');
522
+ expect(viewLink0).toHaveTextContent('ViewLink 0: view-1');
523
+ const viewLink1 = screen.getByTestId('view-link-1');
524
+ expect(viewLink1).toHaveAttribute('data-view-id', 'view-2');
525
+ expect(viewLink1).toHaveTextContent('ViewLink 1: view-2');
526
+ });
527
+ it('should pass correct props to HitFilter components', () => {
528
+ mockParameterContext.filters = ['filter:value'];
529
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
530
+ const filter = screen.getByTestId('hit-filter-0');
531
+ expect(filter).toHaveAttribute('data-value', 'filter:value');
532
+ expect(filter).toHaveTextContent('HitFilter 0: filter:value');
533
+ });
534
+ it('should render components independently', () => {
535
+ render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
536
+ // Each component should render regardless of others
537
+ expect(screen.getByTestId('hit-sort')).toBeInTheDocument();
538
+ expect(screen.getByTestId('search-span')).toBeInTheDocument();
539
+ });
540
+ it('should render multiple views before filters', () => {
541
+ mockParameterContext.views = ['view-1', 'view-2'];
542
+ mockParameterContext.filters = ['filter1'];
543
+ const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
544
+ const gridItems = Array.from(container.querySelectorAll('[class*="MuiGrid-item"]'));
545
+ const viewLink0Index = gridItems.findIndex(item => item.querySelector('#view-link-0'));
546
+ const viewLink1Index = gridItems.findIndex(item => item.querySelector('#view-link-1'));
547
+ const filterIndex = gridItems.findIndex(item => item.querySelector('#hit-filter-0'));
548
+ // Views should come before filters
549
+ expect(viewLink0Index).toBeLessThan(filterIndex);
550
+ expect(viewLink1Index).toBeLessThan(filterIndex);
551
+ });
552
+ });
553
+ });
@@ -7,7 +7,6 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
10
- import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
11
10
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
12
11
  import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
13
12
  import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBox';
@@ -20,8 +19,9 @@ import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
20
19
  import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
21
20
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
22
21
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
23
- import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
24
- import React, { memo, useCallback, useEffect } from 'react';
22
+ import useMyLocalStorage, { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
23
+ import React, { memo, useCallback, useEffect, useMemo } from 'react';
24
+ import { isMobile } from 'react-device-detect';
25
25
  import { useTranslation } from 'react-i18next';
26
26
  import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
27
27
  import { useContextSelector } from 'use-context-selector';
@@ -30,13 +30,12 @@ import BundleParentMenu from './BundleParentMenu';
30
30
  import { BundleScroller } from './BundleScroller';
31
31
  import HitContextMenu from './HitContextMenu';
32
32
  import HitQuery from './HitQuery';
33
- import ViewLink from './ViewLink';
34
- import QuerySettings from './shared/QuerySettings';
33
+ import QuerySettings from './QuerySettings';
35
34
  const Item = memo(({ hit, onClick }) => {
36
35
  const theme = useTheme();
36
+ const { get } = useMyLocalStorage();
37
37
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
38
38
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
39
- const layout = useContextSelector(HitSearchContext, ctx => ctx.layout);
40
39
  const checkMiddleClick = useCallback((e, id) => {
41
40
  if (e.button === 1) {
42
41
  window.open(`${window.origin}/hits/${id}`, '_blank');
@@ -44,6 +43,7 @@ const Item = memo(({ hit, onClick }) => {
44
43
  e.preventDefault();
45
44
  }
46
45
  }, []);
46
+ const layout = useMemo(() => (isMobile ? HitLayout.COMFY : (get(StorageKey.HIT_LAYOUT) ?? HitLayout.NORMAL)), [get]);
47
47
  // Search result list item renderer.
48
48
  return (_jsx(Box, { id: hit.howler.id, onAuxClick: e => checkMiddleClick(e, hit.howler.id), onClick: ev => onClick(ev, hit), sx: [
49
49
  {
@@ -91,14 +91,12 @@ const SearchPane = () => {
91
91
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
92
92
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
93
93
  const error = useContextSelector(HitSearchContext, ctx => ctx.error);
94
- const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
95
94
  const { onClick } = useHitSelection();
96
95
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
97
96
  const clearSelectedHits = useContextSelector(HitContext, ctx => ctx.clearSelectedHits);
98
97
  const bundleHit = useContextSelector(HitContext, ctx => location.pathname.startsWith('/bundles') ? ctx.hits[routeParams.id] : null);
99
98
  const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
100
99
  const verticalSorters = useMediaQuery('(max-width: 1919px)') || (searchPaneWidth ?? Number.MAX_SAFE_INTEGER) < 900;
101
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
102
100
  const getSelectedId = useCallback((event) => {
103
101
  const target = event.target;
104
102
  const selectedElement = target.closest('[id]');
@@ -113,14 +111,14 @@ const SearchPane = () => {
113
111
  }
114
112
  // eslint-disable-next-line react-hooks/exhaustive-deps
115
113
  }, [location.pathname, routeParams.id]);
116
- return (_jsx(FlexPort, { id: "hitscrollbar", children: _jsx(PageCenter, { textAlign: "left", mt: 0, mb: 6, ml: 0, mr: 0, maxWidth: "1500px", children: _jsxs(VSBox, { top: 0, children: [_jsxs(Stack, { ml: -1, mr: -1, sx: { '& .overflowingContentWidgets > *': { zIndex: '2000 !important' } }, spacing: 1, children: [_jsx(ViewLink, {}), bundleHit && (_jsx(BundleScroller, { children: _jsx(HitContextMenu, { getSelectedId: () => bundleHit.howler.id, children: _jsx(Stack, { spacing: 1, sx: { mx: -1 }, children: _jsx(HowlerCard, { sx: [
114
+ return (_jsx(FlexPort, { id: "hitscrollbar", children: _jsx(PageCenter, { textAlign: "left", mt: 0, mb: 6, ml: 0, mr: 0, maxWidth: "1500px", children: _jsxs(VSBox, { top: 0, children: [_jsxs(Stack, { ml: -1, mr: -1, sx: { '& .overflowingContentWidgets > *': { zIndex: '2000 !important' } }, spacing: 1, children: [bundleHit && (_jsx(BundleScroller, { children: _jsx(HitContextMenu, { getSelectedId: () => bundleHit.howler.id, children: _jsx(Stack, { spacing: 1, sx: { mx: -1 }, children: _jsx(HowlerCard, { sx: [
117
115
  { p: 1, border: '4px solid transparent', cursor: 'pointer' },
118
116
  location.pathname.startsWith('/bundles') &&
119
117
  selected === routeParams.id && { borderColor: 'primary.main' }
120
118
  ], onClick: () => {
121
119
  clearSelectedHits(bundleHit.howler.id);
122
120
  setSelected(bundleHit.howler.id);
123
- }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 999 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { disabled: viewId && !selectedView, searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
121
+ }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
124
122
  position: 'absolute',
125
123
  left: 0,
126
124
  right: 0,
@@ -1,2 +1,5 @@
1
- declare const _default: import("react").NamedExoticComponent<{}>;
1
+ declare const _default: import("react").NamedExoticComponent<{
2
+ id: number;
3
+ viewId: string;
4
+ }>;
2
5
  export default _default;