@cccsaurora/howler-ui 2.17.0-dev.617 → 2.18.0-dev.626

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/index.d.ts +1 -2
  4. package/api/search/index.js +1 -2
  5. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  6. package/components/app/App.js +0 -14
  7. package/components/app/providers/FavouritesProvider.js +2 -2
  8. package/components/app/providers/HitSearchProvider.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.js +11 -6
  10. package/components/app/providers/HitSearchProvider.test.js +32 -11
  11. package/components/app/providers/LocalStorageProvider.js +1 -1
  12. package/components/app/providers/ParameterProvider.d.ts +2 -9
  13. package/components/app/providers/ParameterProvider.js +240 -165
  14. package/components/app/providers/ParameterProvider.test.js +14 -307
  15. package/components/elements/EditRow.d.ts +4 -1
  16. package/components/elements/EditRow.js +4 -4
  17. package/components/elements/PluginTypography.d.ts +1 -2
  18. package/components/elements/PluginTypography.js +2 -3
  19. package/components/elements/UserList.d.ts +2 -5
  20. package/components/elements/UserList.js +5 -14
  21. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  22. package/components/elements/display/ChipPopper.d.ts +1 -1
  23. package/components/elements/display/ChipPopper.js +1 -1
  24. package/components/elements/display/HowlerCard.js +1 -1
  25. package/components/elements/display/Modal.js +0 -1
  26. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  27. package/components/elements/display/icons/BundleButton.js +32 -0
  28. package/components/elements/hit/HitBanner.js +48 -28
  29. package/components/elements/hit/HitCard.js +1 -1
  30. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  31. package/components/elements/hit/HitOutline.js +7 -3
  32. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  33. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  34. package/components/elements/hit/HitRelated.d.ts +1 -1
  35. package/components/elements/hit/HitRelated.js +3 -30
  36. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  37. package/components/elements/hit/related/PivotLink.js +1 -1
  38. package/components/elements/hit/related/RelatedLink.d.ts +1 -0
  39. package/components/elements/hit/related/RelatedLink.js +2 -2
  40. package/components/elements/view/ViewTitle.js +1 -1
  41. package/components/hooks/useHitActions.d.ts +1 -1
  42. package/components/hooks/useHitActions.js +2 -2
  43. package/components/hooks/useHitSelection.js +24 -3
  44. package/components/hooks/useLocalStorage.d.ts +13 -0
  45. package/components/hooks/useLocalStorage.js +53 -0
  46. package/components/hooks/useLocalStorageItem.d.ts +18 -0
  47. package/components/hooks/useLocalStorageItem.js +78 -0
  48. package/components/hooks/useLocalStorageItem.test.d.ts +1 -0
  49. package/components/hooks/useLocalStorageItem.test.js +144 -0
  50. package/components/hooks/useMyLocalStorage.js +2 -2
  51. package/components/hooks/useMyPreferences.js +1 -10
  52. package/components/hooks/useMySearch.js +2 -2
  53. package/components/hooks/useMySitemap.js +1 -4
  54. package/components/hooks/useMyTheme.js +2 -9
  55. package/components/routes/action/view/ActionSearch.js +1 -1
  56. package/components/routes/advanced/QueryBuilder.js +1 -1
  57. package/components/routes/analytics/AnalyticDetails.js +2 -2
  58. package/components/routes/analytics/AnalyticSearch.js +1 -1
  59. package/components/routes/help/ApiDocumentation.js +1 -1
  60. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  61. package/components/routes/help/BundleDocumentation.js +12 -0
  62. package/components/routes/help/HitDocumentation.js +3 -1
  63. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  64. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  65. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  66. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  67. package/components/routes/hits/search/HitContextMenu.js +2 -3
  68. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  69. package/components/routes/hits/search/InformationPane.js +28 -6
  70. package/components/routes/hits/search/LayoutSettings.d.ts +3 -0
  71. package/components/routes/hits/search/LayoutSettings.js +18 -0
  72. package/components/routes/hits/search/QuerySettings.js +1 -2
  73. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  74. package/components/routes/hits/search/SearchPane.js +37 -13
  75. package/components/routes/hits/search/ViewLink.js +1 -1
  76. package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
  77. package/components/routes/hits/view/HitViewer.js +4 -3
  78. package/components/routes/home/AnalyticCard.d.ts +2 -3
  79. package/components/routes/home/AnalyticCard.js +2 -2
  80. package/components/routes/home/ViewCard.js +1 -1
  81. package/components/routes/home/ViewRefresh.d.ts +23 -0
  82. package/components/routes/home/ViewRefresh.js +67 -0
  83. package/components/routes/home/index.js +9 -46
  84. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  85. package/components/routes/overviews/OverviewViewer.js +2 -2
  86. package/components/routes/settings/LocalSection.js +2 -1
  87. package/locales/en/translation.json +6 -42
  88. package/locales/fr/translation.json +4 -35
  89. package/models/WithMetadata.d.ts +1 -2
  90. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  91. package/models/entities/generated/Howler.d.ts +4 -0
  92. package/models/entities/generated/Rule.d.ts +10 -2
  93. package/models/entities/generated/Threat.d.ts +2 -2
  94. package/package.json +3 -18
  95. package/plugins/clue/components/ClueTypography.js +2 -2
  96. package/plugins/clue/utils.d.ts +1 -2
  97. package/utils/constants.d.ts +4 -3
  98. package/utils/constants.js +1 -0
  99. package/api/search/case.d.ts +0 -4
  100. package/api/search/case.js +0 -8
  101. package/api/v2/case/index.d.ts +0 -6
  102. package/api/v2/case/index.js +0 -18
  103. package/api/v2/index.d.ts +0 -4
  104. package/api/v2/index.js +0 -6
  105. package/api/v2/search/facet.d.ts +0 -3
  106. package/api/v2/search/facet.js +0 -12
  107. package/api/v2/search/index.d.ts +0 -5
  108. package/api/v2/search/index.js +0 -24
  109. package/components/elements/ObjectDetails.d.ts +0 -6
  110. package/components/elements/case/CaseCard.d.ts +0 -8
  111. package/components/elements/case/CaseCard.js +0 -39
  112. package/components/elements/case/CasePreview.d.ts +0 -6
  113. package/components/elements/case/CasePreview.js +0 -17
  114. package/components/elements/case/StatusIcon.d.ts +0 -5
  115. package/components/elements/case/StatusIcon.js +0 -13
  116. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -8
  117. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  118. package/components/elements/hit/related/RelatedRecords.js +0 -63
  119. package/components/elements/observable/ObservableCard.d.ts +0 -5
  120. package/components/elements/observable/ObservableCard.js +0 -7
  121. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  122. package/components/elements/observable/ObservablePreview.js +0 -12
  123. package/components/hooks/useRelatedRecords.d.ts +0 -13
  124. package/components/hooks/useRelatedRecords.js +0 -32
  125. package/components/routes/cases/CaseViewer.d.ts +0 -2
  126. package/components/routes/cases/CaseViewer.js +0 -24
  127. package/components/routes/cases/Cases.d.ts +0 -2
  128. package/components/routes/cases/Cases.js +0 -101
  129. package/components/routes/cases/constants.d.ts +0 -5
  130. package/components/routes/cases/constants.js +0 -5
  131. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  132. package/components/routes/cases/detail/AlertPanel.js +0 -32
  133. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  134. package/components/routes/cases/detail/CaseDashboard.js +0 -49
  135. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  136. package/components/routes/cases/detail/CaseDetails.js +0 -61
  137. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  138. package/components/routes/cases/detail/CaseOverview.js +0 -43
  139. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -6
  140. package/components/routes/cases/detail/CaseSidebar.js +0 -36
  141. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  142. package/components/routes/cases/detail/CaseTask.js +0 -57
  143. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  144. package/components/routes/cases/detail/ItemPage.js +0 -93
  145. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  146. package/components/routes/cases/detail/RelatedCasePanel.js +0 -31
  147. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  148. package/components/routes/cases/detail/TaskPanel.js +0 -52
  149. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -12
  150. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -19
  151. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  152. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -27
  153. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -13
  154. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -134
  155. package/components/routes/cases/detail/sidebar/types.d.ts +0 -3
  156. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  157. package/components/routes/cases/detail/sidebar/utils.js +0 -25
  158. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  159. package/components/routes/cases/hooks/useCase.js +0 -38
  160. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  161. package/components/routes/cases/modals/ResolveModal.js +0 -59
  162. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  163. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  164. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  165. package/components/routes/observables/ObservableViewer.js +0 -27
  166. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  167. package/models/entities/generated/Case.d.ts +0 -28
  168. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  169. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  170. package/models/entities/generated/EmailParent.d.ts +0 -19
  171. package/models/entities/generated/Enrichments.d.ts +0 -7
  172. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  173. package/models/entities/generated/HttpResponse.d.ts +0 -11
  174. package/models/entities/generated/Item.d.ts +0 -9
  175. package/models/entities/generated/Observable.d.ts +0 -84
  176. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  177. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  178. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  179. package/models/entities/generated/ObservableFile.d.ts +0 -36
  180. package/models/entities/generated/ObservableHowler.d.ts +0 -44
  181. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  182. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  183. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  184. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  185. package/models/entities/generated/ObservableSource.d.ts +0 -23
  186. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  187. package/models/entities/generated/ObservableTls.d.ts +0 -12
  188. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  189. package/models/entities/generated/Task.d.ts +0 -10
  190. package/utils/typeUtils.d.ts +0 -7
  191. package/utils/typeUtils.js +0 -18
  192. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  193. /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 { identity, isEmpty, isEqual, isUndefined, omitBy, uniq } from 'lodash-es';
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
- /** Scalar URL params that map 1:1 to a state key */
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
- if (pathname.startsWith('/bundles') && bundleId)
43
+ }
44
+ if (pathname.startsWith('/bundles') && bundleId) {
48
45
  return bundleId;
46
+ }
49
47
  return null;
50
48
  };
51
49
  /**
52
- * Returns stable add / remove / setAt / setAll / clear handlers for a list field in
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 useUrlSync = (values, _setValues, params, setParams, pathname, search, routeId) => {
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 === urlValue)
96
- return;
97
- if (params.has(urlKey) && stateValue === DEFAULT_VALUES[stateKey]) {
98
- changes[urlKey] = null; // remove
99
- }
100
- else if (stateValue !== DEFAULT_VALUES[stateKey]) {
101
- changes[urlKey] = stateValue; // write
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
- // selected
119
- if (pathname.startsWith('/bundles') && (!params.has('selected') || values.selected === params.get('selected'))) {
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 when 0
126
- if (parseOffset(params.get('offset')) !== values.offset) {
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
- // Drop scalar entries that already match the URL
130
- return omitBy(changes, (val, key) => !ARRAY_URL_KEYS.has(key) && val == params.get(key));
131
- }, [values, params, pathname]);
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
- // Array params: fall back to their declared default when absent from URL
142
- ARRAY_PARAMS.forEach(({ urlKey, stateKey, default: def }) => {
143
- const raw = params.getAll(urlKey);
144
- const resolved = (isEmpty(raw) && def ? def : uniq(raw));
145
- if (!isEqual(resolved, values[stateKey])) {
146
- changes[stateKey] = resolved;
147
- }
148
- });
149
- // selected
150
- const selectedValue = getSelectedValue(params, pathname, routeId);
151
- if (selectedValue !== values.selected)
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
- // offset
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, routeId]);
159
- // State → URL
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 (Array.isArray(value)) {
168
- newParams.delete(key);
169
- value.forEach(val => newParams.append(key, val));
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
- if (key === 'span' && typeof value === 'string' && !value.endsWith('custom')) {
226
- pendingChanges.current.startDate = null;
227
- pendingChanges.current.endDate = null;
228
- }
229
- WRITE_THROTTLER.debounce(() => {
230
- _setValues(c => ({ ...c, ...pendingChanges.current }));
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: filters.add,
248
- removeFilter: filters.remove,
249
- setFilter: filters.setAt,
250
- resetFilters: filters.reset,
251
- addIndex: indexes.add,
252
- removeIndex: indexes.remove,
253
- setIndex: indexes.setAt,
254
- setIndexes: indexes.setAll,
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) => {