@cccsaurora/howler-ui 2.18.0-dev.676 → 2.18.0-dev.682

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 (230) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +6 -0
  8. package/api/v2/case/index.js +18 -0
  9. package/api/v2/index.d.ts +4 -0
  10. package/api/v2/index.js +6 -0
  11. package/api/v2/search/facet.d.ts +3 -0
  12. package/api/v2/search/facet.js +12 -0
  13. package/api/v2/search/index.d.ts +5 -0
  14. package/api/v2/search/index.js +24 -0
  15. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  16. package/components/app/App.js +34 -7
  17. package/components/app/hooks/useMatchers.js +2 -2
  18. package/components/app/hooks/useMatchers.test.js +22 -22
  19. package/components/app/hooks/useTitle.js +3 -3
  20. package/components/app/providers/FavouritesProvider.js +2 -2
  21. package/components/app/providers/ParameterProvider.d.ts +9 -2
  22. package/components/app/providers/ParameterProvider.js +165 -240
  23. package/components/app/providers/ParameterProvider.test.js +307 -14
  24. package/components/app/providers/RecordProvider.d.ts +23 -0
  25. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  26. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  27. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  28. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  29. package/components/elements/ContextMenu.d.ts +56 -0
  30. package/components/elements/ContextMenu.js +109 -0
  31. package/components/elements/ContextMenu.test.js +215 -0
  32. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  33. package/components/elements/ObjectDetails.d.ts +6 -0
  34. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  35. package/components/elements/PluginTypography.d.ts +2 -1
  36. package/components/elements/PluginTypography.js +3 -2
  37. package/components/elements/UserList.d.ts +5 -2
  38. package/components/elements/UserList.js +14 -5
  39. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  40. package/components/elements/case/CaseCard.d.ts +8 -0
  41. package/components/elements/case/CaseCard.js +39 -0
  42. package/components/elements/case/CasePreview.d.ts +6 -0
  43. package/components/elements/case/CasePreview.js +17 -0
  44. package/components/elements/case/StatusIcon.d.ts +5 -0
  45. package/components/elements/case/StatusIcon.js +13 -0
  46. package/components/elements/display/ChipPopper.d.ts +1 -1
  47. package/components/elements/display/HowlerCard.js +1 -1
  48. package/components/elements/display/Modal.js +1 -0
  49. package/components/elements/hit/HitActions.js +4 -4
  50. package/components/elements/hit/HitBanner.js +28 -48
  51. package/components/elements/hit/HitCard.js +5 -5
  52. package/components/elements/hit/HitLabels.js +2 -2
  53. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  54. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  55. package/components/elements/hit/HitSummary.d.ts +2 -1
  56. package/components/elements/hit/HitSummary.js +6 -5
  57. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  58. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  59. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  60. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  61. package/components/elements/hit/related/RelatedRecords.js +63 -0
  62. package/components/elements/observable/ObservableCard.d.ts +6 -0
  63. package/components/elements/observable/ObservableCard.js +23 -0
  64. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  65. package/components/elements/observable/ObservablePreview.js +12 -0
  66. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  67. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  68. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  69. package/components/elements/record/RecordContextMenu.js +235 -0
  70. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  71. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +39 -39
  72. package/components/elements/record/RecordRelated.d.ts +7 -0
  73. package/components/elements/record/RecordRelated.js +34 -0
  74. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  75. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  76. package/components/elements/view/ViewTitle.js +1 -1
  77. package/components/hooks/useHitActions.d.ts +1 -1
  78. package/components/hooks/useHitActions.js +4 -4
  79. package/components/hooks/useMyPreferences.js +10 -1
  80. package/components/hooks/useMySearch.js +2 -2
  81. package/components/hooks/useMySitemap.js +4 -1
  82. package/components/hooks/useMyTheme.js +9 -2
  83. package/components/hooks/useParamState.test.js +3 -4
  84. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  85. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  86. package/components/hooks/useRelatedRecords.d.ts +13 -0
  87. package/components/hooks/useRelatedRecords.js +32 -0
  88. package/components/routes/action/edit/ActionEditor.js +2 -2
  89. package/components/routes/action/view/ActionSearch.js +1 -1
  90. package/components/routes/advanced/QueryBuilder.js +1 -1
  91. package/components/routes/advanced/QueryEditor.js +3 -3
  92. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  93. package/components/routes/analytics/AnalyticDetails.js +2 -2
  94. package/components/routes/analytics/AnalyticSearch.js +1 -1
  95. package/components/routes/cases/CaseViewer.d.ts +2 -0
  96. package/components/routes/cases/CaseViewer.js +22 -0
  97. package/components/routes/cases/Cases.d.ts +2 -0
  98. package/components/routes/cases/Cases.js +101 -0
  99. package/components/routes/cases/constants.d.ts +5 -0
  100. package/components/routes/cases/constants.js +5 -0
  101. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  102. package/components/routes/cases/detail/AlertPanel.js +33 -0
  103. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  104. package/components/routes/cases/detail/CaseAssets.js +101 -0
  105. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  106. package/components/routes/cases/detail/CaseAssets.test.js +163 -0
  107. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  108. package/components/routes/cases/detail/CaseDashboard.js +51 -0
  109. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  110. package/components/routes/cases/detail/CaseDetails.js +61 -0
  111. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  112. package/components/routes/cases/detail/CaseOverview.js +43 -0
  113. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  114. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  115. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  116. package/components/routes/cases/detail/CaseTask.js +57 -0
  117. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  118. package/components/routes/cases/detail/ItemPage.js +99 -0
  119. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  120. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  121. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  122. package/components/routes/cases/detail/TaskPanel.js +52 -0
  123. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  124. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  125. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  126. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  127. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  128. package/components/routes/cases/detail/assets/Asset.js +12 -0
  129. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  130. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  131. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
  132. package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
  133. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  134. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  135. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  136. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  137. package/components/routes/cases/hooks/useCase.js +38 -0
  138. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  139. package/components/routes/cases/modals/ResolveModal.js +59 -0
  140. package/components/routes/dossiers/DossierEditor.js +2 -2
  141. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  142. package/components/routes/help/ApiDocumentation.js +1 -1
  143. package/components/routes/help/HitBannerDocumentation.js +1 -0
  144. package/components/routes/help/HitDocumentation.js +1 -3
  145. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  146. package/components/routes/hits/search/InformationPane.js +47 -60
  147. package/components/routes/hits/search/LayoutSettings.js +3 -3
  148. package/components/routes/hits/search/QuerySettings.js +2 -1
  149. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  150. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  151. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  152. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  153. package/components/routes/hits/search/SearchPane.js +26 -49
  154. package/components/routes/hits/search/ViewLink.js +3 -3
  155. package/components/routes/hits/search/ViewLink.test.js +8 -8
  156. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  157. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  158. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  159. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  160. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  161. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  162. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  163. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  164. package/components/routes/hits/view/HitViewer.js +12 -13
  165. package/components/routes/home/ViewCard.js +4 -4
  166. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  167. package/components/routes/observables/ObservableViewer.js +27 -0
  168. package/components/routes/overviews/OverviewViewer.js +2 -2
  169. package/components/routes/views/ViewComposer.js +4 -4
  170. package/locales/en/translation.json +65 -3
  171. package/locales/fr/translation.json +63 -3
  172. package/models/WithMetadata.d.ts +2 -1
  173. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  174. package/models/entities/generated/Case.d.ts +28 -0
  175. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  176. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  177. package/models/entities/generated/EmailParent.d.ts +19 -0
  178. package/models/entities/generated/Enrichments.d.ts +7 -0
  179. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  180. package/models/entities/generated/Hit.d.ts +1 -0
  181. package/models/entities/generated/Howler.d.ts +0 -4
  182. package/models/entities/generated/HttpResponse.d.ts +11 -0
  183. package/models/entities/generated/Item.d.ts +9 -0
  184. package/models/entities/generated/Observable.d.ts +85 -0
  185. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  186. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  187. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  188. package/models/entities/generated/ObservableFile.d.ts +36 -0
  189. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  190. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  191. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  192. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  193. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  194. package/models/entities/generated/ObservableSource.d.ts +23 -0
  195. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  196. package/models/entities/generated/ObservableTls.d.ts +12 -0
  197. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  198. package/models/entities/generated/Rule.d.ts +2 -10
  199. package/models/entities/generated/Task.d.ts +10 -0
  200. package/models/entities/generated/Threat.d.ts +2 -2
  201. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  202. package/package.json +121 -104
  203. package/plugins/clue/components/ClueTypography.js +2 -2
  204. package/plugins/clue/utils.d.ts +2 -1
  205. package/tests/utils.d.ts +2 -0
  206. package/tests/utils.js +8 -0
  207. package/utils/constants.d.ts +3 -3
  208. package/utils/hitFunctions.d.ts +2 -1
  209. package/utils/hitFunctions.js +4 -4
  210. package/utils/typeUtils.d.ts +7 -0
  211. package/utils/typeUtils.js +27 -0
  212. package/components/app/providers/HitProvider.d.ts +0 -22
  213. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  214. package/components/elements/display/icons/BundleButton.js +0 -32
  215. package/components/elements/hit/HitRelated.d.ts +0 -6
  216. package/components/elements/hit/HitRelated.js +0 -7
  217. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  218. package/components/routes/help/BundleDocumentation.js +0 -12
  219. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  220. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  221. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  222. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  223. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  224. package/components/routes/hits/search/BundleScroller.js +0 -6
  225. package/components/routes/hits/search/HitContextMenu.js +0 -227
  226. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  227. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  228. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  229. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  230. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -272,17 +272,17 @@ describe('ParameterContext', () => {
272
272
  });
273
273
  });
274
274
  });
275
- describe('clearFilters', () => {
275
+ describe('resetFilters', () => {
276
276
  it('should clear all filters', async () => {
277
277
  mockSearchParams = new URLSearchParams();
278
278
  mockSearchParams.append('filter', 'filter1');
279
279
  mockSearchParams.append('filter', 'filter2');
280
280
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
281
281
  filters: ctx.filters,
282
- clearFilters: ctx.clearFilters
282
+ resetFilters: ctx.resetFilters
283
283
  })), { wrapper: Wrapper });
284
284
  await act(async () => {
285
- hook.result.current.clearFilters();
285
+ hook.result.current.resetFilters();
286
286
  });
287
287
  await waitFor(() => {
288
288
  expect(hook.result.current.filters).toEqual([]);
@@ -291,10 +291,10 @@ describe('ParameterContext', () => {
291
291
  it('should be no-op when already empty', async () => {
292
292
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
293
293
  filters: ctx.filters,
294
- clearFilters: ctx.clearFilters
294
+ resetFilters: ctx.resetFilters
295
295
  })), { wrapper: Wrapper });
296
296
  await act(async () => {
297
- hook.result.current.clearFilters();
297
+ hook.result.current.resetFilters();
298
298
  });
299
299
  await waitFor(() => {
300
300
  expect(hook.result.current.filters).toEqual([]);
@@ -441,10 +441,10 @@ describe('ParameterContext', () => {
441
441
  mockSearchParams.append('filter', 'filter1');
442
442
  mockSearchParams.append('filter', 'filter2');
443
443
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
444
- clearFilters: ctx.clearFilters
444
+ resetFilters: ctx.resetFilters
445
445
  })), { wrapper: Wrapper });
446
446
  await act(async () => {
447
- hook.result.current.clearFilters();
447
+ hook.result.current.resetFilters();
448
448
  });
449
449
  await waitFor(() => {
450
450
  expect(mockSetParams).toHaveBeenCalled();
@@ -706,6 +706,299 @@ describe('ParameterContext', () => {
706
706
  });
707
707
  });
708
708
  });
709
+ describe('indexes (multi-index support)', () => {
710
+ it('should initialize with default ["hit"] when no index params are present', async () => {
711
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
712
+ expect(hook.result.current).toEqual(['hit']);
713
+ });
714
+ it('should initialize with single index from URL', async () => {
715
+ mockSearchParams = new URLSearchParams({ index: 'observable' });
716
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
717
+ expect(hook.result.current).toEqual(['observable']);
718
+ });
719
+ it('should initialize with multiple indexes from URL', async () => {
720
+ mockSearchParams = new URLSearchParams();
721
+ mockSearchParams.append('index', 'hit');
722
+ mockSearchParams.append('index', 'observable');
723
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
724
+ expect(hook.result.current).toEqual(['hit', 'observable']);
725
+ });
726
+ it('should deduplicate repeated index values from URL', async () => {
727
+ mockSearchParams = new URLSearchParams();
728
+ mockSearchParams.append('index', 'hit');
729
+ mockSearchParams.append('index', 'hit');
730
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
731
+ expect(hook.result.current).toEqual(['hit']);
732
+ });
733
+ describe('addIndex', () => {
734
+ it('should add an index to the default array', async () => {
735
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
736
+ indexes: ctx.indexes,
737
+ addIndex: ctx.addIndex
738
+ })), { wrapper: Wrapper });
739
+ await act(async () => {
740
+ hook.result.current.addIndex('observable');
741
+ });
742
+ await waitFor(() => {
743
+ expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
744
+ });
745
+ });
746
+ it('should not add a duplicate index', async () => {
747
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
748
+ indexes: ctx.indexes,
749
+ addIndex: ctx.addIndex
750
+ })), { wrapper: Wrapper });
751
+ await act(async () => {
752
+ hook.result.current.addIndex('hit');
753
+ });
754
+ await waitFor(() => {
755
+ expect(hook.result.current.indexes).toEqual(['hit']);
756
+ });
757
+ });
758
+ });
759
+ describe('removeIndex', () => {
760
+ it('should remove an index from the list', async () => {
761
+ mockSearchParams = new URLSearchParams();
762
+ mockSearchParams.append('index', 'hit');
763
+ mockSearchParams.append('index', 'observable');
764
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
765
+ indexes: ctx.indexes,
766
+ removeIndex: ctx.removeIndex
767
+ })), { wrapper: Wrapper });
768
+ await act(async () => {
769
+ hook.result.current.removeIndex('hit');
770
+ });
771
+ await waitFor(() => {
772
+ expect(hook.result.current.indexes).toEqual(['observable']);
773
+ });
774
+ });
775
+ it('should do nothing when removing a nonexistent index', async () => {
776
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
777
+ indexes: ctx.indexes,
778
+ removeIndex: ctx.removeIndex
779
+ })), { wrapper: Wrapper });
780
+ await act(async () => {
781
+ hook.result.current.removeIndex('observable');
782
+ });
783
+ await waitFor(() => {
784
+ expect(hook.result.current.indexes).toEqual(['hit']);
785
+ });
786
+ });
787
+ it('should handle removing from empty array', async () => {
788
+ mockSearchParams = new URLSearchParams();
789
+ mockSearchParams.append('index', 'hit');
790
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
791
+ indexes: ctx.indexes,
792
+ removeIndex: ctx.removeIndex
793
+ })), { wrapper: Wrapper });
794
+ await act(async () => {
795
+ hook.result.current.removeIndex('hit');
796
+ });
797
+ await waitFor(() => {
798
+ expect(hook.result.current.indexes).toEqual([]);
799
+ });
800
+ });
801
+ });
802
+ describe('setIndex', () => {
803
+ it('should update the index at the specified position', async () => {
804
+ mockSearchParams = new URLSearchParams();
805
+ mockSearchParams.append('index', 'hit');
806
+ mockSearchParams.append('index', 'observable');
807
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
808
+ indexes: ctx.indexes,
809
+ setIndex: ctx.setIndex
810
+ })), { wrapper: Wrapper });
811
+ await act(async () => {
812
+ hook.result.current.setIndex(0, 'observable');
813
+ });
814
+ await waitFor(() => {
815
+ expect(hook.result.current.indexes).toEqual(['observable', 'observable']);
816
+ });
817
+ });
818
+ it('should do nothing when index is out of bounds', async () => {
819
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
820
+ indexes: ctx.indexes,
821
+ setIndex: ctx.setIndex
822
+ })), { wrapper: Wrapper });
823
+ await act(async () => {
824
+ hook.result.current.setIndex(5, 'observable');
825
+ });
826
+ await waitFor(() => {
827
+ expect(hook.result.current.indexes).toEqual(['hit']);
828
+ });
829
+ });
830
+ it('should do nothing when position is negative', async () => {
831
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
832
+ indexes: ctx.indexes,
833
+ setIndex: ctx.setIndex
834
+ })), { wrapper: Wrapper });
835
+ await act(async () => {
836
+ hook.result.current.setIndex(-1, 'observable');
837
+ });
838
+ await waitFor(() => {
839
+ expect(hook.result.current.indexes).toEqual(['hit']);
840
+ });
841
+ });
842
+ });
843
+ describe('setIndexes', () => {
844
+ it('should replace all indexes with the provided list', async () => {
845
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
846
+ indexes: ctx.indexes,
847
+ setIndexes: ctx.setIndexes
848
+ })), { wrapper: Wrapper });
849
+ await act(async () => {
850
+ hook.result.current.setIndexes(['observable']);
851
+ });
852
+ await waitFor(() => {
853
+ expect(hook.result.current.indexes).toEqual(['observable']);
854
+ });
855
+ });
856
+ it('should deduplicate values when setting all indexes', async () => {
857
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
858
+ indexes: ctx.indexes,
859
+ setIndexes: ctx.setIndexes
860
+ })), { wrapper: Wrapper });
861
+ await act(async () => {
862
+ hook.result.current.setIndexes(['hit', 'hit', 'observable']);
863
+ });
864
+ await waitFor(() => {
865
+ expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
866
+ });
867
+ });
868
+ it('should set to empty array', async () => {
869
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
870
+ indexes: ctx.indexes,
871
+ setIndexes: ctx.setIndexes
872
+ })), { wrapper: Wrapper });
873
+ await act(async () => {
874
+ hook.result.current.setIndexes([]);
875
+ });
876
+ await waitFor(() => {
877
+ expect(hook.result.current.indexes).toEqual([]);
878
+ });
879
+ });
880
+ });
881
+ describe('resetIndexes', () => {
882
+ it('should reset indexes to default ["hit"]', async () => {
883
+ mockSearchParams = new URLSearchParams();
884
+ mockSearchParams.append('index', 'hit');
885
+ mockSearchParams.append('index', 'observable');
886
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
887
+ indexes: ctx.indexes,
888
+ resetIndexes: ctx.resetIndexes
889
+ })), { wrapper: Wrapper });
890
+ await act(async () => {
891
+ hook.result.current.resetIndexes();
892
+ });
893
+ await waitFor(() => {
894
+ expect(hook.result.current.indexes).toEqual(['hit']);
895
+ });
896
+ });
897
+ it('should reset to default even when called on empty array', async () => {
898
+ mockSearchParams = new URLSearchParams();
899
+ mockSearchParams.append('index', 'hit');
900
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
901
+ indexes: ctx.indexes,
902
+ removeIndex: ctx.removeIndex,
903
+ resetIndexes: ctx.resetIndexes
904
+ })), { wrapper: Wrapper });
905
+ // First empty it
906
+ await act(async () => {
907
+ hook.result.current.removeIndex('hit');
908
+ });
909
+ await waitFor(() => {
910
+ expect(hook.result.current.indexes).toEqual([]);
911
+ });
912
+ // Resetting always returns to default ['hit']
913
+ await act(async () => {
914
+ hook.result.current.resetIndexes();
915
+ });
916
+ await waitFor(() => {
917
+ expect(hook.result.current.indexes).toEqual(['hit']);
918
+ });
919
+ });
920
+ });
921
+ describe('URL synchronization', () => {
922
+ it('should not write the default ["hit"] index to the URL', async () => {
923
+ renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
924
+ // Allow any effects to flush
925
+ await waitFor(() => {
926
+ // If setParams was called, the URL must not contain ?index=hit
927
+ if (mockSetParams.mock.calls.length > 0) {
928
+ const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
929
+ const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
930
+ expect(urlParams.getAll('index')).toEqual([]);
931
+ }
932
+ else {
933
+ // setParams not called at all is also fine
934
+ expect(true).toBe(true);
935
+ }
936
+ });
937
+ });
938
+ it('should write a non-default index to the URL', async () => {
939
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
940
+ addIndex: ctx.addIndex,
941
+ resetIndexes: ctx.resetIndexes
942
+ })), { wrapper: Wrapper });
943
+ await act(async () => {
944
+ hook.result.current.resetIndexes();
945
+ hook.result.current.addIndex('observable');
946
+ });
947
+ await waitFor(() => {
948
+ expect(mockSetParams).toHaveBeenCalled();
949
+ const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
950
+ const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
951
+ expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
952
+ });
953
+ });
954
+ it('should sync multiple indexes to URL as multiple index params', async () => {
955
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
956
+ addIndex: ctx.addIndex
957
+ })), { wrapper: Wrapper });
958
+ await act(async () => {
959
+ hook.result.current.addIndex('observable');
960
+ });
961
+ await waitFor(() => {
962
+ expect(mockSetParams).toHaveBeenCalled();
963
+ const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
964
+ const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
965
+ expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
966
+ });
967
+ });
968
+ it('should remove all index params from URL when state resets to default', async () => {
969
+ mockSearchParams = new URLSearchParams();
970
+ mockSearchParams.append('index', 'hit');
971
+ mockSearchParams.append('index', 'observable');
972
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
973
+ resetIndexes: ctx.resetIndexes
974
+ })), { wrapper: Wrapper });
975
+ await act(async () => {
976
+ hook.result.current.resetIndexes();
977
+ });
978
+ await waitFor(() => {
979
+ expect(mockSetParams).toHaveBeenCalled();
980
+ const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
981
+ const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
982
+ expect(urlParams.getAll('index')).toEqual([]);
983
+ });
984
+ });
985
+ it('should remove index param from URL when state returns to default', async () => {
986
+ mockSearchParams = new URLSearchParams({ index: 'observable' });
987
+ const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
988
+ setIndexes: ctx.setIndexes
989
+ })), { wrapper: Wrapper });
990
+ await act(async () => {
991
+ hook.result.current.setIndexes(['hit']);
992
+ });
993
+ await waitFor(() => {
994
+ expect(mockSetParams).toHaveBeenCalled();
995
+ const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
996
+ const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
997
+ expect(urlParams.getAll('index')).toEqual([]);
998
+ });
999
+ });
1000
+ });
1001
+ });
709
1002
  describe('views (multi-view support)', () => {
710
1003
  it('should initialize with empty array when no view params present', async () => {
711
1004
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
@@ -839,17 +1132,17 @@ describe('ParameterContext', () => {
839
1132
  });
840
1133
  });
841
1134
  });
842
- describe('clearViews', () => {
1135
+ describe('resetViews', () => {
843
1136
  it('should clear all views', async () => {
844
1137
  mockSearchParams = new URLSearchParams();
845
1138
  mockSearchParams.append('view', 'view_1');
846
1139
  mockSearchParams.append('view', 'view_2');
847
1140
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
848
1141
  views: ctx.views,
849
- clearViews: ctx.clearViews
1142
+ resetViews: ctx.resetViews
850
1143
  })), { wrapper: Wrapper });
851
1144
  await act(async () => {
852
- hook.result.current.clearViews();
1145
+ hook.result.current.resetViews();
853
1146
  });
854
1147
  await waitFor(() => {
855
1148
  expect(hook.result.current.views).toEqual([]);
@@ -858,10 +1151,10 @@ describe('ParameterContext', () => {
858
1151
  it('should be no-op when already empty', async () => {
859
1152
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
860
1153
  views: ctx.views,
861
- clearViews: ctx.clearViews
1154
+ resetViews: ctx.resetViews
862
1155
  })), { wrapper: Wrapper });
863
1156
  await act(async () => {
864
- hook.result.current.clearViews();
1157
+ hook.result.current.resetViews();
865
1158
  });
866
1159
  await waitFor(() => {
867
1160
  expect(hook.result.current.views).toEqual([]);
@@ -1008,10 +1301,10 @@ describe('ParameterContext', () => {
1008
1301
  mockSearchParams.append('view', 'view_1');
1009
1302
  mockSearchParams.append('view', 'view_2');
1010
1303
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1011
- clearViews: ctx.clearViews
1304
+ resetViews: ctx.resetViews
1012
1305
  })), { wrapper: Wrapper });
1013
1306
  await act(async () => {
1014
- hook.result.current.clearViews();
1307
+ hook.result.current.resetViews();
1015
1308
  });
1016
1309
  await waitFor(() => {
1017
1310
  expect(mockSetParams).toHaveBeenCalled();
@@ -0,0 +1,23 @@
1
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
4
+ import type { FC, PropsWithChildren } from 'react';
5
+ export interface RecordContextType {
6
+ records: {
7
+ [index: string]: Hit | Observable;
8
+ };
9
+ selectedRecords: (Hit | Observable)[];
10
+ addRecordToSelection: (id: string) => void;
11
+ removeRecordFromSelection: (id: string) => void;
12
+ clearSelectedRecords: (except?: string) => void;
13
+ loadRecords: (hits: (Hit | Observable)[]) => void;
14
+ updateRecord: (newHit: Hit | Observable) => void;
15
+ getRecord: (id: string, force?: boolean) => Promise<WithMetadata<Hit | Observable>>;
16
+ }
17
+ export declare const RecordContext: import("use-context-selector").Context<RecordContextType>;
18
+ /**
19
+ * Central repository for storing individual hit data across the application. Allows efficient retrieval of hits across componenents.
20
+ */
21
+ declare const RecordProvider: FC<PropsWithChildren>;
22
+ export declare const useHitContextSelector: <Selected>(selector: (value: RecordContextType) => Selected) => Selected;
23
+ export default RecordProvider;
@@ -5,25 +5,25 @@ import { uniq } from 'lodash-es';
5
5
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { createContext, useContextSelector } from 'use-context-selector';
7
7
  import { SocketContext } from './SocketProvider';
8
- export const HitContext = createContext(null);
8
+ export const RecordContext = createContext(null);
9
9
  /**
10
10
  * Central repository for storing individual hit data across the application. Allows efficient retrieval of hits across componenents.
11
11
  */
12
- const HitProvider = ({ children }) => {
12
+ const RecordProvider = ({ children }) => {
13
13
  const { dispatchApi } = useMyApi();
14
14
  const { addListener, removeListener } = useContext(SocketContext);
15
15
  /**
16
16
  * The most immediate of two levels of caching, this ref stores the raw promises for each hit.
17
17
  * Rapidly updates, good for uses in-context where parallel updates my be occurring, i.e.
18
- * when two cards request the same hit that's missing from the store. Used in getHit for this reason.
18
+ * when two cards request the same hit that's missing from the store. Used in getRecord for this reason.
19
19
  */
20
- const hitRequests = useRef({});
20
+ const recordRequests = useRef({});
21
21
  /**
22
22
  * The "Authoritative" store of hits. Changes here will trigger rerenders, and essentially
23
23
  * caches the result of the above promises. Slower to update, so used outside of the hitcontext
24
24
  * where parallel requests aren't an issue.
25
25
  */
26
- const [hits, setHits] = useState({});
26
+ const [records, setRecords] = useState({});
27
27
  const [selectedHitIds, setSelectedHitIds] = useState([]);
28
28
  // The central location where we propagate any changes from hits made via websockets into the repository. We just save every update,
29
29
  // instead of caching it across many components inconsistently as before.
@@ -31,8 +31,8 @@ const HitProvider = ({ children }) => {
31
31
  if (data.hit) {
32
32
  // eslint-disable-next-line no-console
33
33
  console.debug('Received websocket update for hit', data.hit.howler.id);
34
- hitRequests.current[data.hit.howler.id] = Promise.resolve(data.hit);
35
- setHits(_hits => ({
34
+ recordRequests.current[data.hit.howler.id] = Promise.resolve(data.hit);
35
+ setRecords(_hits => ({
36
36
  ..._hits,
37
37
  [data.hit.howler.id]: {
38
38
  ..._hits[data.hit.howler.id],
@@ -46,68 +46,68 @@ const HitProvider = ({ children }) => {
46
46
  return () => removeListener('hits');
47
47
  }, [addListener, handler, removeListener]);
48
48
  /**
49
- * A method to retrieve a hit from the context. It first checks the hit state,
49
+ * A method to retrieve a record from the context. It first checks the hit state,
50
50
  * then checks for ongoing requests, then finally executes a new request if necessary.
51
51
  */
52
- const getHit = useCallback(async (id, force = false) => {
53
- if (!hitRequests.current[id] || force) {
54
- hitRequests.current[id] = dispatchApi(api.hit.get(id, ['template', 'dossiers', 'analytic', 'overview']));
55
- const newHit = await hitRequests.current[id];
56
- setHits(_hits => ({ ..._hits, [id]: newHit }));
52
+ const getRecord = useCallback(async (id, force = false) => {
53
+ if (!recordRequests.current[id] || force) {
54
+ recordRequests.current[id] = dispatchApi(api.hit.get(id, ['template', 'dossiers', 'analytic', 'overview']));
55
+ const newRecord = await recordRequests.current[id];
56
+ setRecords(_records => ({ ..._records, [id]: newRecord }));
57
57
  }
58
- return hitRequests.current[id];
58
+ return recordRequests.current[id];
59
59
  }, [dispatchApi]);
60
60
  /**
61
61
  * Update a hit in the context locally
62
62
  */
63
- const updateHit = useCallback((newHit) => {
64
- hitRequests.current[newHit.howler.id] = Promise.resolve(newHit);
65
- setHits(_hits => ({ ..._hits, [newHit.howler.id]: newHit }));
63
+ const updateRecord = useCallback((newHit) => {
64
+ recordRequests.current[newHit.howler.id] = Promise.resolve(newHit);
65
+ setRecords(_hits => ({ ..._hits, [newHit.howler.id]: newHit }));
66
66
  }, []);
67
67
  /**
68
68
  * Add a large number of hits to the cache. Used for results of searches.
69
69
  */
70
- const loadHits = useCallback((newHits) => {
70
+ const loadRecords = useCallback((newHits) => {
71
71
  const mappedHits = newHits.map(hit => [hit.howler.id, hit]);
72
72
  mappedHits.forEach(([id, hit]) => {
73
- hitRequests.current[id] = Promise.resolve(hit);
73
+ recordRequests.current[id] = Promise.resolve(hit);
74
74
  });
75
- setHits(_hits => ({ ..._hits, ...Object.fromEntries(mappedHits) }));
75
+ setRecords(_hits => ({ ..._hits, ...Object.fromEntries(mappedHits) }));
76
76
  }, []);
77
- const addHitToSelection = useCallback((id) => {
77
+ const addRecordToSelection = useCallback((id) => {
78
78
  setSelectedHitIds(_selected => uniq([..._selected, id]));
79
79
  }, []);
80
- const removeHitFromSelection = useCallback((id) => {
80
+ const removeRecordFromSelection = useCallback((id) => {
81
81
  setSelectedHitIds(_selected => _selected.filter(_id => _id !== id));
82
82
  }, []);
83
- const clearSelectedHits = useCallback((except) => {
83
+ const clearSelectedRecords = useCallback((except) => {
84
84
  setSelectedHitIds(!!except ? [except] : []);
85
85
  }, []);
86
- const selectedHits = useMemo(() => selectedHitIds.map(id => hits[id]).filter(hit => !!hit), [hits, selectedHitIds]);
86
+ const selectedRecords = useMemo(() => selectedHitIds.map(id => records[id]).filter(hit => !!hit), [records, selectedHitIds]);
87
87
  useEffect(() => {
88
88
  selectedHitIds.forEach(id => {
89
- if (!hitRequests.current[id]) {
90
- getHit(id);
89
+ if (!recordRequests.current[id]) {
90
+ getRecord(id);
91
91
  }
92
92
  });
93
- }, [getHit, selectedHitIds]);
93
+ }, [getRecord, selectedHitIds]);
94
94
  useEffect(() => {
95
- Object.entries(hits).forEach(([id, hit]) => {
96
- hitRequests.current[id] = Promise.resolve(hit);
95
+ Object.entries(records).forEach(([id, hit]) => {
96
+ recordRequests.current[id] = Promise.resolve(hit);
97
97
  });
98
- }, [hits]);
99
- return (_jsx(HitContext.Provider, { value: {
100
- hits,
101
- getHit,
102
- updateHit,
103
- selectedHits,
104
- addHitToSelection,
105
- removeHitFromSelection,
106
- clearSelectedHits,
107
- loadHits
98
+ }, [records]);
99
+ return (_jsx(RecordContext.Provider, { value: {
100
+ records,
101
+ getRecord,
102
+ updateRecord,
103
+ selectedRecords,
104
+ addRecordToSelection,
105
+ removeRecordFromSelection,
106
+ clearSelectedRecords,
107
+ loadRecords
108
108
  }, children: children }));
109
109
  };
110
110
  export const useHitContextSelector = (selector) => {
111
- return useContextSelector(HitContext, selector);
111
+ return useContextSelector(RecordContext, selector);
112
112
  };
113
- export default HitProvider;
113
+ export default RecordProvider;
@@ -1,17 +1,17 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
3
3
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
4
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
4
5
  import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
5
6
  import { type Dispatch, type FC, type PropsWithChildren, type SetStateAction } from 'react';
6
7
  export interface QueryEntry {
7
8
  [query: string]: string;
8
9
  }
9
- export interface HitSearchContextType {
10
+ export interface RecordSearchContextType {
10
11
  displayType: 'list' | 'grid';
11
12
  searching: boolean;
12
13
  error: string | null;
13
- response: HowlerSearchResponse<WithMetadata<Hit>> | null;
14
- bundleId: string | null;
14
+ response: HowlerSearchResponse<WithMetadata<Hit | Observable>> | null;
15
15
  fzfSearch: boolean;
16
16
  setDisplayType: (type: 'list' | 'grid') => void;
17
17
  setFzfSearch: Dispatch<SetStateAction<boolean>>;
@@ -20,6 +20,6 @@ export interface HitSearchContextType {
20
20
  queryHistory: QueryEntry;
21
21
  setQueryHistory: ReturnType<typeof useMyLocalStorageItem>[1];
22
22
  }
23
- export declare const HitSearchContext: import("use-context-selector").Context<HitSearchContextType>;
24
- declare const HitSearchProvider: FC<PropsWithChildren>;
25
- export default HitSearchProvider;
23
+ export declare const RecordSearchContext: import("use-context-selector").Context<RecordSearchContextType>;
24
+ declare const RecordSearchProvider: FC<PropsWithChildren>;
25
+ export default RecordSearchProvider;