@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.
- package/components/app/App.js +2 -0
- package/components/app/hooks/useMatchers.js +0 -4
- package/components/app/providers/FavouritesProvider.js +2 -1
- package/components/app/providers/FieldProvider.d.ts +2 -2
- package/components/app/providers/HitProvider.d.ts +3 -3
- package/components/app/providers/HitSearchProvider.d.ts +7 -8
- package/components/app/providers/HitSearchProvider.js +64 -39
- package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
- package/components/app/providers/HitSearchProvider.test.js +505 -0
- package/components/app/providers/ParameterProvider.d.ts +13 -5
- package/components/app/providers/ParameterProvider.js +240 -84
- package/components/app/providers/ParameterProvider.test.d.ts +1 -0
- package/components/app/providers/ParameterProvider.test.js +1041 -0
- package/components/app/providers/ViewProvider.d.ts +3 -2
- package/components/app/providers/ViewProvider.js +21 -14
- package/components/app/providers/ViewProvider.test.js +19 -29
- package/components/elements/display/ChipPopper.d.ts +21 -0
- package/components/elements/display/ChipPopper.js +36 -0
- package/components/elements/display/ChipPopper.test.d.ts +1 -0
- package/components/elements/display/ChipPopper.test.js +309 -0
- package/components/elements/hit/HitActions.js +3 -3
- package/components/elements/hit/HitSummary.d.ts +0 -1
- package/components/elements/hit/HitSummary.js +11 -21
- package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
- package/components/elements/hit/aggregate/HitGraph.js +9 -15
- package/components/routes/dossiers/DossierCard.test.js +0 -2
- package/components/routes/dossiers/DossierEditor.test.js +27 -33
- package/components/routes/hits/search/HitBrowser.js +7 -48
- package/components/routes/hits/search/HitContextMenu.test.js +11 -29
- package/components/routes/hits/search/InformationPane.js +1 -1
- package/components/routes/hits/search/QuerySettings.js +30 -0
- package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
- package/components/routes/hits/search/QuerySettings.test.js +553 -0
- package/components/routes/hits/search/SearchPane.js +8 -10
- package/components/routes/hits/search/ViewLink.d.ts +4 -1
- package/components/routes/hits/search/ViewLink.js +37 -19
- package/components/routes/hits/search/ViewLink.test.js +349 -303
- package/components/routes/hits/search/grid/HitGrid.js +2 -6
- package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
- package/components/routes/hits/search/shared/HitFilter.js +31 -23
- package/components/routes/hits/search/shared/HitSort.js +16 -8
- package/components/routes/hits/search/shared/SearchSpan.js +19 -10
- package/components/routes/views/ViewComposer.js +7 -6
- package/components/routes/views/Views.js +2 -1
- package/locales/en/translation.json +6 -0
- package/locales/fr/translation.json +6 -0
- package/package.json +2 -2
- package/setupTests.js +4 -1
- package/tests/mocks.d.ts +18 -0
- package/tests/mocks.js +65 -0
- package/tests/server-handlers.js +10 -28
- package/utils/viewUtils.d.ts +2 -0
- package/utils/viewUtils.js +11 -0
- package/components/routes/hits/search/shared/QuerySettings.js +0 -22
- /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
- /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
- /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&ersands', '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&ersands');
|
|
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
|
|
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: [
|
|
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:
|
|
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,
|