@cccsaurora/howler-ui 2.18.0-dev.732 → 2.18.0-dev.736

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 (273) 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/facet/hit.d.ts +1 -3
  6. package/api/search/facet/index.d.ts +3 -1
  7. package/api/search/index.d.ts +2 -1
  8. package/api/search/index.js +2 -1
  9. package/api/v2/case/index.d.ts +8 -0
  10. package/api/v2/case/index.js +20 -0
  11. package/api/v2/case/items.d.ts +6 -0
  12. package/api/v2/case/items.js +18 -0
  13. package/api/v2/index.d.ts +4 -0
  14. package/api/v2/index.js +6 -0
  15. package/api/v2/search/facet.d.ts +3 -0
  16. package/api/v2/search/facet.js +12 -0
  17. package/api/v2/search/index.d.ts +5 -0
  18. package/api/v2/search/index.js +24 -0
  19. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  20. package/components/app/App.js +39 -7
  21. package/components/app/hooks/useMatchers.d.ts +1 -1
  22. package/components/app/hooks/useMatchers.js +23 -11
  23. package/components/app/hooks/useMatchers.test.js +22 -22
  24. package/components/app/hooks/useTitle.js +3 -3
  25. package/components/app/providers/FavouritesProvider.js +2 -2
  26. package/components/app/providers/ModalProvider.d.ts +1 -0
  27. package/components/app/providers/ParameterProvider.d.ts +9 -2
  28. package/components/app/providers/ParameterProvider.js +165 -240
  29. package/components/app/providers/ParameterProvider.test.js +346 -94
  30. package/components/app/providers/RecordProvider.d.ts +23 -0
  31. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  32. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  33. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  34. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  35. package/components/app/providers/UserListProvider.js +28 -8
  36. package/components/elements/ContextMenu.d.ts +56 -0
  37. package/components/elements/ContextMenu.js +109 -0
  38. package/components/elements/ContextMenu.test.js +215 -0
  39. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  40. package/components/elements/ObjectDetails.d.ts +6 -0
  41. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  42. package/components/elements/PluginTypography.d.ts +2 -1
  43. package/components/elements/PluginTypography.js +3 -2
  44. package/components/elements/UserList.d.ts +5 -2
  45. package/components/elements/UserList.js +18 -8
  46. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  47. package/components/elements/case/CaseCard.d.ts +12 -0
  48. package/components/elements/case/CaseCard.js +42 -0
  49. package/components/elements/case/CasePreview.d.ts +6 -0
  50. package/components/elements/case/CasePreview.js +17 -0
  51. package/components/elements/case/StatusIcon.d.ts +5 -0
  52. package/components/elements/case/StatusIcon.js +13 -0
  53. package/components/elements/display/ChipPopper.d.ts +1 -1
  54. package/components/elements/display/HowlerCard.js +1 -1
  55. package/components/elements/display/Modal.js +2 -0
  56. package/components/elements/hit/HitActions.js +4 -4
  57. package/components/elements/hit/HitBanner.d.ts +1 -0
  58. package/components/elements/hit/HitBanner.js +29 -49
  59. package/components/elements/hit/HitCard.d.ts +2 -0
  60. package/components/elements/hit/HitCard.js +7 -7
  61. package/components/elements/hit/HitLabels.js +2 -2
  62. package/components/elements/hit/HitOutline.d.ts +1 -0
  63. package/components/elements/hit/HitOutline.js +3 -3
  64. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  65. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  66. package/components/elements/hit/HitSummary.d.ts +2 -1
  67. package/components/elements/hit/HitSummary.js +6 -5
  68. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  69. package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
  70. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  71. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  72. package/components/elements/hit/related/RelatedRecords.js +63 -0
  73. package/components/elements/observable/ObservableCard.d.ts +6 -0
  74. package/components/elements/observable/ObservableCard.js +22 -0
  75. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  76. package/components/elements/observable/ObservablePreview.js +12 -0
  77. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  78. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  79. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  80. package/components/elements/record/RecordContextMenu.js +247 -0
  81. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  82. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -39
  83. package/components/elements/record/RecordRelated.d.ts +7 -0
  84. package/components/elements/record/RecordRelated.js +34 -0
  85. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  86. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  87. package/components/elements/view/ViewTitle.d.ts +1 -0
  88. package/components/elements/view/ViewTitle.js +9 -2
  89. package/components/hooks/useHitActions.d.ts +1 -1
  90. package/components/hooks/useHitActions.js +4 -4
  91. package/components/hooks/useMyPreferences.js +10 -1
  92. package/components/hooks/useMySearch.js +2 -2
  93. package/components/hooks/useMySitemap.js +4 -1
  94. package/components/hooks/useMyTheme.js +9 -2
  95. package/components/hooks/useParamState.test.js +3 -4
  96. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  97. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  98. package/components/hooks/useRelatedRecords.d.ts +13 -0
  99. package/components/hooks/useRelatedRecords.js +32 -0
  100. package/components/routes/action/edit/ActionEditor.js +2 -2
  101. package/components/routes/action/view/ActionSearch.js +1 -1
  102. package/components/routes/advanced/QueryBuilder.js +1 -1
  103. package/components/routes/advanced/QueryEditor.js +3 -3
  104. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  105. package/components/routes/analytics/AnalyticDetails.js +2 -2
  106. package/components/routes/analytics/AnalyticSearch.js +1 -1
  107. package/components/routes/cases/CaseViewer.d.ts +2 -0
  108. package/components/routes/cases/CaseViewer.js +22 -0
  109. package/components/routes/cases/Cases.d.ts +2 -0
  110. package/components/routes/cases/Cases.js +101 -0
  111. package/components/routes/cases/constants.d.ts +5 -0
  112. package/components/routes/cases/constants.js +5 -0
  113. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  114. package/components/routes/cases/detail/AlertPanel.js +33 -0
  115. package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
  116. package/components/routes/cases/detail/CaseAssets.js +104 -0
  117. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  118. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  119. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  120. package/components/routes/cases/detail/CaseDashboard.js +66 -0
  121. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  122. package/components/routes/cases/detail/CaseDetails.js +61 -0
  123. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  124. package/components/routes/cases/detail/CaseOverview.js +43 -0
  125. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  126. package/components/routes/cases/detail/CaseSidebar.js +107 -0
  127. package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
  128. package/components/routes/cases/detail/CaseSidebar.test.js +246 -0
  129. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  130. package/components/routes/cases/detail/CaseTask.js +57 -0
  131. package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
  132. package/components/routes/cases/detail/CaseTimeline.js +106 -0
  133. package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
  134. package/components/routes/cases/detail/CaseTimeline.test.js +227 -0
  135. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  136. package/components/routes/cases/detail/ItemPage.js +99 -0
  137. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  138. package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
  139. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  140. package/components/routes/cases/detail/TaskPanel.js +52 -0
  141. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
  142. package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
  143. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  144. package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
  145. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  146. package/components/routes/cases/detail/assets/Asset.js +12 -0
  147. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  148. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  149. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
  150. package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
  151. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
  152. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
  153. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  154. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
  155. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  156. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
  157. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
  158. package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
  159. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
  160. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
  161. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
  162. package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
  163. package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
  164. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  165. package/components/routes/cases/detail/sidebar/utils.js +29 -0
  166. package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
  167. package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
  168. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  169. package/components/routes/cases/hooks/useCase.js +51 -0
  170. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  171. package/components/routes/cases/modals/AddToCaseModal.js +62 -0
  172. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  173. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  174. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  175. package/components/routes/cases/modals/ResolveModal.js +115 -0
  176. package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
  177. package/components/routes/cases/modals/ResolveModal.test.js +384 -0
  178. package/components/routes/dossiers/DossierEditor.js +2 -2
  179. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  180. package/components/routes/help/ApiDocumentation.js +1 -1
  181. package/components/routes/help/HitBannerDocumentation.js +1 -0
  182. package/components/routes/help/HitDocumentation.js +1 -3
  183. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  184. package/components/routes/hits/search/InformationPane.js +47 -60
  185. package/components/routes/hits/search/LayoutSettings.js +3 -3
  186. package/components/routes/hits/search/QuerySettings.js +2 -1
  187. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  188. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  189. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  190. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  191. package/components/routes/hits/search/SearchPane.js +26 -49
  192. package/components/routes/hits/search/ViewLink.js +3 -3
  193. package/components/routes/hits/search/ViewLink.test.js +8 -8
  194. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  195. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  196. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  197. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  198. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  199. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  200. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  201. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  202. package/components/routes/hits/view/HitViewer.js +12 -13
  203. package/components/routes/home/ViewCard.js +47 -41
  204. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  205. package/components/routes/observables/ObservableViewer.js +27 -0
  206. package/components/routes/overviews/OverviewViewer.js +2 -2
  207. package/components/routes/views/ViewComposer.js +46 -19
  208. package/locales/en/translation.json +89 -3
  209. package/locales/fr/translation.json +87 -3
  210. package/models/WithMetadata.d.ts +2 -1
  211. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  212. package/models/entities/generated/Case.d.ts +28 -0
  213. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  214. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  215. package/models/entities/generated/EmailParent.d.ts +19 -0
  216. package/models/entities/generated/Enrichments.d.ts +7 -0
  217. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  218. package/models/entities/generated/Hit.d.ts +1 -0
  219. package/models/entities/generated/Howler.d.ts +0 -4
  220. package/models/entities/generated/HttpResponse.d.ts +11 -0
  221. package/models/entities/generated/Item.d.ts +9 -0
  222. package/models/entities/generated/Observable.d.ts +85 -0
  223. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  224. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  225. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  226. package/models/entities/generated/ObservableFile.d.ts +36 -0
  227. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  228. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  229. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  230. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  231. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  232. package/models/entities/generated/ObservableSource.d.ts +23 -0
  233. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  234. package/models/entities/generated/ObservableTls.d.ts +12 -0
  235. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  236. package/models/entities/generated/Rule.d.ts +2 -10
  237. package/models/entities/generated/Task.d.ts +10 -0
  238. package/models/entities/generated/Threat.d.ts +2 -2
  239. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  240. package/models/entities/generated/View.d.ts +1 -0
  241. package/package.json +114 -97
  242. package/plugins/clue/components/ClueTypography.js +2 -2
  243. package/plugins/clue/utils.d.ts +2 -1
  244. package/tests/mocks.d.ts +11 -1
  245. package/tests/mocks.js +12 -7
  246. package/tests/server-handlers.js +6 -1
  247. package/tests/utils.d.ts +4 -0
  248. package/tests/utils.js +20 -0
  249. package/utils/constants.d.ts +3 -3
  250. package/utils/hitFunctions.d.ts +2 -1
  251. package/utils/hitFunctions.js +4 -4
  252. package/utils/typeUtils.d.ts +7 -0
  253. package/utils/typeUtils.js +27 -0
  254. package/utils/viewUtils.js +3 -0
  255. package/components/app/providers/HitProvider.d.ts +0 -22
  256. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  257. package/components/elements/display/icons/BundleButton.js +0 -32
  258. package/components/elements/hit/HitRelated.d.ts +0 -6
  259. package/components/elements/hit/HitRelated.js +0 -7
  260. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  261. package/components/routes/help/BundleDocumentation.js +0 -12
  262. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  263. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  264. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  265. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  266. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  267. package/components/routes/hits/search/BundleScroller.js +0 -6
  268. package/components/routes/hits/search/HitContextMenu.js +0 -227
  269. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  270. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  271. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  272. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  273. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -47,6 +47,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
47
47
  getMatchingTemplate: mockGetMatchingTemplate
48
48
  }))
49
49
  }));
50
+ const mockShowModal = vi.fn();
50
51
  const mockDispatchApi = vi.fn();
51
52
  vi.mock('components/hooks/useMyApi', () => ({
52
53
  default: vi.fn(() => ({
@@ -72,6 +73,9 @@ vi.mock('plugins/store', () => ({
72
73
  plugins: ['plugin1']
73
74
  }
74
75
  }));
76
+ vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
77
+ default: () => null
78
+ }));
75
79
  // Mock MUI components
76
80
  vi.mock('@mui/material', async () => {
77
81
  const actual = await vi.importActual('@mui/material');
@@ -91,13 +95,14 @@ vi.mock('@mui/material', async () => {
91
95
  });
92
96
  // Import component after mocks
93
97
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
94
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
98
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
95
99
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
100
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
96
101
  import i18n from '@cccsaurora/howler-ui/i18n';
97
102
  import { I18nextProvider } from 'react-i18next';
98
103
  import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
99
104
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
100
- import HitContextMenu from './HitContextMenu';
105
+ import RecordContextMenu from './RecordContextMenu';
101
106
  const mockGetSelectedId = vi.fn(() => 'test-hit-1');
102
107
  const mockConfig = {
103
108
  lookups: {
@@ -105,16 +110,16 @@ const mockConfig = {
105
110
  }
106
111
  };
107
112
  const mockApiContext = { config: mockConfig };
108
- const mockHitContext = {
109
- hits: {
113
+ const mockRecordContext = {
114
+ records: {
110
115
  'test-hit-1': createMockHit()
111
116
  },
112
- selectedHits: []
117
+ selectedRecords: []
113
118
  };
114
119
  const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
115
120
  // Test wrapper
116
121
  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 }) }) }) }));
122
+ return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(ModalContext.Provider, { value: { showModal: mockShowModal }, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }) }));
118
123
  };
119
124
  describe('HitContextMenu', () => {
120
125
  let user;
@@ -122,11 +127,11 @@ describe('HitContextMenu', () => {
122
127
  beforeEach(() => {
123
128
  user = userEvent.setup();
124
129
  vi.clearAllMocks();
125
- mockHitContext.selectedHits.length = 0;
126
- mockHitContext.hits['test-hit-1'] = createMockHit();
130
+ mockRecordContext.selectedRecords.length = 0;
131
+ mockRecordContext.records['test-hit-1'] = createMockHit();
127
132
  mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
128
133
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
129
- rerender = render(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
134
+ rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
130
135
  });
131
136
  describe('Context Menu Initialization', () => {
132
137
  it('should open menu on right-click', async () => {
@@ -190,13 +195,13 @@ describe('HitContextMenu', () => {
190
195
  });
191
196
  it('should disable "Open Hit" when hit is null', async () => {
192
197
  act(() => {
193
- mockHitContext.hits['test-hit-1'] = null;
198
+ mockRecordContext.records['test-hit-1'] = null;
194
199
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
195
200
  fireEvent.contextMenu(contextMenuWrapper);
196
201
  });
197
202
  await waitFor(() => {
198
203
  const menuItems = screen.getAllByRole('menuitem');
199
- const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
204
+ const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
200
205
  expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
201
206
  });
202
207
  });
@@ -237,7 +242,7 @@ describe('HitContextMenu', () => {
237
242
  skip_rationale: false
238
243
  }
239
244
  }));
240
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
245
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
241
246
  act(() => {
242
247
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
243
248
  fireEvent.contextMenu(contextMenuWrapper);
@@ -295,7 +300,7 @@ describe('HitContextMenu', () => {
295
300
  createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
296
301
  ];
297
302
  mockDispatchApi.mockResolvedValue({ items: mockActions });
298
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
303
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
299
304
  act(() => {
300
305
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
301
306
  fireEvent.contextMenu(contextMenuWrapper);
@@ -339,7 +344,7 @@ describe('HitContextMenu', () => {
339
344
  });
340
345
  it('should disable custom actions menu when no actions are available', async () => {
341
346
  mockDispatchApi.mockResolvedValueOnce({ items: [] });
342
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
347
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
343
348
  act(() => {
344
349
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
345
350
  fireEvent.contextMenu(contextMenuWrapper);
@@ -381,7 +386,7 @@ describe('HitContextMenu', () => {
381
386
  skip_rationale: true
382
387
  }
383
388
  }));
384
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
389
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
385
390
  act(() => {
386
391
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
387
392
  fireEvent.contextMenu(contextMenuWrapper);
@@ -449,7 +454,7 @@ describe('HitContextMenu', () => {
449
454
  it('should call executeAction with action_id and hit query', async () => {
450
455
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
451
456
  mockDispatchApi.mockResolvedValue({ items: mockActions });
452
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
457
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
453
458
  act(() => {
454
459
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
455
460
  fireEvent.contextMenu(contextMenuWrapper);
@@ -502,7 +507,7 @@ describe('HitContextMenu', () => {
502
507
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
503
508
  keys: ['howler.detection', 'event.id']
504
509
  }));
505
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
510
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
506
511
  });
507
512
  it('should render exclusion submenu with template keys', async () => {
508
513
  act(() => {
@@ -550,7 +555,7 @@ describe('HitContextMenu', () => {
550
555
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
551
556
  keys: ['howler.outline.indicators']
552
557
  }));
553
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
558
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
554
559
  act(() => {
555
560
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
556
561
  fireEvent.contextMenu(contextMenuWrapper);
@@ -593,7 +598,7 @@ describe('HitContextMenu', () => {
593
598
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
594
599
  keys: []
595
600
  }));
596
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
601
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
597
602
  act(() => {
598
603
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
599
604
  fireEvent.contextMenu(contextMenuWrapper);
@@ -605,7 +610,7 @@ describe('HitContextMenu', () => {
605
610
  });
606
611
  it('should skip null field values in exclusion menu', async () => {
607
612
  act(() => {
608
- mockHitContext.hits['test-hit-1'].event = {};
613
+ mockRecordContext.records['test-hit-1'].event = {};
609
614
  });
610
615
  act(() => {
611
616
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -628,7 +633,7 @@ describe('HitContextMenu', () => {
628
633
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
629
634
  keys: ['howler.detection', 'event.id']
630
635
  }));
631
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
636
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
632
637
  });
633
638
  it('should render inclusion submenu with template keys', async () => {
634
639
  act(() => {
@@ -676,7 +681,7 @@ describe('HitContextMenu', () => {
676
681
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
677
682
  keys: ['howler.outline.indicators']
678
683
  }));
679
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
684
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
680
685
  act(() => {
681
686
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
682
687
  fireEvent.contextMenu(contextMenuWrapper);
@@ -719,7 +724,7 @@ describe('HitContextMenu', () => {
719
724
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
720
725
  keys: []
721
726
  }));
722
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
727
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
723
728
  act(() => {
724
729
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
725
730
  fireEvent.contextMenu(contextMenuWrapper);
@@ -731,7 +736,7 @@ describe('HitContextMenu', () => {
731
736
  });
732
737
  it('should skip null field values in inclusion menu', async () => {
733
738
  act(() => {
734
- mockHitContext.hits['test-hit-1'].event = {};
739
+ mockRecordContext.records['test-hit-1'].event = {};
735
740
  });
736
741
  act(() => {
737
742
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -750,24 +755,24 @@ describe('HitContextMenu', () => {
750
755
  });
751
756
  });
752
757
  describe('Multiple Hit Selection', () => {
753
- it('should use selectedHits when current hit is included', async () => {
758
+ it('should use selectedRecords when current hit is included', async () => {
754
759
  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']);
760
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
761
+ mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
762
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
758
763
  mockGetSelectedId.mockReturnValue('hit-1');
759
764
  });
760
765
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
761
766
  await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
762
- // The component should use selectedHits for actions
767
+ // The component should use selectedRecords for actions
763
768
  // We can verify this indirectly through the useHitActions hook receiving the right data
764
769
  expect(screen.getByRole('menu')).toBeInTheDocument();
765
770
  expect(mockGetSelectedId).toHaveBeenCalled();
766
771
  });
767
- it('should use only current hit when not in selectedHits', async () => {
772
+ it('should use only current hit when not in selectedRecords', async () => {
768
773
  act(() => {
769
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
770
- mockHitContext.selectedHits.push(mockHitContext.hits['hit-1']);
774
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
775
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
771
776
  mockGetSelectedId.mockReturnValue('test-hit-1');
772
777
  });
773
778
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -786,12 +791,12 @@ describe('HitContextMenu', () => {
786
791
  });
787
792
  it('should call getMatchingAnalytic when hit has analytic', async () => {
788
793
  await waitFor(() => {
789
- expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
794
+ expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
790
795
  });
791
796
  });
792
797
  it('should call getMatchingTemplate when menu opens', async () => {
793
798
  await waitFor(() => {
794
- expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
799
+ expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
795
800
  });
796
801
  });
797
802
  it('should reset state when menu closes', async () => {
@@ -824,7 +829,7 @@ describe('HitContextMenu', () => {
824
829
  describe('Edge Cases and Error Handling', () => {
825
830
  it('should not crash when hit is null', async () => {
826
831
  act(() => {
827
- mockHitContext.hits = {};
832
+ mockRecordContext.records = {};
828
833
  });
829
834
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
830
835
  fireEvent.contextMenu(contextMenuWrapper);
@@ -835,7 +840,7 @@ describe('HitContextMenu', () => {
835
840
  });
836
841
  it('should not render exclusion menu when template is null', async () => {
837
842
  mockGetMatchingTemplate.mockResolvedValue(null);
838
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
843
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
839
844
  act(() => {
840
845
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
841
846
  fireEvent.contextMenu(contextMenuWrapper);
@@ -849,7 +854,7 @@ describe('HitContextMenu', () => {
849
854
  });
850
855
  it('should not render inclusion menu when template is null', async () => {
851
856
  mockGetMatchingTemplate.mockResolvedValue(null);
852
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
857
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
853
858
  act(() => {
854
859
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
855
860
  fireEvent.contextMenu(contextMenuWrapper);
@@ -863,7 +868,7 @@ describe('HitContextMenu', () => {
863
868
  });
864
869
  it('should handle API failure gracefully', async () => {
865
870
  mockDispatchApi.mockResolvedValue(null);
866
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
871
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
867
872
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
868
873
  fireEvent.contextMenu(contextMenuWrapper);
869
874
  await waitFor(() => {
@@ -874,7 +879,7 @@ describe('HitContextMenu', () => {
874
879
  });
875
880
  it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
876
881
  act(() => {
877
- mockHitContext.hits['test-hit-1'].howler.analytic = null;
882
+ mockRecordContext.records['test-hit-1'].howler.analytic = null;
878
883
  });
879
884
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
880
885
  fireEvent.contextMenu(contextMenuWrapper);
@@ -893,4 +898,54 @@ describe('HitContextMenu', () => {
893
898
  expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
894
899
  });
895
900
  });
901
+ describe('Add to Case Menu Item', () => {
902
+ it('should render "Add to Case" item in the menu', async () => {
903
+ act(() => {
904
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
905
+ fireEvent.contextMenu(contextMenuWrapper);
906
+ });
907
+ await waitFor(() => {
908
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
909
+ });
910
+ });
911
+ it('should enable "Add to Case" when a record is present', async () => {
912
+ act(() => {
913
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
914
+ fireEvent.contextMenu(contextMenuWrapper);
915
+ });
916
+ await waitFor(() => {
917
+ const menuItems = screen.getAllByRole('menuitem');
918
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
919
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'false');
920
+ });
921
+ });
922
+ it('should disable "Add to Case" when record is null', async () => {
923
+ act(() => {
924
+ mockRecordContext.records['test-hit-1'] = null;
925
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
926
+ fireEvent.contextMenu(contextMenuWrapper);
927
+ });
928
+ await waitFor(() => {
929
+ const menuItems = screen.getAllByRole('menuitem');
930
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
931
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'true');
932
+ });
933
+ });
934
+ it('should call showModal with an AddToCaseModal element when clicked', async () => {
935
+ act(() => {
936
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
937
+ fireEvent.contextMenu(contextMenuWrapper);
938
+ });
939
+ await waitFor(() => {
940
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
941
+ });
942
+ await act(async () => {
943
+ await user.click(screen.getByText('Add to Case'));
944
+ });
945
+ await waitFor(() => {
946
+ expect(mockShowModal).toHaveBeenCalledOnce();
947
+ expect(mockShowModal).toHaveBeenCalledWith(expect.objectContaining({ type: expect.any(Function) }));
948
+ });
949
+ });
950
+ });
896
951
  });
@@ -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'), isLeaf: true, 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',