@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { isEmpty,
|
|
2
|
+
import { isEmpty, isEqual, isUndefined, omitBy, uniq } from 'lodash-es';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
|
5
5
|
import { createContext, useContextSelector } from 'use-context-selector';
|
|
@@ -11,6 +11,17 @@ const DEFAULT_VALUES = {
|
|
|
11
11
|
sort: 'event.created desc',
|
|
12
12
|
span: 'date.range.1.month'
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Mapping of URL parameter keys to internal state keys
|
|
16
|
+
* Note: 'filters' is handled separately due to multi-value support
|
|
17
|
+
*/
|
|
18
|
+
const PARAM_MAPPINGS = [
|
|
19
|
+
['query', 'query'],
|
|
20
|
+
['sort', 'sort'],
|
|
21
|
+
['span', 'span'],
|
|
22
|
+
['start_date', 'startDate'],
|
|
23
|
+
['end_date', 'endDate']
|
|
24
|
+
];
|
|
14
25
|
const WRITE_THROTTLER = new Throttler(100);
|
|
15
26
|
/**
|
|
16
27
|
* Helper function to convert a number/string representation of a number into a valid offset.
|
|
@@ -23,6 +34,18 @@ const parseOffset = (_offset) => {
|
|
|
23
34
|
const candidate = parseInt(_offset);
|
|
24
35
|
return isNaN(candidate) ? 0 : candidate;
|
|
25
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Helper function to determine the selected value based on URL params and route context.
|
|
39
|
+
*/
|
|
40
|
+
const getSelectedValue = (params, pathname, bundleId) => {
|
|
41
|
+
if (params.has('selected')) {
|
|
42
|
+
return params.get('selected');
|
|
43
|
+
}
|
|
44
|
+
if (pathname.startsWith('/bundles') && bundleId) {
|
|
45
|
+
return bundleId;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
26
49
|
/**
|
|
27
50
|
* Context responsible for tracking updates to query operations in hit and view search.
|
|
28
51
|
*/
|
|
@@ -32,28 +55,39 @@ const ParameterProvider = ({ children }) => {
|
|
|
32
55
|
const [params, setParams] = useSearchParams();
|
|
33
56
|
const pendingChanges = useRef({});
|
|
34
57
|
const [values, _setValues] = useState({
|
|
35
|
-
selected: params.
|
|
58
|
+
selected: getSelectedValue(params, location.pathname, routeParams.id),
|
|
36
59
|
query: params.get('query') ?? DEFAULT_VALUES.query,
|
|
37
60
|
sort: params.get('sort') ?? DEFAULT_VALUES.sort,
|
|
38
61
|
span: params.get('span') ?? DEFAULT_VALUES.span,
|
|
39
|
-
|
|
62
|
+
filters: params.getAll('filter'),
|
|
63
|
+
views: params.getAll('view'),
|
|
40
64
|
startDate: params.get('start_date'),
|
|
41
65
|
endDate: params.get('end_date'),
|
|
42
66
|
offset: parseOffset(params.get('offset')),
|
|
43
67
|
trackTotalHits: (params.get('track_total_hits') ?? 'false') !== 'false'
|
|
44
68
|
});
|
|
45
69
|
// TODO: SELECTING A BUNDLE STILL CAUSES A FREAKOUT
|
|
46
|
-
const set = useCallback(
|
|
70
|
+
const set = useCallback(key => value => {
|
|
71
|
+
if (key === 'filters') {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.error('Cannot use set() for filters. Use addFilter/removeFilter/clearFilters instead.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (key === 'views') {
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.error('Cannot use set() for views. Use addView/removeView/clearViews instead.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
47
81
|
if (value === values[key]) {
|
|
48
82
|
return;
|
|
49
83
|
}
|
|
50
|
-
if (key === 'selected' && !value
|
|
51
|
-
pendingChanges.current
|
|
84
|
+
if (key === 'selected' && !value) {
|
|
85
|
+
pendingChanges.current.selected = getSelectedValue(params, location.pathname, routeParams.id);
|
|
52
86
|
}
|
|
53
87
|
else {
|
|
54
88
|
pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
|
|
55
89
|
}
|
|
56
|
-
if (key === 'span' && !value.endsWith('custom')) {
|
|
90
|
+
if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
|
|
57
91
|
pendingChanges.current.startDate = null;
|
|
58
92
|
pendingChanges.current.endDate = null;
|
|
59
93
|
}
|
|
@@ -61,7 +95,7 @@ const ParameterProvider = ({ children }) => {
|
|
|
61
95
|
_setValues(_current => ({ ..._current, ...pendingChanges.current }));
|
|
62
96
|
pendingChanges.current = {};
|
|
63
97
|
});
|
|
64
|
-
}, [location.pathname, routeParams.id, values]);
|
|
98
|
+
}, [location.pathname, routeParams.id, values, params]);
|
|
65
99
|
const setOffset = useCallback(_offset => _setValues(_current => ({ ..._current, offset: parseOffset(_offset) })), []);
|
|
66
100
|
const setCustomSpan = useCallback((startDate, endDate) => {
|
|
67
101
|
_setValues(_values => ({
|
|
@@ -70,101 +104,216 @@ const ParameterProvider = ({ children }) => {
|
|
|
70
104
|
endDate
|
|
71
105
|
}));
|
|
72
106
|
}, []);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
[
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Get the value from the URL, using the default values as fallback (and set to undefined if neither is set)
|
|
88
|
-
const fromSearchWithFallback = params.has(key) ? params.get(key) : (DEFAULT_VALUES[key] ?? undefined);
|
|
89
|
-
// If there's a difference between the search value and the value in the internal store, we append the key and new value to changes
|
|
90
|
-
// This is based on the operation
|
|
91
|
-
if (fromSearchWithFallback !== value) {
|
|
92
|
-
changes[key] = operation === 'write' ? value : fromSearchWithFallback;
|
|
107
|
+
/**
|
|
108
|
+
* Filter manipulation
|
|
109
|
+
*/
|
|
110
|
+
const addFilter = useCallback(filter => {
|
|
111
|
+
_setValues(_current => ({
|
|
112
|
+
..._current,
|
|
113
|
+
filters: uniq([..._current.filters, filter])
|
|
114
|
+
}));
|
|
115
|
+
}, []);
|
|
116
|
+
const removeFilter = useCallback(filter => {
|
|
117
|
+
_setValues(_current => {
|
|
118
|
+
const index = _current.filters.indexOf(filter);
|
|
119
|
+
if (index === -1) {
|
|
120
|
+
return _current;
|
|
93
121
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
return {
|
|
123
|
+
..._current,
|
|
124
|
+
filters: _current.filters.filter((_, i) => i !== index)
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}, []);
|
|
128
|
+
const setFilter = useCallback((index, filter) => {
|
|
129
|
+
_setValues(_current => {
|
|
130
|
+
// Validate index
|
|
131
|
+
if (index < 0 || index >= _current.filters.length) {
|
|
132
|
+
return _current;
|
|
103
133
|
}
|
|
134
|
+
const newFilters = [..._current.filters];
|
|
135
|
+
newFilters[index] = filter;
|
|
136
|
+
return {
|
|
137
|
+
..._current,
|
|
138
|
+
filters: newFilters
|
|
139
|
+
};
|
|
104
140
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
}, []);
|
|
142
|
+
const clearFilters = useCallback(() => {
|
|
143
|
+
_setValues(_current => ({
|
|
144
|
+
..._current,
|
|
145
|
+
filters: []
|
|
146
|
+
}));
|
|
147
|
+
}, []);
|
|
148
|
+
/**
|
|
149
|
+
* View manipulation
|
|
150
|
+
*/
|
|
151
|
+
const addView = useCallback(view => {
|
|
152
|
+
_setValues(_current => ({
|
|
153
|
+
..._current,
|
|
154
|
+
views: uniq([..._current.views, view])
|
|
155
|
+
}));
|
|
156
|
+
}, []);
|
|
157
|
+
const removeView = useCallback(view => {
|
|
158
|
+
_setValues(_current => {
|
|
159
|
+
const index = _current.views.indexOf(view);
|
|
160
|
+
if (index === -1) {
|
|
161
|
+
return _current;
|
|
112
162
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
return {
|
|
164
|
+
..._current,
|
|
165
|
+
views: _current.views.filter((_, i) => i !== index)
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}, []);
|
|
169
|
+
const setView = useCallback((index, view) => {
|
|
170
|
+
_setValues(_current => {
|
|
171
|
+
// Validate index
|
|
172
|
+
if (index < 0 || index >= _current.views.length) {
|
|
173
|
+
return _current;
|
|
116
174
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
const newViews = [..._current.views];
|
|
176
|
+
newViews[index] = view;
|
|
177
|
+
return {
|
|
178
|
+
..._current,
|
|
179
|
+
views: newViews
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}, []);
|
|
183
|
+
const clearViews = useCallback(() => {
|
|
184
|
+
_setValues(_current => ({
|
|
185
|
+
..._current,
|
|
186
|
+
views: []
|
|
187
|
+
}));
|
|
188
|
+
}, []);
|
|
189
|
+
/**
|
|
190
|
+
* Get URL parameter changes needed to sync internal state to the address bar.
|
|
191
|
+
* Returns null values for params that should be removed from URL.
|
|
192
|
+
*/
|
|
193
|
+
const getUrlFromState = useCallback(() => {
|
|
194
|
+
const changes = {};
|
|
195
|
+
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
196
|
+
const stateValue = values[stateKey];
|
|
197
|
+
const urlValue = params.get(urlKey);
|
|
198
|
+
if (stateValue !== urlValue) {
|
|
199
|
+
// If the value matches the default, remove it from URL (null signals removal)
|
|
200
|
+
if (params.has(urlKey) && stateValue === DEFAULT_VALUES[stateKey]) {
|
|
201
|
+
changes[urlKey] = null;
|
|
202
|
+
}
|
|
203
|
+
else if (stateValue !== DEFAULT_VALUES[stateKey]) {
|
|
204
|
+
changes[urlKey] = stateValue;
|
|
205
|
+
}
|
|
122
206
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
207
|
+
});
|
|
208
|
+
// Handle filters: compare arrays with isEqual
|
|
209
|
+
const urlFilters = params.getAll('filter');
|
|
210
|
+
if (!isEqual(values.filters, urlFilters)) {
|
|
211
|
+
// Coerce empty array to null for removal signal
|
|
212
|
+
changes.filters = values.filters.length === 0 ? null : values.filters;
|
|
213
|
+
}
|
|
214
|
+
// Handle views: compare arrays with isEqual
|
|
215
|
+
const urlViews = params.getAll('view');
|
|
216
|
+
if (!isEqual(values.views, urlViews)) {
|
|
217
|
+
// Coerce empty array to null for removal signal
|
|
218
|
+
changes.views = values.views.length === 0 ? null : values.views;
|
|
219
|
+
}
|
|
220
|
+
// Handle selected: remove if redundant in bundle context, otherwise set
|
|
221
|
+
if (location.pathname.startsWith('/bundles') &&
|
|
222
|
+
(!params.has('selected') || values.selected === params.get('selected'))) {
|
|
223
|
+
changes.selected = null;
|
|
224
|
+
}
|
|
225
|
+
else if (values.selected !== params.get('selected')) {
|
|
226
|
+
changes.selected = values.selected;
|
|
227
|
+
}
|
|
228
|
+
// Handle offset: remove if 0, otherwise set
|
|
229
|
+
const urlOffset = parseOffset(params.get('offset'));
|
|
230
|
+
if (urlOffset !== values.offset) {
|
|
231
|
+
changes.offset = values.offset || null;
|
|
232
|
+
}
|
|
233
|
+
// Filter out values that already match the URL (skip 'filters', 'views' as they're handled above)
|
|
234
|
+
return omitBy(changes, (val, key) => {
|
|
235
|
+
if (['filters', 'views'].includes(key)) {
|
|
236
|
+
return false;
|
|
126
237
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
238
|
+
return val == params.get(key);
|
|
239
|
+
});
|
|
240
|
+
}, [values, params, location.pathname]);
|
|
241
|
+
/**
|
|
242
|
+
* Get state changes needed to sync URL parameters to internal state.
|
|
243
|
+
*/
|
|
244
|
+
const getStateFromUrl = useCallback(() => {
|
|
245
|
+
const changes = {};
|
|
246
|
+
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
247
|
+
const urlValue = params.has(urlKey) ? params.get(urlKey) : (DEFAULT_VALUES[stateKey] ?? undefined);
|
|
248
|
+
if (urlValue !== values[stateKey]) {
|
|
249
|
+
changes[stateKey] = urlValue;
|
|
130
250
|
}
|
|
251
|
+
});
|
|
252
|
+
// Handle filters: compare arrays with isEqual
|
|
253
|
+
const urlFilters = uniq(params.getAll('filter'));
|
|
254
|
+
if (!isEqual(urlFilters, values.filters)) {
|
|
255
|
+
changes.filters = urlFilters;
|
|
131
256
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
257
|
+
// Handle filters: compare arrays with isEqual
|
|
258
|
+
const urlViews = uniq(params.getAll('view'));
|
|
259
|
+
if (!isEqual(urlViews, values.views)) {
|
|
260
|
+
changes.views = urlViews;
|
|
261
|
+
}
|
|
262
|
+
// Handle selected using helper
|
|
263
|
+
const selectedValue = getSelectedValue(params, location.pathname, routeParams.id);
|
|
264
|
+
if (selectedValue !== values.selected) {
|
|
265
|
+
changes.selected = selectedValue;
|
|
138
266
|
}
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
267
|
+
// Handle offset
|
|
268
|
+
const urlOffset = parseOffset(params.get('offset'));
|
|
269
|
+
if (urlOffset !== values.offset) {
|
|
270
|
+
changes.offset = urlOffset;
|
|
271
|
+
}
|
|
272
|
+
// Filter out undefined values and values that already match state
|
|
273
|
+
return omitBy(omitBy(changes, isUndefined), (val, key) => val == values[key]);
|
|
144
274
|
}, [values, params, location.pathname, routeParams.id]);
|
|
145
275
|
/**
|
|
146
276
|
* Effect to synchronize the context's state with the address bar
|
|
147
277
|
*/
|
|
148
278
|
useEffect(() => {
|
|
149
|
-
const changes =
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
setParams(_params => {
|
|
153
|
-
const newParams = new URLSearchParams({ ...existingParams, ...changes });
|
|
154
|
-
Object.entries(pickBy(changes, isNull)).forEach(([key]) => newParams.delete(key));
|
|
155
|
-
return newParams;
|
|
156
|
-
}, { replace: !changes.query && !Object.keys(changes).includes('offset') });
|
|
279
|
+
const changes = getUrlFromState();
|
|
280
|
+
if (isEmpty(changes)) {
|
|
281
|
+
return;
|
|
157
282
|
}
|
|
283
|
+
setParams(_params => {
|
|
284
|
+
// Build fresh URLSearchParams from existing params
|
|
285
|
+
const newParams = new URLSearchParams(_params);
|
|
286
|
+
// Handle standard params
|
|
287
|
+
Object.entries(changes).forEach(([key, value]) => {
|
|
288
|
+
if (['filters', 'views'].includes(key)) {
|
|
289
|
+
const multiFieldKey = key.replace(/s$/, '');
|
|
290
|
+
// Special handling for arrays
|
|
291
|
+
newParams.delete(multiFieldKey);
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
value.forEach(val => newParams.append(multiFieldKey, val));
|
|
294
|
+
}
|
|
295
|
+
// null/undefined means remove (already deleted above)
|
|
296
|
+
}
|
|
297
|
+
else if (value === null || value === undefined) {
|
|
298
|
+
newParams.delete(key);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
newParams.set(key, String(value));
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return newParams;
|
|
305
|
+
}, { replace: !changes.query && !Object.keys(changes).includes('offset') });
|
|
158
306
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
307
|
}, [values]);
|
|
160
308
|
useEffect(() => {
|
|
161
|
-
const changes =
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
..._current,
|
|
165
|
-
...changes
|
|
166
|
-
}));
|
|
309
|
+
const changes = getStateFromUrl();
|
|
310
|
+
if (isEmpty(changes)) {
|
|
311
|
+
return;
|
|
167
312
|
}
|
|
313
|
+
_setValues(_current => ({
|
|
314
|
+
..._current,
|
|
315
|
+
...changes
|
|
316
|
+
}));
|
|
168
317
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
318
|
}, [location.search, location.pathname, routeParams.id]);
|
|
170
319
|
return (_jsx(ParameterContext.Provider, { value: {
|
|
@@ -175,7 +324,14 @@ const ParameterProvider = ({ children }) => {
|
|
|
175
324
|
setQuery: useMemo(() => set('query'), [set]),
|
|
176
325
|
setSort: useMemo(() => set('sort'), [set]),
|
|
177
326
|
setSpan: useMemo(() => set('span'), [set]),
|
|
178
|
-
|
|
327
|
+
addFilter,
|
|
328
|
+
removeFilter,
|
|
329
|
+
setFilter,
|
|
330
|
+
clearFilters,
|
|
331
|
+
addView,
|
|
332
|
+
removeView,
|
|
333
|
+
setView,
|
|
334
|
+
clearViews
|
|
179
335
|
}, children: children }));
|
|
180
336
|
};
|
|
181
337
|
export const useParameterContextSelector = (selector) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|