@cccsaurora/howler-ui 2.18.0-dev.683 → 2.18.0-dev.686

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 (233) 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.d.ts +1 -0
  77. package/components/elements/view/ViewTitle.js +9 -2
  78. package/components/hooks/useHitActions.d.ts +1 -1
  79. package/components/hooks/useHitActions.js +4 -4
  80. package/components/hooks/useMyPreferences.js +10 -1
  81. package/components/hooks/useMySearch.js +2 -2
  82. package/components/hooks/useMySitemap.js +4 -1
  83. package/components/hooks/useMyTheme.js +9 -2
  84. package/components/hooks/useParamState.test.js +3 -4
  85. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  86. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  87. package/components/hooks/useRelatedRecords.d.ts +13 -0
  88. package/components/hooks/useRelatedRecords.js +32 -0
  89. package/components/routes/action/edit/ActionEditor.js +2 -2
  90. package/components/routes/action/view/ActionSearch.js +1 -1
  91. package/components/routes/advanced/QueryBuilder.js +1 -1
  92. package/components/routes/advanced/QueryEditor.js +3 -3
  93. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  94. package/components/routes/analytics/AnalyticDetails.js +2 -2
  95. package/components/routes/analytics/AnalyticSearch.js +1 -1
  96. package/components/routes/cases/CaseViewer.d.ts +2 -0
  97. package/components/routes/cases/CaseViewer.js +22 -0
  98. package/components/routes/cases/Cases.d.ts +2 -0
  99. package/components/routes/cases/Cases.js +101 -0
  100. package/components/routes/cases/constants.d.ts +5 -0
  101. package/components/routes/cases/constants.js +5 -0
  102. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  103. package/components/routes/cases/detail/AlertPanel.js +33 -0
  104. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  105. package/components/routes/cases/detail/CaseAssets.js +101 -0
  106. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  107. package/components/routes/cases/detail/CaseAssets.test.js +163 -0
  108. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  109. package/components/routes/cases/detail/CaseDashboard.js +51 -0
  110. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  111. package/components/routes/cases/detail/CaseDetails.js +61 -0
  112. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  113. package/components/routes/cases/detail/CaseOverview.js +43 -0
  114. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  115. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  116. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  117. package/components/routes/cases/detail/CaseTask.js +57 -0
  118. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  119. package/components/routes/cases/detail/ItemPage.js +99 -0
  120. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  121. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  122. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  123. package/components/routes/cases/detail/TaskPanel.js +52 -0
  124. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  125. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  126. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  127. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  128. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  129. package/components/routes/cases/detail/assets/Asset.js +12 -0
  130. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  131. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  132. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
  133. package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
  134. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  135. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  136. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  137. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  138. package/components/routes/cases/hooks/useCase.js +38 -0
  139. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  140. package/components/routes/cases/modals/ResolveModal.js +59 -0
  141. package/components/routes/dossiers/DossierEditor.js +2 -2
  142. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  143. package/components/routes/help/ApiDocumentation.js +1 -1
  144. package/components/routes/help/HitBannerDocumentation.js +1 -0
  145. package/components/routes/help/HitDocumentation.js +1 -3
  146. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  147. package/components/routes/hits/search/InformationPane.js +47 -60
  148. package/components/routes/hits/search/LayoutSettings.js +3 -3
  149. package/components/routes/hits/search/QuerySettings.js +2 -1
  150. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  151. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  152. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  153. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  154. package/components/routes/hits/search/SearchPane.js +26 -49
  155. package/components/routes/hits/search/ViewLink.js +3 -3
  156. package/components/routes/hits/search/ViewLink.test.js +8 -8
  157. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  158. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  159. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  160. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  161. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  162. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  163. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  164. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  165. package/components/routes/hits/view/HitViewer.js +12 -13
  166. package/components/routes/home/ViewCard.js +47 -41
  167. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  168. package/components/routes/observables/ObservableViewer.js +27 -0
  169. package/components/routes/overviews/OverviewViewer.js +2 -2
  170. package/components/routes/views/ViewComposer.js +46 -19
  171. package/locales/en/translation.json +65 -3
  172. package/locales/fr/translation.json +63 -3
  173. package/models/WithMetadata.d.ts +2 -1
  174. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  175. package/models/entities/generated/Case.d.ts +28 -0
  176. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  177. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  178. package/models/entities/generated/EmailParent.d.ts +19 -0
  179. package/models/entities/generated/Enrichments.d.ts +7 -0
  180. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  181. package/models/entities/generated/Hit.d.ts +1 -0
  182. package/models/entities/generated/Howler.d.ts +0 -4
  183. package/models/entities/generated/HttpResponse.d.ts +11 -0
  184. package/models/entities/generated/Item.d.ts +9 -0
  185. package/models/entities/generated/Observable.d.ts +85 -0
  186. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  187. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  188. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  189. package/models/entities/generated/ObservableFile.d.ts +36 -0
  190. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  191. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  192. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  193. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  194. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  195. package/models/entities/generated/ObservableSource.d.ts +23 -0
  196. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  197. package/models/entities/generated/ObservableTls.d.ts +12 -0
  198. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  199. package/models/entities/generated/Rule.d.ts +2 -10
  200. package/models/entities/generated/Task.d.ts +10 -0
  201. package/models/entities/generated/Threat.d.ts +2 -2
  202. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  203. package/models/entities/generated/View.d.ts +1 -0
  204. package/package.json +19 -2
  205. package/plugins/clue/components/ClueTypography.js +2 -2
  206. package/plugins/clue/utils.d.ts +2 -1
  207. package/tests/utils.d.ts +2 -0
  208. package/tests/utils.js +8 -0
  209. package/utils/constants.d.ts +3 -3
  210. package/utils/hitFunctions.d.ts +2 -1
  211. package/utils/hitFunctions.js +4 -4
  212. package/utils/typeUtils.d.ts +7 -0
  213. package/utils/typeUtils.js +27 -0
  214. package/utils/viewUtils.js +3 -0
  215. package/components/app/providers/HitProvider.d.ts +0 -22
  216. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  217. package/components/elements/display/icons/BundleButton.js +0 -32
  218. package/components/elements/hit/HitRelated.d.ts +0 -6
  219. package/components/elements/hit/HitRelated.js +0 -7
  220. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  221. package/components/routes/help/BundleDocumentation.js +0 -12
  222. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  223. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  224. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  225. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  226. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  227. package/components/routes/hits/search/BundleScroller.js +0 -6
  228. package/components/routes/hits/search/HitContextMenu.js +0 -227
  229. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  230. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  231. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  232. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  233. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -91,13 +91,13 @@ vi.mock('@mui/material', async () => {
91
91
  });
92
92
  // Import component after mocks
93
93
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
94
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
95
94
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
95
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
96
96
  import i18n from '@cccsaurora/howler-ui/i18n';
97
97
  import { I18nextProvider } from 'react-i18next';
98
98
  import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
99
99
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
100
- import HitContextMenu from './HitContextMenu';
100
+ import RecordContextMenu from './RecordContextMenu';
101
101
  const mockGetSelectedId = vi.fn(() => 'test-hit-1');
102
102
  const mockConfig = {
103
103
  lookups: {
@@ -105,16 +105,16 @@ const mockConfig = {
105
105
  }
106
106
  };
107
107
  const mockApiContext = { config: mockConfig };
108
- const mockHitContext = {
109
- hits: {
108
+ const mockRecordContext = {
109
+ records: {
110
110
  'test-hit-1': createMockHit()
111
111
  },
112
- selectedHits: []
112
+ selectedRecords: []
113
113
  };
114
114
  const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
115
115
  // Test wrapper
116
116
  const Wrapper = ({ children }) => {
117
- return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(HitContext.Provider, { value: mockHitContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
117
+ return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
118
118
  };
119
119
  describe('HitContextMenu', () => {
120
120
  let user;
@@ -122,11 +122,11 @@ describe('HitContextMenu', () => {
122
122
  beforeEach(() => {
123
123
  user = userEvent.setup();
124
124
  vi.clearAllMocks();
125
- mockHitContext.selectedHits.length = 0;
126
- mockHitContext.hits['test-hit-1'] = createMockHit();
125
+ mockRecordContext.selectedRecords.length = 0;
126
+ mockRecordContext.records['test-hit-1'] = createMockHit();
127
127
  mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
128
128
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
129
- rerender = render(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
129
+ rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
130
130
  });
131
131
  describe('Context Menu Initialization', () => {
132
132
  it('should open menu on right-click', async () => {
@@ -190,13 +190,13 @@ describe('HitContextMenu', () => {
190
190
  });
191
191
  it('should disable "Open Hit" when hit is null', async () => {
192
192
  act(() => {
193
- mockHitContext.hits['test-hit-1'] = null;
193
+ mockRecordContext.records['test-hit-1'] = null;
194
194
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
195
195
  fireEvent.contextMenu(contextMenuWrapper);
196
196
  });
197
197
  await waitFor(() => {
198
198
  const menuItems = screen.getAllByRole('menuitem');
199
- const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
199
+ const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
200
200
  expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
201
201
  });
202
202
  });
@@ -237,7 +237,7 @@ describe('HitContextMenu', () => {
237
237
  skip_rationale: false
238
238
  }
239
239
  }));
240
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
240
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
241
241
  act(() => {
242
242
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
243
243
  fireEvent.contextMenu(contextMenuWrapper);
@@ -295,7 +295,7 @@ describe('HitContextMenu', () => {
295
295
  createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
296
296
  ];
297
297
  mockDispatchApi.mockResolvedValue({ items: mockActions });
298
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
298
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
299
299
  act(() => {
300
300
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
301
301
  fireEvent.contextMenu(contextMenuWrapper);
@@ -339,7 +339,7 @@ describe('HitContextMenu', () => {
339
339
  });
340
340
  it('should disable custom actions menu when no actions are available', async () => {
341
341
  mockDispatchApi.mockResolvedValueOnce({ items: [] });
342
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
342
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
343
343
  act(() => {
344
344
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
345
345
  fireEvent.contextMenu(contextMenuWrapper);
@@ -381,7 +381,7 @@ describe('HitContextMenu', () => {
381
381
  skip_rationale: true
382
382
  }
383
383
  }));
384
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
384
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
385
385
  act(() => {
386
386
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
387
387
  fireEvent.contextMenu(contextMenuWrapper);
@@ -449,7 +449,7 @@ describe('HitContextMenu', () => {
449
449
  it('should call executeAction with action_id and hit query', async () => {
450
450
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
451
451
  mockDispatchApi.mockResolvedValue({ items: mockActions });
452
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
452
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
453
453
  act(() => {
454
454
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
455
455
  fireEvent.contextMenu(contextMenuWrapper);
@@ -502,7 +502,7 @@ describe('HitContextMenu', () => {
502
502
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
503
503
  keys: ['howler.detection', 'event.id']
504
504
  }));
505
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
505
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
506
506
  });
507
507
  it('should render exclusion submenu with template keys', async () => {
508
508
  act(() => {
@@ -550,7 +550,7 @@ describe('HitContextMenu', () => {
550
550
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
551
551
  keys: ['howler.outline.indicators']
552
552
  }));
553
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
553
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
554
554
  act(() => {
555
555
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
556
556
  fireEvent.contextMenu(contextMenuWrapper);
@@ -593,7 +593,7 @@ describe('HitContextMenu', () => {
593
593
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
594
594
  keys: []
595
595
  }));
596
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
596
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
597
597
  act(() => {
598
598
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
599
599
  fireEvent.contextMenu(contextMenuWrapper);
@@ -605,7 +605,7 @@ describe('HitContextMenu', () => {
605
605
  });
606
606
  it('should skip null field values in exclusion menu', async () => {
607
607
  act(() => {
608
- mockHitContext.hits['test-hit-1'].event = {};
608
+ mockRecordContext.records['test-hit-1'].event = {};
609
609
  });
610
610
  act(() => {
611
611
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -628,7 +628,7 @@ describe('HitContextMenu', () => {
628
628
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
629
629
  keys: ['howler.detection', 'event.id']
630
630
  }));
631
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
631
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
632
632
  });
633
633
  it('should render inclusion submenu with template keys', async () => {
634
634
  act(() => {
@@ -676,7 +676,7 @@ describe('HitContextMenu', () => {
676
676
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
677
677
  keys: ['howler.outline.indicators']
678
678
  }));
679
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
679
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
680
680
  act(() => {
681
681
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
682
682
  fireEvent.contextMenu(contextMenuWrapper);
@@ -719,7 +719,7 @@ describe('HitContextMenu', () => {
719
719
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
720
720
  keys: []
721
721
  }));
722
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
722
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
723
723
  act(() => {
724
724
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
725
725
  fireEvent.contextMenu(contextMenuWrapper);
@@ -731,7 +731,7 @@ describe('HitContextMenu', () => {
731
731
  });
732
732
  it('should skip null field values in inclusion menu', async () => {
733
733
  act(() => {
734
- mockHitContext.hits['test-hit-1'].event = {};
734
+ mockRecordContext.records['test-hit-1'].event = {};
735
735
  });
736
736
  act(() => {
737
737
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -750,24 +750,24 @@ describe('HitContextMenu', () => {
750
750
  });
751
751
  });
752
752
  describe('Multiple Hit Selection', () => {
753
- it('should use selectedHits when current hit is included', async () => {
753
+ it('should use selectedRecords when current hit is included', async () => {
754
754
  act(() => {
755
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
756
- mockHitContext.hits['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
757
- mockHitContext.selectedHits.push(mockHitContext.hits['hit-1'], mockHitContext.hits['hit-2']);
755
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
756
+ mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
757
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
758
758
  mockGetSelectedId.mockReturnValue('hit-1');
759
759
  });
760
760
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
761
761
  await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
762
- // The component should use selectedHits for actions
762
+ // The component should use selectedRecords for actions
763
763
  // We can verify this indirectly through the useHitActions hook receiving the right data
764
764
  expect(screen.getByRole('menu')).toBeInTheDocument();
765
765
  expect(mockGetSelectedId).toHaveBeenCalled();
766
766
  });
767
- it('should use only current hit when not in selectedHits', async () => {
767
+ it('should use only current hit when not in selectedRecords', async () => {
768
768
  act(() => {
769
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
770
- mockHitContext.selectedHits.push(mockHitContext.hits['hit-1']);
769
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
770
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
771
771
  mockGetSelectedId.mockReturnValue('test-hit-1');
772
772
  });
773
773
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -786,12 +786,12 @@ describe('HitContextMenu', () => {
786
786
  });
787
787
  it('should call getMatchingAnalytic when hit has analytic', async () => {
788
788
  await waitFor(() => {
789
- expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
789
+ expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
790
790
  });
791
791
  });
792
792
  it('should call getMatchingTemplate when menu opens', async () => {
793
793
  await waitFor(() => {
794
- expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
794
+ expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
795
795
  });
796
796
  });
797
797
  it('should reset state when menu closes', async () => {
@@ -824,7 +824,7 @@ describe('HitContextMenu', () => {
824
824
  describe('Edge Cases and Error Handling', () => {
825
825
  it('should not crash when hit is null', async () => {
826
826
  act(() => {
827
- mockHitContext.hits = {};
827
+ mockRecordContext.records = {};
828
828
  });
829
829
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
830
830
  fireEvent.contextMenu(contextMenuWrapper);
@@ -835,7 +835,7 @@ describe('HitContextMenu', () => {
835
835
  });
836
836
  it('should not render exclusion menu when template is null', async () => {
837
837
  mockGetMatchingTemplate.mockResolvedValue(null);
838
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
838
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
839
839
  act(() => {
840
840
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
841
841
  fireEvent.contextMenu(contextMenuWrapper);
@@ -849,7 +849,7 @@ describe('HitContextMenu', () => {
849
849
  });
850
850
  it('should not render inclusion menu when template is null', async () => {
851
851
  mockGetMatchingTemplate.mockResolvedValue(null);
852
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
852
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
853
853
  act(() => {
854
854
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
855
855
  fireEvent.contextMenu(contextMenuWrapper);
@@ -863,7 +863,7 @@ describe('HitContextMenu', () => {
863
863
  });
864
864
  it('should handle API failure gracefully', async () => {
865
865
  mockDispatchApi.mockResolvedValue(null);
866
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
866
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
867
867
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
868
868
  fireEvent.contextMenu(contextMenuWrapper);
869
869
  await waitFor(() => {
@@ -874,7 +874,7 @@ describe('HitContextMenu', () => {
874
874
  });
875
875
  it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
876
876
  act(() => {
877
- mockHitContext.hits['test-hit-1'].howler.analytic = null;
877
+ mockRecordContext.records['test-hit-1'].howler.analytic = null;
878
878
  });
879
879
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
880
880
  fireEvent.contextMenu(contextMenuWrapper);
@@ -0,0 +1,7 @@
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 FC } from 'react';
4
+ declare const RecordRelated: FC<{
5
+ record: Hit | Observable;
6
+ }>;
7
+ export default RecordRelated;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Stack, Tab, Tabs, useTheme } from '@mui/material';
3
+ import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
4
+ import useRelatedRecords from '@cccsaurora/howler-ui/components/hooks/useRelatedRecords';
5
+ import { groupBy } from 'lodash-es';
6
+ import { useMemo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Link } from 'react-router-dom';
9
+ import { isCase, isHit, isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
10
+ import CaseCard from '../case/CaseCard';
11
+ import HitCard from '../hit/HitCard';
12
+ import { HitLayout } from '../hit/HitLayout';
13
+ import RelatedLink from '../hit/related/RelatedLink';
14
+ const RecordRelated = ({ record }) => {
15
+ const theme = useTheme();
16
+ const { t } = useTranslation();
17
+ const related = useMemo(() => record?.howler.related ?? [], [record?.howler.related]);
18
+ const records = useRelatedRecords(related, related.length > 0);
19
+ const groups = groupBy(records, '__index');
20
+ const hasLinks = (record?.howler.links?.length ?? 0) > 0;
21
+ const tabs = [
22
+ hasLinks && 'links',
23
+ groups.hit?.length > 0 && 'hit',
24
+ groups.case?.length > 0 && 'case',
25
+ groups.observable?.length > 0 && 'observable'
26
+ ].filter(Boolean);
27
+ const [activeTab, setActiveTab] = useState(false);
28
+ const currentTab = activeTab !== false && tabs.includes(activeTab) ? activeTab : (tabs[0] ?? false);
29
+ if (!record) {
30
+ return null;
31
+ }
32
+ return (_jsxs(Box, { sx: { borderTop: `thin solid ${theme.palette.divider}`, height: '100%', flex: 1, mr: 2, pb: 2 }, children: [_jsxs(Tabs, { value: currentTab, onChange: (_, v) => setActiveTab(v), variant: "scrollable", scrollButtons: "auto", children: [hasLinks && _jsx(Tab, { value: "links", label: t('hit.related.tab.links') }), groups.hit?.length > 0 && _jsx(Tab, { value: "hit", label: t('hit.related.tab.hit') }), groups.case?.length > 0 && _jsx(Tab, { value: "case", label: t('hit.related.tab.case') }), groups.observable?.length > 0 && _jsx(Tab, { value: "observable", label: t('hit.related.tab.observable') })] }), currentTab === 'links' && (_jsx(Box, { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 1, pt: 1, children: record.howler.links.map(l => (_jsx(RelatedLink, { ...l }, l.title + l.href))) })), currentTab === 'hit' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isHit).map(h => (_jsx(Link, { to: `/hits/${h.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(HitCard, { id: h.howler.id, layout: HitLayout.NORMAL }) }, h.howler.id))) })), currentTab === 'case' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isCase).map(c => (_jsx(Link, { to: `/cases/${c.case_id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(CaseCard, { case: c }) }, c.case_id))) })), currentTab === 'observable' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isObservable).map(o => (_jsx(Link, { to: `/observables/${o.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(ObservableCard, { observable: o }) }, o.howler.id))) }))] }));
33
+ };
34
+ export default RecordRelated;
@@ -1,10 +1,11 @@
1
1
  import type { HowlerUser } from '@cccsaurora/howler-ui/models/entities/HowlerUser';
2
2
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
4
  import type { FC } from 'react';
4
- declare const HitWorklog: FC<{
5
- hit: Hit;
5
+ declare const RecordWorklog: FC<{
6
+ record: Hit | Observable;
6
7
  users: {
7
8
  [id: string]: HowlerUser;
8
9
  };
9
10
  }>;
10
- export default HitWorklog;
11
+ export default RecordWorklog;
@@ -10,7 +10,7 @@ import { compareTimestamp, twitterShort } from '@cccsaurora/howler-ui/utils/util
10
10
  import HowlerAvatar from '../display/HowlerAvatar';
11
11
  import HowlerCard from '../display/HowlerCard';
12
12
  import Markdown from '../display/Markdown';
13
- const HitWorklog = ({ hit, users }) => {
13
+ const RecordWorklog = ({ record, users }) => {
14
14
  const theme = useTheme();
15
15
  const { shiftColor } = useMyUtils();
16
16
  const { t } = useTranslation();
@@ -23,15 +23,15 @@ const HitWorklog = ({ hit, users }) => {
23
23
  */
24
24
  const worklogGroups = useMemo(() => {
25
25
  let setInitialVersion = false;
26
- return (hit?.howler?.log || [])
26
+ return (record?.howler?.log || [])
27
27
  .slice()
28
28
  .sort((a, b) => compareTimestamp(b.timestamp, a.timestamp))
29
29
  .reduce((acc, l) => {
30
- if (!initialVersions[hit.howler.id] && !setInitialVersion) {
30
+ if (!initialVersions[record.howler.id] && !setInitialVersion) {
31
31
  setInitialVersion = true;
32
32
  setInitialVersions({
33
33
  ...initialVersions,
34
- [hit.howler.id]: l.previous_version
34
+ [record.howler.id]: l.previous_version
35
35
  });
36
36
  }
37
37
  // Initialize the worklog card groups
@@ -42,9 +42,9 @@ const HitWorklog = ({ hit, users }) => {
42
42
  const currArr = acc[acc.length - 1];
43
43
  if (
44
44
  // Does this log version match the saved version?
45
- l.previous_version === initialVersions[hit.howler.id] &&
45
+ l.previous_version === initialVersions[record.howler.id] &&
46
46
  // Does the previous entry not match?
47
- currArr[currArr.length - 1].previous_version !== initialVersions[hit.howler.id]) {
47
+ currArr[currArr.length - 1].previous_version !== initialVersions[record.howler.id]) {
48
48
  // If so, we've figured out where the new logs should start, so we start a new card.
49
49
  acc.push([l]);
50
50
  return acc;
@@ -59,14 +59,14 @@ const HitWorklog = ({ hit, users }) => {
59
59
  }, []);
60
60
  },
61
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
62
- [hit?.howler?.log]);
62
+ [record?.howler?.log]);
63
63
  useEffect(() => {
64
64
  // On unmount, mark the latest entry version as the last seen version.
65
65
  return () => {
66
- if (hit?.howler.id) {
66
+ if (record?.howler.id) {
67
67
  setInitialVersions({
68
68
  ...initialVersions,
69
- [hit.howler.id]: worklogGroups[0][0]?.previous_version ?? initialVersions[hit.howler.id]
69
+ [record.howler.id]: worklogGroups[0][0]?.previous_version ?? initialVersions[record.howler.id]
70
70
  });
71
71
  }
72
72
  };
@@ -77,7 +77,9 @@ const HitWorklog = ({ hit, users }) => {
77
77
  if (worklogGroups.length > 0) {
78
78
  return worklogGroups.flatMap((ls, index) => {
79
79
  const result = [];
80
- if (index > 0 && initialVersions[hit.howler.id] && ls[0].previous_version === initialVersions[hit.howler.id]) {
80
+ if (index > 0 &&
81
+ initialVersions[record.howler.id] &&
82
+ ls[0].previous_version === initialVersions[record.howler.id]) {
81
83
  result.push(_jsx(Divider, { children: _jsxs(Stack, { direction: "row", children: [_jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('hit.worklog.new') }), _jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" })] }) }, "new"));
82
84
  }
83
85
  result.push(_jsxs(HowlerCard, { elevation: 4, children: [_jsx(CardHeader, { avatar: _jsx(HowlerAvatar, { userId: ls[0].user }), title: users[ls[0].user]?.name ?? ls[0].user, subheader: _jsx(Tooltip, { title: new Date(ls[0].timestamp).toLocaleString(), children: _jsx(Typography, { variant: "caption", children: twitterShort(ls[0].timestamp) }) }) }), _jsx(CardContent, { children: _jsx(Stack, { spacing: 1, divider: _jsx(Divider, { orientation: "horizontal" }), children: ls.map(l => (_jsxs(Typography, { variant: "body2", color: "text.secondary", component: "div", position: "relative", children: [l.explanation ? (_jsx(Markdown, { md: l.explanation.trim() })) : (_jsxs(_Fragment, { children: [_jsxs("span", { children: [t('hit.worklog.updated'), "\u00A0"] }), _jsx("code", { children: l.key }), _jsx("span", { children: ":\u00A0" }), {
@@ -88,10 +90,10 @@ const HitWorklog = ({ hit, users }) => {
88
90
  return result;
89
91
  });
90
92
  }
91
- else if (!hit?.howler) {
93
+ else if (!record?.howler) {
92
94
  return (_jsxs(_Fragment, { children: [_jsx(Skeleton, { width: "100%", height: 200, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 220, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 150, variant: "rounded" })] }));
93
95
  }
94
- }, [worklogGroups, hit.howler, initialVersions, users, t, shiftColor, theme.palette.text.primary]);
96
+ }, [worklogGroups, record.howler, initialVersions, users, t, shiftColor, theme.palette.text.primary]);
95
97
  return (_jsx(Stack, { sx: { p: 2 }, spacing: 1, children: worklogEls }));
96
98
  };
97
- export default HitWorklog;
99
+ export default RecordWorklog;
@@ -3,6 +3,7 @@ interface ViewTitleProps {
3
3
  title?: string;
4
4
  type?: string;
5
5
  query?: string;
6
+ indexes?: string[];
6
7
  sort?: string;
7
8
  span?: string;
8
9
  }
@@ -4,7 +4,7 @@ import { Chip, Stack, Tooltip, Typography } from '@mui/material';
4
4
  import { useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
7
- export const ViewTitle = ({ title, type, query, sort, span }) => {
7
+ export const ViewTitle = ({ title, type, query, sort, span, indexes }) => {
8
8
  const { t } = useTranslation();
9
9
  const spanLabel = useMemo(() => {
10
10
  if (!span) {
@@ -17,9 +17,16 @@ export const ViewTitle = ({ title, type, query, sort, span }) => {
17
17
  return t(span);
18
18
  }
19
19
  }, [span, t]);
20
+ const indexLabel = useMemo(() => {
21
+ if (!indexes || indexes.length === 0) {
22
+ return '';
23
+ }
24
+ else
25
+ return `(${indexes.join(', ')})`;
26
+ }, [indexes]);
20
27
  return (_jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", alignItems: "start", spacing: 1, children: [_jsx(Tooltip, { title: t(`route.views.manager.${type}`), children: {
21
28
  readonly: _jsx(Lock, { fontSize: "small" }),
22
29
  global: _jsx(Language, { fontSize: "small" }),
23
30
  personal: _jsx(Person, { fontSize: "small" })
24
- }[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { size: "small", label: spanLabel })] }))] }));
31
+ }[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span || indexLabel) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { label: spanLabel }), indexLabel && _jsx(Chip, { label: indexLabel })] }))] }));
25
32
  };
@@ -7,7 +7,7 @@ declare const useHitActions: (_hits: Hit | Hit[]) => {
7
7
  canAssess: boolean;
8
8
  loading: boolean;
9
9
  manage: (transition: string) => Promise<void>;
10
- assess: (assessment: string, skipRationale?: boolean) => Promise<void>;
10
+ assess: (assessment: string, skipRationale?: boolean, providedRationale?: any) => Promise<void>;
11
11
  vote: (v: string) => Promise<void>;
12
12
  selectedVote: string;
13
13
  };
@@ -4,8 +4,8 @@ import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
4
  import AssignUserDrawer from '@cccsaurora/howler-ui/components/app/drawers/AssignUserDrawer';
5
5
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
6
  import { AppDrawerContext } from '@cccsaurora/howler-ui/components/app/providers/AppDrawerProvider';
7
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
7
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
8
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
9
9
  import RationaleModal from '@cccsaurora/howler-ui/components/elements/display/modals/RationaleModal';
10
10
  import { useCallback, useContext, useMemo, useState } from 'react';
11
11
  import { useTranslation } from 'react-i18next';
@@ -31,7 +31,7 @@ const useHitActions = (_hits) => {
31
31
  const { showModal } = useContext(ModalContext);
32
32
  const { showWarningMessage } = useMySnackbar();
33
33
  const { dispatchApi } = useMyApi();
34
- const updateHit = useContextSelector(HitContext, ctx => ctx.updateHit);
34
+ const updateHit = useContextSelector(RecordContext, ctx => ctx.updateRecord);
35
35
  const [loading, setLoading] = useState(false);
36
36
  const hits = useMemo(() => (Array.isArray(_hits) ? _hits : [_hits]).filter(_hit => !!_hit), [_hits]);
37
37
  const canVote = useMemo(() => hits.every(hit => hit?.howler.assignment !== user.username || hit?.howler.status === 'in-progress'), [hits, user.username]);
@@ -90,9 +90,9 @@ const useHitActions = (_hits) => {
90
90
  }
91
91
  }
92
92
  }, [dispatchApi, hits, selectedVote, updateHit, user.email]);
93
- const assess = useCallback(async (assessment, skipRationale = false) => {
93
+ const assess = useCallback(async (assessment, skipRationale = false, providedRationale = null) => {
94
94
  const rationale = skipRationale
95
- ? t('rationale.default', { assessment })
95
+ ? (providedRationale ?? t('rationale.default', { assessment }))
96
96
  : await new Promise(res => {
97
97
  showModal(_jsx(RationaleModal, { hits: hits, onSubmit: _rationale => {
98
98
  res(_rationale);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Api, Article, Book, Code, Dashboard, Description, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
2
+ import { Api, Article, Book, BookRounded, Code, Dashboard, Description, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
3
3
  import { Stack } from '@mui/material';
4
4
  import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
5
5
  import { AppBarContext } from '@cccsaurora/howler-ui/components/app/providers/AppBarProvider';
@@ -24,6 +24,15 @@ const useMyPreferences = () => {
24
24
  icon: _jsx(Dashboard, {})
25
25
  }
26
26
  },
27
+ {
28
+ type: 'item',
29
+ element: {
30
+ id: 'cases',
31
+ i18nKey: 'route.cases',
32
+ route: '/cases',
33
+ icon: _jsx(BookRounded, {})
34
+ }
35
+ },
27
36
  {
28
37
  type: 'group',
29
38
  element: {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Alert, Box, Typography } from '@mui/material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
- import HitQuickSearch from '@cccsaurora/howler-ui/components/elements/hit/HitQuickSearch';
4
+ import HitPreview from '@cccsaurora/howler-ui/components/elements/hit/HitPreview';
5
5
  import { useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Link, useNavigate } from 'react-router-dom';
@@ -40,7 +40,7 @@ const useMySearch = () => {
40
40
  },
41
41
  headerRenderer: (state) => (state.result?.error || !state.items) && (_jsx(Box, { sx: { p: 1, pb: 0, textAlign: 'center' }, children: state.result?.error ? (_jsx(Alert, { severity: "error", color: "error", children: t('hit.search.invalid') })) : ((!state.items || state.items.length === 0) && (_jsx(Typography, { sx: { mb: -1, color: 'text.secondary' }, children: t('hit.quicksearch') }))) })),
42
42
  itemRenderer: (item, options) => {
43
- return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(HitQuickSearch, { hit: item.item, options: options }) }));
43
+ return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(HitPreview, { hit: item.item, options: options }) }));
44
44
  }
45
45
  }), [navigate, pageCount, t]);
46
46
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Article, Book, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
2
+ import { Article, Book, BookRounded, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
3
3
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
4
4
  import { useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -24,6 +24,9 @@ const useMySitemap = () => {
24
24
  return useMemo(() => ({
25
25
  routes: [
26
26
  { path: '/', title: t('route.home'), isRoot: true, icon: _jsx(Dashboard, {}) },
27
+ { path: '/cases', title: t('route.cases'), isRoot: true, icon: _jsx(BookRounded, {}) },
28
+ { path: '/cases/:id', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
29
+ { path: '/cases/:id/*', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
27
30
  { path: '/admin/users', title: t('route.admin.user.search'), isRoot: true, icon: _jsx(PersonSearch, {}) },
28
31
  {
29
32
  path: '/admin/users/:id',
@@ -1,9 +1,16 @@
1
1
  const DEFAULT_THEME = {
2
+ components: {
3
+ MuiChip: {
4
+ defaultProps: {
5
+ size: 'small'
6
+ }
7
+ }
8
+ },
2
9
  palette: {
3
10
  dark: {
4
11
  background: {
5
- default: '#202020',
6
- paper: '#202020'
12
+ default: '#181818',
13
+ paper: '#181818'
7
14
  },
8
15
  primary: {
9
16
  main: '#7DA1DB'
@@ -5,10 +5,9 @@ import { MemoryRouter, useSearchParams } from 'react-router-dom';
5
5
  import { describe, expect, it } from 'vitest';
6
6
  import useParamState from './useParamState';
7
7
  // Creates a MemoryRouter wrapper using createElement to avoid JSX in a .ts file
8
- const makeWrapper = (search = '') => {
9
- // eslint-disable-next-line react/function-component-definition
10
- return ({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
11
- };
8
+ const makeWrapper = (search = '') =>
9
+ // eslint-disable-next-line react/function-component-definition
10
+ ({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
12
11
  // Composite hook: exposes the param state AND the live URL params for URL-level assertions
13
12
  const useParamStateWithUrl = (key, defaultValue) => {
14
13
  const [value, setValue] = useParamState(key, defaultValue);
@@ -1,8 +1,8 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
2
  import type React from 'react';
3
- declare const useHitSelection: () => {
3
+ declare const useRecordSelection: () => {
4
4
  lastSelected: string;
5
5
  setLastSelected: React.Dispatch<React.SetStateAction<string>>;
6
6
  onClick: (e: React.MouseEvent<HTMLDivElement>, hit: Hit) => void;
7
7
  };
8
- export default useHitSelection;
8
+ export default useRecordSelection;