@cccsaurora/howler-ui 2.17.0-dev.600 → 2.17.1
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/api/index.d.ts +0 -2
- package/api/index.js +2 -4
- package/api/search/index.d.ts +1 -2
- package/api/search/index.js +1 -2
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +0 -14
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/HitSearchProvider.d.ts +1 -0
- package/components/app/providers/HitSearchProvider.js +11 -6
- package/components/app/providers/HitSearchProvider.test.js +32 -11
- package/components/app/providers/LocalStorageProvider.js +1 -1
- package/components/app/providers/ParameterProvider.d.ts +2 -9
- package/components/app/providers/ParameterProvider.js +240 -165
- package/components/app/providers/ParameterProvider.test.js +14 -307
- package/components/elements/EditRow.d.ts +4 -1
- package/components/elements/EditRow.js +4 -4
- package/components/elements/PluginTypography.d.ts +1 -2
- package/components/elements/PluginTypography.js +2 -3
- package/components/elements/UserList.d.ts +2 -5
- package/components/elements/UserList.js +5 -14
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/ChipPopper.js +1 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +0 -1
- package/components/elements/display/icons/BundleButton.d.ts +6 -0
- package/components/elements/display/icons/BundleButton.js +32 -0
- package/components/elements/hit/HitBanner.js +48 -28
- package/components/elements/hit/HitCard.js +1 -1
- package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
- package/components/elements/hit/HitOutline.js +7 -3
- package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
- package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
- package/components/elements/hit/HitRelated.d.ts +1 -1
- package/components/elements/hit/HitRelated.js +3 -30
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/PivotLink.js +1 -1
- package/components/elements/hit/related/RelatedLink.d.ts +1 -0
- package/components/elements/hit/related/RelatedLink.js +2 -2
- package/components/elements/view/ViewTitle.js +1 -1
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +2 -2
- package/components/hooks/useHitSelection.js +24 -3
- package/components/hooks/useLocalStorage.d.ts +13 -0
- package/components/hooks/useLocalStorage.js +53 -0
- package/components/hooks/useLocalStorageItem.d.ts +18 -0
- package/components/hooks/useLocalStorageItem.js +78 -0
- package/components/hooks/useLocalStorageItem.test.d.ts +1 -0
- package/components/hooks/useLocalStorageItem.test.js +144 -0
- package/components/hooks/useMyLocalStorage.js +2 -2
- package/components/hooks/useMyPreferences.js +1 -10
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +1 -4
- package/components/hooks/useMyTheme.js +2 -9
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/BundleDocumentation.d.ts +3 -0
- package/components/routes/help/BundleDocumentation.js +12 -0
- package/components/routes/help/HitDocumentation.js +3 -1
- package/components/routes/help/markdown/en/bundles.md.js +1 -0
- package/components/routes/help/markdown/fr/bundles.md.js +1 -0
- package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
- package/components/routes/hits/search/BundleParentMenu.js +32 -0
- package/components/routes/hits/search/HitContextMenu.js +2 -3
- package/components/routes/hits/search/InformationPane.d.ts +0 -1
- package/components/routes/hits/search/InformationPane.js +28 -6
- package/components/routes/hits/search/LayoutSettings.d.ts +3 -0
- package/components/routes/hits/search/LayoutSettings.js +18 -0
- package/components/routes/hits/search/QuerySettings.js +1 -2
- package/components/routes/hits/search/QuerySettings.test.js +9 -14
- package/components/routes/hits/search/SearchPane.js +37 -13
- package/components/routes/hits/search/ViewLink.js +1 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
- package/components/routes/hits/view/HitViewer.js +4 -3
- package/components/routes/home/AnalyticCard.d.ts +2 -3
- package/components/routes/home/AnalyticCard.js +2 -2
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/home/ViewRefresh.d.ts +23 -0
- package/components/routes/home/ViewRefresh.js +67 -0
- package/components/routes/home/index.js +9 -46
- package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/settings/LocalSection.js +2 -1
- package/locales/en/translation.json +6 -42
- package/locales/fr/translation.json +4 -35
- package/models/WithMetadata.d.ts +1 -2
- package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
- package/models/entities/generated/Howler.d.ts +4 -0
- package/models/entities/generated/Rule.d.ts +10 -2
- package/models/entities/generated/Threat.d.ts +2 -2
- package/package.json +1 -16
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +1 -2
- package/utils/constants.d.ts +4 -3
- package/utils/constants.js +1 -0
- package/api/search/case.d.ts +0 -4
- package/api/search/case.js +0 -8
- package/api/v2/case/index.d.ts +0 -6
- package/api/v2/case/index.js +0 -18
- package/api/v2/index.d.ts +0 -4
- package/api/v2/index.js +0 -6
- package/api/v2/search/facet.d.ts +0 -3
- package/api/v2/search/facet.js +0 -12
- package/api/v2/search/index.d.ts +0 -5
- package/api/v2/search/index.js +0 -24
- package/components/elements/ObjectDetails.d.ts +0 -6
- package/components/elements/case/CaseCard.d.ts +0 -8
- package/components/elements/case/CaseCard.js +0 -39
- package/components/elements/case/CasePreview.d.ts +0 -6
- package/components/elements/case/CasePreview.js +0 -17
- package/components/elements/case/StatusIcon.d.ts +0 -5
- package/components/elements/case/StatusIcon.js +0 -13
- package/components/elements/hit/elements/AnalyticLink.d.ts +0 -8
- package/components/elements/hit/elements/AnalyticLink.js +0 -22
- package/components/elements/hit/related/RelatedRecords.js +0 -63
- package/components/elements/observable/ObservableCard.d.ts +0 -5
- package/components/elements/observable/ObservableCard.js +0 -7
- package/components/elements/observable/ObservablePreview.d.ts +0 -6
- package/components/elements/observable/ObservablePreview.js +0 -12
- package/components/hooks/useRelatedRecords.d.ts +0 -13
- package/components/hooks/useRelatedRecords.js +0 -32
- package/components/routes/cases/CaseViewer.d.ts +0 -2
- package/components/routes/cases/CaseViewer.js +0 -24
- package/components/routes/cases/Cases.d.ts +0 -2
- package/components/routes/cases/Cases.js +0 -101
- package/components/routes/cases/constants.d.ts +0 -5
- package/components/routes/cases/constants.js +0 -5
- package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
- package/components/routes/cases/detail/AlertPanel.js +0 -32
- package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
- package/components/routes/cases/detail/CaseDashboard.js +0 -49
- package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
- package/components/routes/cases/detail/CaseDetails.js +0 -61
- package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
- package/components/routes/cases/detail/CaseOverview.js +0 -43
- package/components/routes/cases/detail/CaseSidebar.d.ts +0 -6
- package/components/routes/cases/detail/CaseSidebar.js +0 -36
- package/components/routes/cases/detail/CaseTask.d.ts +0 -11
- package/components/routes/cases/detail/CaseTask.js +0 -57
- package/components/routes/cases/detail/ItemPage.d.ts +0 -6
- package/components/routes/cases/detail/ItemPage.js +0 -93
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
- package/components/routes/cases/detail/RelatedCasePanel.js +0 -31
- package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
- package/components/routes/cases/detail/TaskPanel.js +0 -52
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -12
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -19
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -27
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -12
- package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -179
- package/components/routes/cases/detail/sidebar/types.d.ts +0 -3
- package/components/routes/cases/hooks/useCase.d.ts +0 -13
- package/components/routes/cases/hooks/useCase.js +0 -38
- package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
- package/components/routes/cases/modals/ResolveModal.js +0 -59
- package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
- package/components/routes/hits/search/shared/IndexPicker.js +0 -20
- package/components/routes/observables/ObservableViewer.d.ts +0 -7
- package/components/routes/observables/ObservableViewer.js +0 -27
- package/models/entities/generated/AttachmentsFile.d.ts +0 -12
- package/models/entities/generated/Case.d.ts +0 -28
- package/models/entities/generated/DestinationOriginal.d.ts +0 -19
- package/models/entities/generated/EmailAttachment.d.ts +0 -8
- package/models/entities/generated/EmailParent.d.ts +0 -19
- package/models/entities/generated/Enrichments.d.ts +0 -7
- package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
- package/models/entities/generated/HttpResponse.d.ts +0 -11
- package/models/entities/generated/Item.d.ts +0 -9
- package/models/entities/generated/Observable.d.ts +0 -84
- package/models/entities/generated/ObservableCloud.d.ts +0 -20
- package/models/entities/generated/ObservableDestination.d.ts +0 -23
- package/models/entities/generated/ObservableEmail.d.ts +0 -30
- package/models/entities/generated/ObservableFile.d.ts +0 -36
- package/models/entities/generated/ObservableHowler.d.ts +0 -44
- package/models/entities/generated/ObservableHttp.d.ts +0 -11
- package/models/entities/generated/ObservableObserver.d.ts +0 -21
- package/models/entities/generated/ObservableOrganization.d.ts +0 -7
- package/models/entities/generated/ObservableProcess.d.ts +0 -34
- package/models/entities/generated/ObservableSource.d.ts +0 -23
- package/models/entities/generated/ObservableThreat.d.ts +0 -21
- package/models/entities/generated/ObservableTls.d.ts +0 -12
- package/models/entities/generated/ObserverIngress.d.ts +0 -9
- package/models/entities/generated/Task.d.ts +0 -10
- package/utils/typeUtils.d.ts +0 -7
- package/utils/typeUtils.js +0 -18
- /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
- /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
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';
|
|
@@ -9,10 +9,12 @@ export const ParameterContext = createContext(null);
|
|
|
9
9
|
const DEFAULT_VALUES = {
|
|
10
10
|
query: DEFAULT_QUERY,
|
|
11
11
|
sort: 'event.created desc',
|
|
12
|
-
span: 'date.range.1.month'
|
|
13
|
-
indexes: ['hit']
|
|
12
|
+
span: 'date.range.1.month'
|
|
14
13
|
};
|
|
15
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Mapping of URL parameter keys to internal state keys
|
|
16
|
+
* Note: 'filters' is handled separately due to multi-value support
|
|
17
|
+
*/
|
|
16
18
|
const PARAM_MAPPINGS = [
|
|
17
19
|
['query', 'query'],
|
|
18
20
|
['sort', 'sort'],
|
|
@@ -20,21 +22,15 @@ const PARAM_MAPPINGS = [
|
|
|
20
22
|
['start_date', 'startDate'],
|
|
21
23
|
['end_date', 'endDate']
|
|
22
24
|
];
|
|
23
|
-
/** Multi-value URL params that map to array state keys */
|
|
24
|
-
const ARRAY_PARAMS = [
|
|
25
|
-
{ urlKey: 'filter', stateKey: 'filters' },
|
|
26
|
-
{ urlKey: 'view', stateKey: 'views' },
|
|
27
|
-
{ urlKey: 'index', stateKey: 'indexes', default: DEFAULT_VALUES.indexes }
|
|
28
|
-
];
|
|
29
|
-
const ARRAY_URL_KEYS = new Set(ARRAY_PARAMS.map(p => p.urlKey));
|
|
30
25
|
const WRITE_THROTTLER = new Throttler(100);
|
|
31
26
|
/**
|
|
32
27
|
* Helper function to convert a number/string representation of a number into a valid offset.
|
|
33
28
|
* @returns
|
|
34
29
|
*/
|
|
35
30
|
const parseOffset = (_offset) => {
|
|
36
|
-
if (typeof _offset === 'number')
|
|
31
|
+
if (typeof _offset === 'number') {
|
|
37
32
|
return _offset;
|
|
33
|
+
}
|
|
38
34
|
const candidate = parseInt(_offset);
|
|
39
35
|
return isNaN(candidate) ? 0 : candidate;
|
|
40
36
|
};
|
|
@@ -42,131 +38,261 @@ const parseOffset = (_offset) => {
|
|
|
42
38
|
* Helper function to determine the selected value based on URL params and route context.
|
|
43
39
|
*/
|
|
44
40
|
const getSelectedValue = (params, pathname, bundleId) => {
|
|
45
|
-
if (params.has('selected'))
|
|
41
|
+
if (params.has('selected')) {
|
|
46
42
|
return params.get('selected');
|
|
47
|
-
|
|
43
|
+
}
|
|
44
|
+
if (pathname.startsWith('/bundles') && bundleId) {
|
|
48
45
|
return bundleId;
|
|
46
|
+
}
|
|
49
47
|
return null;
|
|
50
48
|
};
|
|
51
49
|
/**
|
|
52
|
-
*
|
|
53
|
-
* SearchValues. All returned functions are memoized; since _setValues (from useState)
|
|
54
|
-
* and key are both stable for the lifetime of the component, the deps array is empty.
|
|
55
|
-
*/
|
|
56
|
-
const useListHandlers = (key, _setValues) => {
|
|
57
|
-
const add = useCallback((item) => _setValues(c => ({ ...c, [key]: uniq([...c[key], item]) })),
|
|
58
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
-
[]);
|
|
60
|
-
const remove = useCallback((item) => _setValues(c => {
|
|
61
|
-
const arr = c[key];
|
|
62
|
-
const i = arr.indexOf(item);
|
|
63
|
-
return i === -1 ? c : { ...c, [key]: arr.filter((_, idx) => idx !== i) };
|
|
64
|
-
}),
|
|
65
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
[]);
|
|
67
|
-
const setAt = useCallback((pos, item) => _setValues(c => {
|
|
68
|
-
const arr = c[key];
|
|
69
|
-
if (pos < 0 || pos >= arr.length)
|
|
70
|
-
return c;
|
|
71
|
-
const next = [...arr];
|
|
72
|
-
next[pos] = item;
|
|
73
|
-
return { ...c, [key]: next };
|
|
74
|
-
}),
|
|
75
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
-
[]);
|
|
77
|
-
const setAll = useCallback((items) => _setValues(c => ({ ...c, [key]: uniq(items) })),
|
|
78
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
-
[]);
|
|
80
|
-
const reset = useCallback((defaultValue = []) => _setValues(c => ({ ...c, [key]: defaultValue })),
|
|
81
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
-
[]);
|
|
83
|
-
return { add, remove, setAt, setAll, reset };
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* Synchronizes SearchValues state with the URL search string, and vice-versa.
|
|
50
|
+
* Context responsible for tracking updates to query operations in hit and view search.
|
|
87
51
|
*/
|
|
88
|
-
const
|
|
52
|
+
const ParameterProvider = ({ children }) => {
|
|
53
|
+
const location = useLocation();
|
|
54
|
+
const routeParams = useParams();
|
|
55
|
+
const [params, setParams] = useSearchParams();
|
|
56
|
+
const pendingChanges = useRef({});
|
|
57
|
+
const [values, _setValues] = useState({
|
|
58
|
+
selected: getSelectedValue(params, location.pathname, routeParams.id),
|
|
59
|
+
query: params.get('query') ?? DEFAULT_VALUES.query,
|
|
60
|
+
sort: params.get('sort') ?? DEFAULT_VALUES.sort,
|
|
61
|
+
span: params.get('span') ?? DEFAULT_VALUES.span,
|
|
62
|
+
filters: params.getAll('filter'),
|
|
63
|
+
views: params.getAll('view'),
|
|
64
|
+
startDate: params.get('start_date'),
|
|
65
|
+
endDate: params.get('end_date'),
|
|
66
|
+
offset: parseOffset(params.get('offset')),
|
|
67
|
+
trackTotalHits: (params.get('track_total_hits') ?? 'false') !== 'false'
|
|
68
|
+
});
|
|
69
|
+
// TODO: SELECTING A BUNDLE STILL CAUSES A FREAKOUT
|
|
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
|
+
}
|
|
81
|
+
if (value === values[key]) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key === 'selected') {
|
|
85
|
+
pendingChanges.current.selected = value;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
|
|
89
|
+
}
|
|
90
|
+
if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
|
|
91
|
+
pendingChanges.current.startDate = null;
|
|
92
|
+
pendingChanges.current.endDate = null;
|
|
93
|
+
}
|
|
94
|
+
WRITE_THROTTLER.debounce(() => {
|
|
95
|
+
_setValues(_current => ({ ..._current, ...pendingChanges.current }));
|
|
96
|
+
pendingChanges.current = {};
|
|
97
|
+
});
|
|
98
|
+
}, [values]);
|
|
99
|
+
const setOffset = useCallback(_offset => _setValues(_current => ({ ..._current, offset: parseOffset(_offset) })), []);
|
|
100
|
+
const setCustomSpan = useCallback((startDate, endDate) => {
|
|
101
|
+
_setValues(_values => ({
|
|
102
|
+
..._values,
|
|
103
|
+
startDate,
|
|
104
|
+
endDate
|
|
105
|
+
}));
|
|
106
|
+
}, []);
|
|
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;
|
|
121
|
+
}
|
|
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;
|
|
133
|
+
}
|
|
134
|
+
const newFilters = [..._current.filters];
|
|
135
|
+
newFilters[index] = filter;
|
|
136
|
+
return {
|
|
137
|
+
..._current,
|
|
138
|
+
filters: newFilters
|
|
139
|
+
};
|
|
140
|
+
});
|
|
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;
|
|
162
|
+
}
|
|
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;
|
|
174
|
+
}
|
|
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
|
+
*/
|
|
89
193
|
const getUrlFromState = useCallback(() => {
|
|
90
194
|
const changes = {};
|
|
91
|
-
// Scalar params: write if changed from URL, remove if back to default
|
|
92
195
|
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
93
196
|
const stateValue = values[stateKey];
|
|
94
197
|
const urlValue = params.get(urlKey);
|
|
95
|
-
if (stateValue
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
// Array params: skip when state equals default and URL is already empty
|
|
105
|
-
ARRAY_PARAMS.forEach(({ urlKey, stateKey, default: def }) => {
|
|
106
|
-
const stateArr = values[stateKey];
|
|
107
|
-
const urlArr = params.getAll(urlKey);
|
|
108
|
-
if (isEqual(stateArr, urlArr))
|
|
109
|
-
return;
|
|
110
|
-
const isDefault = def ? isEqual(stateArr, def) : stateArr.length === 0;
|
|
111
|
-
if (!isDefault) {
|
|
112
|
-
changes[urlKey] = stateArr.length === 0 ? null : stateArr;
|
|
113
|
-
}
|
|
114
|
-
else if (urlArr.length > 0) {
|
|
115
|
-
changes[urlKey] = null; // state is default but URL isn't — remove
|
|
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
|
+
}
|
|
116
206
|
}
|
|
117
207
|
});
|
|
118
|
-
//
|
|
119
|
-
|
|
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'))) {
|
|
120
223
|
changes.selected = null;
|
|
121
224
|
}
|
|
122
225
|
else if (values.selected !== params.get('selected')) {
|
|
123
226
|
changes.selected = values.selected;
|
|
124
227
|
}
|
|
125
|
-
// offset: remove
|
|
126
|
-
|
|
228
|
+
// Handle offset: remove if 0, otherwise set
|
|
229
|
+
const urlOffset = parseOffset(params.get('offset'));
|
|
230
|
+
if (urlOffset !== values.offset) {
|
|
127
231
|
changes.offset = values.offset || null;
|
|
128
232
|
}
|
|
129
|
-
//
|
|
130
|
-
return omitBy(changes, (val, key) =>
|
|
131
|
-
|
|
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;
|
|
237
|
+
}
|
|
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
|
+
*/
|
|
132
244
|
const getStateFromUrl = useCallback(() => {
|
|
133
245
|
const changes = {};
|
|
134
|
-
// Scalar params: fall back to default when absent from URL
|
|
135
246
|
PARAM_MAPPINGS.forEach(([urlKey, stateKey]) => {
|
|
136
247
|
const urlValue = params.has(urlKey) ? params.get(urlKey) : (DEFAULT_VALUES[stateKey] ?? undefined);
|
|
137
248
|
if (urlValue !== values[stateKey]) {
|
|
138
249
|
changes[stateKey] = urlValue;
|
|
139
250
|
}
|
|
140
251
|
});
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
252
|
+
// Handle filters: compare arrays with isEqual
|
|
253
|
+
const urlFilters = uniq(params.getAll('filter'));
|
|
254
|
+
if (!isEqual(urlFilters, values.filters)) {
|
|
255
|
+
changes.filters = urlFilters;
|
|
256
|
+
}
|
|
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) {
|
|
152
265
|
changes.selected = selectedValue;
|
|
153
|
-
|
|
266
|
+
}
|
|
267
|
+
// Handle offset
|
|
154
268
|
const urlOffset = parseOffset(params.get('offset'));
|
|
155
|
-
if (urlOffset !== values.offset)
|
|
269
|
+
if (urlOffset !== values.offset) {
|
|
156
270
|
changes.offset = urlOffset;
|
|
271
|
+
}
|
|
272
|
+
// Filter out undefined values and values that already match state
|
|
157
273
|
return omitBy(omitBy(changes, isUndefined), (val, key) => val == values[key]);
|
|
158
|
-
}, [values, params, pathname,
|
|
159
|
-
|
|
274
|
+
}, [values, params, location.pathname, routeParams.id]);
|
|
275
|
+
/**
|
|
276
|
+
* Effect to synchronize the context's state with the address bar
|
|
277
|
+
*/
|
|
160
278
|
useEffect(() => {
|
|
161
279
|
const changes = getUrlFromState();
|
|
162
|
-
if (isEmpty(changes))
|
|
280
|
+
if (isEmpty(changes)) {
|
|
163
281
|
return;
|
|
282
|
+
}
|
|
164
283
|
setParams(_params => {
|
|
284
|
+
// Build fresh URLSearchParams from existing params
|
|
165
285
|
const newParams = new URLSearchParams(_params);
|
|
286
|
+
// Handle standard params
|
|
166
287
|
Object.entries(changes).forEach(([key, value]) => {
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
170
296
|
}
|
|
171
297
|
else if (value === null || value === undefined) {
|
|
172
298
|
newParams.delete(key);
|
|
@@ -179,63 +305,17 @@ const useUrlSync = (values, _setValues, params, setParams, pathname, search, rou
|
|
|
179
305
|
}, { replace: !changes.query && !Object.keys(changes).includes('offset') });
|
|
180
306
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
181
307
|
}, [values]);
|
|
182
|
-
// URL → State
|
|
183
308
|
useEffect(() => {
|
|
184
309
|
const changes = getStateFromUrl();
|
|
185
|
-
if (isEmpty(changes))
|
|
310
|
+
if (isEmpty(changes)) {
|
|
186
311
|
return;
|
|
187
|
-
_setValues(c => ({ ...c, ...changes }));
|
|
188
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
189
|
-
}, [search, pathname, routeId]);
|
|
190
|
-
};
|
|
191
|
-
/**
|
|
192
|
-
* Context responsible for tracking updates to query operations in hit and view search.
|
|
193
|
-
*/
|
|
194
|
-
const ParameterProvider = ({ children }) => {
|
|
195
|
-
const location = useLocation();
|
|
196
|
-
const routeParams = useParams();
|
|
197
|
-
const [params, setParams] = useSearchParams();
|
|
198
|
-
const pendingChanges = useRef({});
|
|
199
|
-
const [values, _setValues] = useState({
|
|
200
|
-
selected: getSelectedValue(params, location.pathname, routeParams.id),
|
|
201
|
-
query: params.get('query') ?? DEFAULT_VALUES.query,
|
|
202
|
-
sort: params.get('sort') ?? DEFAULT_VALUES.sort,
|
|
203
|
-
span: params.get('span') ?? DEFAULT_VALUES.span,
|
|
204
|
-
indexes: params.has('index')
|
|
205
|
-
? uniq(params.getAll('index')).filter(identity)
|
|
206
|
-
: DEFAULT_VALUES.indexes,
|
|
207
|
-
filters: params.getAll('filter'),
|
|
208
|
-
views: params.getAll('view'),
|
|
209
|
-
startDate: params.get('start_date'),
|
|
210
|
-
endDate: params.get('end_date'),
|
|
211
|
-
offset: parseOffset(params.get('offset')),
|
|
212
|
-
trackTotalHits: (params.get('track_total_hits') ?? 'false') !== 'false'
|
|
213
|
-
});
|
|
214
|
-
// TODO: SELECTING A BUNDLE STILL CAUSES A FREAKOUT
|
|
215
|
-
useUrlSync(values, _setValues, params, setParams, location.pathname, location.search, routeParams.id);
|
|
216
|
-
const set = useCallback((key) => (value) => {
|
|
217
|
-
if (value === values[key])
|
|
218
|
-
return;
|
|
219
|
-
if (key === 'selected') {
|
|
220
|
-
pendingChanges.current.selected = value;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
pendingChanges.current[key] = value ?? DEFAULT_VALUES[key] ?? null;
|
|
224
312
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
pendingChanges.current = {};
|
|
232
|
-
});
|
|
233
|
-
}, [values]);
|
|
234
|
-
const setOffset = useCallback((_offset) => _setValues(c => ({ ...c, offset: parseOffset(_offset) })), []);
|
|
235
|
-
const setCustomSpan = useCallback((startDate, endDate) => _setValues(c => ({ ...c, startDate, endDate })), []);
|
|
236
|
-
const filters = useListHandlers('filters', _setValues);
|
|
237
|
-
const indexes = useListHandlers('indexes', _setValues);
|
|
238
|
-
const views = useListHandlers('views', _setValues);
|
|
313
|
+
_setValues(_current => ({
|
|
314
|
+
..._current,
|
|
315
|
+
...changes
|
|
316
|
+
}));
|
|
317
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
318
|
+
}, [location.search, location.pathname, routeParams.id]);
|
|
239
319
|
return (_jsx(ParameterContext.Provider, { value: {
|
|
240
320
|
...values,
|
|
241
321
|
setOffset,
|
|
@@ -244,19 +324,14 @@ const ParameterProvider = ({ children }) => {
|
|
|
244
324
|
setQuery: useMemo(() => set('query'), [set]),
|
|
245
325
|
setSort: useMemo(() => set('sort'), [set]),
|
|
246
326
|
setSpan: useMemo(() => set('span'), [set]),
|
|
247
|
-
addFilter
|
|
248
|
-
removeFilter
|
|
249
|
-
setFilter
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
resetIndexes: useCallback(() => indexes.reset(DEFAULT_VALUES.indexes), [indexes]),
|
|
256
|
-
addView: views.add,
|
|
257
|
-
removeView: views.remove,
|
|
258
|
-
setView: views.setAt,
|
|
259
|
-
resetViews: views.reset
|
|
327
|
+
addFilter,
|
|
328
|
+
removeFilter,
|
|
329
|
+
setFilter,
|
|
330
|
+
clearFilters,
|
|
331
|
+
addView,
|
|
332
|
+
removeView,
|
|
333
|
+
setView,
|
|
334
|
+
clearViews
|
|
260
335
|
}, children: children }));
|
|
261
336
|
};
|
|
262
337
|
export const useParameterContextSelector = (selector) => {
|