@cccsaurora/howler-ui 2.18.0 → 2.19.0-cases.861

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 (342) hide show
  1. package/api/index.d.ts +4 -0
  2. package/api/index.js +10 -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/socket/index.d.ts +3 -0
  10. package/api/socket/index.js +6 -0
  11. package/api/socket/viewers.d.ts +2 -0
  12. package/api/socket/viewers.js +8 -0
  13. package/api/socket/viewers.test.js +44 -0
  14. package/api/v2/case/index.d.ts +9 -0
  15. package/api/v2/case/index.js +21 -0
  16. package/api/v2/case/items.d.ts +6 -0
  17. package/api/v2/case/items.js +18 -0
  18. package/api/v2/case/rules.d.ts +6 -0
  19. package/api/v2/case/rules.js +18 -0
  20. package/api/v2/index.d.ts +4 -0
  21. package/api/v2/index.js +6 -0
  22. package/api/v2/search/facet.d.ts +3 -0
  23. package/api/v2/search/facet.js +12 -0
  24. package/api/v2/search/index.d.ts +5 -0
  25. package/api/v2/search/index.js +24 -0
  26. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  27. package/components/app/App.js +52 -12
  28. package/components/app/hooks/useMatchers.d.ts +1 -1
  29. package/components/app/hooks/useMatchers.js +23 -11
  30. package/components/app/hooks/useMatchers.test.js +22 -22
  31. package/components/app/hooks/useTitle.js +5 -5
  32. package/components/app/providers/FavouritesProvider.js +2 -2
  33. package/components/app/providers/ModalProvider.d.ts +1 -0
  34. package/components/app/providers/ParameterProvider.d.ts +9 -2
  35. package/components/app/providers/ParameterProvider.js +165 -240
  36. package/components/app/providers/ParameterProvider.test.js +346 -94
  37. package/components/app/providers/RecordProvider.d.ts +23 -0
  38. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  39. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  40. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  41. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
  42. package/components/app/providers/SocketProvider.d.ts +11 -2
  43. package/components/app/providers/SocketProvider.js +18 -5
  44. package/components/app/providers/UserListProvider.js +28 -8
  45. package/components/elements/ContextMenu.d.ts +56 -0
  46. package/components/elements/ContextMenu.js +109 -0
  47. package/components/elements/ContextMenu.test.d.ts +1 -0
  48. package/components/elements/ContextMenu.test.js +215 -0
  49. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  50. package/components/elements/ObjectDetails.d.ts +6 -0
  51. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  52. package/components/elements/PluginTypography.d.ts +2 -1
  53. package/components/elements/PluginTypography.js +3 -2
  54. package/components/elements/UserList.d.ts +5 -2
  55. package/components/elements/UserList.js +18 -8
  56. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  57. package/components/elements/case/CaseCard.d.ts +12 -0
  58. package/components/elements/case/CaseCard.js +42 -0
  59. package/components/elements/case/CasePreview.d.ts +6 -0
  60. package/components/elements/case/CasePreview.js +17 -0
  61. package/components/elements/case/StatusIcon.d.ts +5 -0
  62. package/components/elements/case/StatusIcon.js +13 -0
  63. package/components/elements/display/ChipPopper.d.ts +1 -1
  64. package/components/elements/display/ChipPopper.js +5 -5
  65. package/components/elements/display/HowlerCard.js +1 -1
  66. package/components/elements/display/Modal.js +2 -0
  67. package/components/elements/hit/HitActions.js +4 -4
  68. package/components/elements/hit/HitBanner.d.ts +1 -0
  69. package/components/elements/hit/HitBanner.js +34 -51
  70. package/components/elements/hit/HitCard.d.ts +2 -0
  71. package/components/elements/hit/HitCard.js +7 -7
  72. package/components/elements/hit/HitLabels.js +2 -2
  73. package/components/elements/hit/HitOutline.d.ts +1 -0
  74. package/components/elements/hit/HitOutline.js +3 -3
  75. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  76. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  77. package/components/elements/hit/HitSummary.d.ts +2 -1
  78. package/components/elements/hit/HitSummary.js +6 -5
  79. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  80. package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
  81. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  82. package/components/elements/hit/elements/Assigned.js +6 -3
  83. package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
  84. package/components/elements/hit/elements/Assigned.test.js +65 -0
  85. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  86. package/components/elements/hit/related/RelatedRecords.js +63 -0
  87. package/components/elements/observable/ObservableCard.d.ts +6 -0
  88. package/components/elements/observable/ObservableCard.js +22 -0
  89. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  90. package/components/elements/observable/ObservablePreview.js +12 -0
  91. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  92. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  93. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  94. package/components/elements/record/RecordContextMenu.js +268 -0
  95. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  96. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +190 -39
  97. package/components/elements/record/RecordRelated.d.ts +7 -0
  98. package/components/elements/record/RecordRelated.js +34 -0
  99. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  100. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  101. package/components/elements/view/ViewTitle.d.ts +1 -0
  102. package/components/elements/view/ViewTitle.js +9 -2
  103. package/components/hooks/useHitActions.d.ts +1 -1
  104. package/components/hooks/useHitActions.js +4 -4
  105. package/components/hooks/useMyPreferences.js +10 -1
  106. package/components/hooks/useMySearch.js +2 -2
  107. package/components/hooks/useMySitemap.js +4 -1
  108. package/components/hooks/useMyTheme.js +9 -2
  109. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  110. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  111. package/components/hooks/useRelatedRecords.d.ts +13 -0
  112. package/components/hooks/useRelatedRecords.js +32 -0
  113. package/components/routes/403.d.ts +3 -0
  114. package/components/routes/403.js +10 -0
  115. package/components/routes/action/edit/ActionEditor.js +3 -3
  116. package/components/routes/action/useMyActionFunctions.js +4 -1
  117. package/components/routes/action/view/ActionDetails.js +6 -1
  118. package/components/routes/action/view/ActionSearch.js +5 -4
  119. package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
  120. package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
  121. package/components/routes/advanced/QueryBuilder.js +1 -1
  122. package/components/routes/advanced/QueryEditor.js +3 -3
  123. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  124. package/components/routes/analytics/AnalyticDetails.js +2 -2
  125. package/components/routes/analytics/AnalyticSearch.js +1 -1
  126. package/components/routes/cases/CaseViewer.d.ts +2 -0
  127. package/components/routes/cases/CaseViewer.js +44 -0
  128. package/components/routes/cases/CaseViewer.test.d.ts +1 -0
  129. package/components/routes/cases/CaseViewer.test.js +133 -0
  130. package/components/routes/cases/Cases.d.ts +2 -0
  131. package/components/routes/cases/Cases.js +148 -0
  132. package/components/routes/cases/constants.d.ts +6 -0
  133. package/components/routes/cases/constants.js +6 -0
  134. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  135. package/components/routes/cases/detail/AlertPanel.js +33 -0
  136. package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
  137. package/components/routes/cases/detail/CaseAssets.js +104 -0
  138. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  139. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  140. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  141. package/components/routes/cases/detail/CaseDashboard.js +66 -0
  142. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  143. package/components/routes/cases/detail/CaseDetails.js +70 -0
  144. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  145. package/components/routes/cases/detail/CaseOverview.js +43 -0
  146. package/components/routes/cases/detail/CaseRules.d.ts +7 -0
  147. package/components/routes/cases/detail/CaseRules.js +57 -0
  148. package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
  149. package/components/routes/cases/detail/CaseRules.test.js +221 -0
  150. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  151. package/components/routes/cases/detail/CaseSidebar.js +107 -0
  152. package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
  153. package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
  154. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  155. package/components/routes/cases/detail/CaseTask.js +66 -0
  156. package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
  157. package/components/routes/cases/detail/CaseTimeline.js +106 -0
  158. package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
  159. package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
  160. package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
  161. package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
  162. package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
  163. package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
  164. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  165. package/components/routes/cases/detail/ItemPage.js +95 -0
  166. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  167. package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
  168. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  169. package/components/routes/cases/detail/TaskPanel.js +52 -0
  170. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
  171. package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
  172. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  173. package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
  174. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  175. package/components/routes/cases/detail/assets/Asset.js +12 -0
  176. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  177. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  178. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
  179. package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
  180. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
  181. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
  182. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  183. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
  184. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  185. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
  186. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
  187. package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
  188. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
  189. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
  190. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
  191. package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
  192. package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
  193. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  194. package/components/routes/cases/detail/sidebar/utils.js +29 -0
  195. package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
  196. package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
  197. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  198. package/components/routes/cases/hooks/useCase.js +69 -0
  199. package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
  200. package/components/routes/cases/hooks/useCase.test.js +141 -0
  201. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  202. package/components/routes/cases/modals/AddToCaseModal.js +59 -0
  203. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
  204. package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
  205. package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
  206. package/components/routes/cases/modals/CaseRecordRow.js +15 -0
  207. package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
  208. package/components/routes/cases/modals/CreateCaseModal.js +55 -0
  209. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
  210. package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
  211. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  212. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  213. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  214. package/components/routes/cases/modals/ResolveModal.js +115 -0
  215. package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
  216. package/components/routes/cases/modals/ResolveModal.test.js +394 -0
  217. package/components/routes/cases/modals/hooks.d.ts +7 -0
  218. package/components/routes/cases/modals/hooks.js +44 -0
  219. package/components/routes/cases/modals/types.d.ts +5 -0
  220. package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
  221. package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
  222. package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
  223. package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
  224. package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
  225. package/components/routes/cases/search/CaseDateFilter.js +26 -0
  226. package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
  227. package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
  228. package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
  229. package/components/routes/cases/search/CaseStatusFilter.js +13 -0
  230. package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
  231. package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
  232. package/components/routes/dossiers/DossierEditor.js +2 -2
  233. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  234. package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
  235. package/components/routes/help/ApiDocumentation.js +1 -1
  236. package/components/routes/help/HitBannerDocumentation.js +1 -0
  237. package/components/routes/help/HitDocumentation.js +1 -3
  238. package/components/routes/help/markdown/en/retention.md.js +1 -1
  239. package/components/routes/help/markdown/fr/retention.md.js +1 -1
  240. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  241. package/components/routes/hits/search/InformationPane.js +50 -63
  242. package/components/routes/hits/search/LayoutSettings.js +3 -3
  243. package/components/routes/hits/search/QuerySettings.js +2 -1
  244. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  245. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  246. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  247. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  248. package/components/routes/hits/search/SearchPane.js +26 -49
  249. package/components/routes/hits/search/ViewLink.js +3 -3
  250. package/components/routes/hits/search/ViewLink.test.js +8 -8
  251. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  252. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  253. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  254. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  255. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  256. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  257. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  258. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  259. package/components/routes/hits/view/HitViewer.js +12 -13
  260. package/components/routes/home/AddNewCard.js +1 -1
  261. package/components/routes/home/ViewCard.js +47 -41
  262. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  263. package/components/routes/observables/ObservableViewer.js +27 -0
  264. package/components/routes/overviews/OverviewViewer.js +2 -2
  265. package/components/routes/overviews/template/en.md.js +1 -1
  266. package/components/routes/overviews/template/fr.md.js +1 -1
  267. package/components/routes/views/ViewComposer.js +46 -19
  268. package/locales/en/translation.json +125 -3
  269. package/locales/fr/translation.json +123 -3
  270. package/models/WithMetadata.d.ts +2 -1
  271. package/models/entities/generated/ApiType.d.ts +1 -1
  272. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  273. package/models/entities/generated/Case.d.ts +28 -0
  274. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  275. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  276. package/models/entities/generated/EmailParent.d.ts +19 -0
  277. package/models/entities/generated/Enrichments.d.ts +7 -0
  278. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  279. package/models/entities/generated/Hit.d.ts +1 -0
  280. package/models/entities/generated/Howler.d.ts +0 -5
  281. package/models/entities/generated/HttpResponse.d.ts +11 -0
  282. package/models/entities/generated/Item.d.ts +9 -0
  283. package/models/entities/generated/Observable.d.ts +85 -0
  284. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  285. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  286. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  287. package/models/entities/generated/ObservableFile.d.ts +36 -0
  288. package/models/entities/generated/ObservableHowler.d.ts +42 -0
  289. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  290. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  291. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  292. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  293. package/models/entities/generated/ObservableSource.d.ts +23 -0
  294. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  295. package/models/entities/generated/ObservableTls.d.ts +12 -0
  296. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  297. package/models/entities/generated/Rule.d.ts +6 -9
  298. package/models/entities/generated/Task.d.ts +10 -0
  299. package/models/entities/generated/Threat.d.ts +2 -2
  300. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  301. package/models/entities/generated/View.d.ts +1 -0
  302. package/models/socket/CaseUpdate.d.ts +5 -0
  303. package/models/socket/ViewersUpdate.d.ts +4 -0
  304. package/package.json +23 -8
  305. package/plugins/clue/components/ClueTypography.js +2 -2
  306. package/plugins/clue/utils.d.ts +2 -1
  307. package/tests/mocks.d.ts +11 -1
  308. package/tests/mocks.js +12 -7
  309. package/tests/server-handlers.js +6 -1
  310. package/tests/utils.d.ts +4 -0
  311. package/tests/utils.js +20 -0
  312. package/utils/constants.d.ts +4 -3
  313. package/utils/constants.js +6 -0
  314. package/utils/hitFunctions.d.ts +2 -1
  315. package/utils/hitFunctions.js +4 -4
  316. package/utils/menuUtils.js +1 -1
  317. package/utils/socketUtils.d.ts +14 -0
  318. package/utils/socketUtils.js +17 -1
  319. package/utils/socketUtils.test.d.ts +1 -0
  320. package/utils/socketUtils.test.js +59 -0
  321. package/utils/typeUtils.d.ts +7 -0
  322. package/utils/typeUtils.js +27 -0
  323. package/utils/viewUtils.js +3 -0
  324. package/components/app/providers/HitProvider.d.ts +0 -22
  325. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  326. package/components/elements/display/icons/BundleButton.js +0 -32
  327. package/components/elements/hit/HitRelated.d.ts +0 -6
  328. package/components/elements/hit/HitRelated.js +0 -7
  329. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  330. package/components/routes/help/BundleDocumentation.js +0 -12
  331. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  332. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  333. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  334. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  335. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  336. package/components/routes/hits/search/BundleScroller.js +0 -6
  337. package/components/routes/hits/search/HitContextMenu.js +0 -229
  338. /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
  339. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
  340. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  341. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  342. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -8,6 +8,16 @@ import { vi } from 'vitest';
8
8
  // Mock API
9
9
  vi.mock('api', { spy: true });
10
10
  setupContextSelectorMock();
11
+ // Mock useAppUser hook
12
+ const mockUseAppUser = vi.hoisted(() => vi.fn(() => ({
13
+ user: {
14
+ username: 'test-user',
15
+ roles: ['automation_basic', 'actionrunner_basic']
16
+ }
17
+ })));
18
+ vi.mock('commons/components/app/hooks/useAppUser', () => ({
19
+ useAppUser: mockUseAppUser
20
+ }));
11
21
  // Mock react-router-dom
12
22
  const mockNavigate = vi.fn();
13
23
  vi.mock('react-router-dom', async () => {
@@ -47,6 +57,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
47
57
  getMatchingTemplate: mockGetMatchingTemplate
48
58
  }))
49
59
  }));
60
+ const mockShowModal = vi.fn();
50
61
  const mockDispatchApi = vi.fn();
51
62
  vi.mock('components/hooks/useMyApi', () => ({
52
63
  default: vi.fn(() => ({
@@ -72,6 +83,9 @@ vi.mock('plugins/store', () => ({
72
83
  plugins: ['plugin1']
73
84
  }
74
85
  }));
86
+ vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
87
+ default: () => null
88
+ }));
75
89
  // Mock MUI components
76
90
  vi.mock('@mui/material', async () => {
77
91
  const actual = await vi.importActual('@mui/material');
@@ -91,13 +105,14 @@ vi.mock('@mui/material', async () => {
91
105
  });
92
106
  // Import component after mocks
93
107
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
94
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
108
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
95
109
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
110
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
96
111
  import i18n from '@cccsaurora/howler-ui/i18n';
97
112
  import { I18nextProvider } from 'react-i18next';
98
113
  import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
99
114
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
100
- import HitContextMenu from './HitContextMenu';
115
+ import RecordContextMenu from './RecordContextMenu';
101
116
  const mockGetSelectedId = vi.fn(() => 'test-hit-1');
102
117
  const mockConfig = {
103
118
  lookups: {
@@ -105,16 +120,16 @@ const mockConfig = {
105
120
  }
106
121
  };
107
122
  const mockApiContext = { config: mockConfig };
108
- const mockHitContext = {
109
- hits: {
123
+ const mockRecordContext = {
124
+ records: {
110
125
  'test-hit-1': createMockHit()
111
126
  },
112
- selectedHits: []
127
+ selectedRecords: []
113
128
  };
114
129
  const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
115
130
  // Test wrapper
116
131
  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 }) }) }) }));
132
+ 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
133
  };
119
134
  describe('HitContextMenu', () => {
120
135
  let user;
@@ -122,11 +137,11 @@ describe('HitContextMenu', () => {
122
137
  beforeEach(() => {
123
138
  user = userEvent.setup();
124
139
  vi.clearAllMocks();
125
- mockHitContext.selectedHits.length = 0;
126
- mockHitContext.hits['test-hit-1'] = createMockHit();
140
+ mockRecordContext.selectedRecords.length = 0;
141
+ mockRecordContext.records['test-hit-1'] = createMockHit();
127
142
  mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
128
143
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
129
- rerender = render(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
144
+ rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
130
145
  });
131
146
  describe('Context Menu Initialization', () => {
132
147
  it('should open menu on right-click', async () => {
@@ -190,13 +205,13 @@ describe('HitContextMenu', () => {
190
205
  });
191
206
  it('should disable "Open Hit" when hit is null', async () => {
192
207
  act(() => {
193
- mockHitContext.hits['test-hit-1'] = null;
208
+ mockRecordContext.records['test-hit-1'] = null;
194
209
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
195
210
  fireEvent.contextMenu(contextMenuWrapper);
196
211
  });
197
212
  await waitFor(() => {
198
213
  const menuItems = screen.getAllByRole('menuitem');
199
- const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
214
+ const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
200
215
  expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
201
216
  });
202
217
  });
@@ -237,7 +252,7 @@ describe('HitContextMenu', () => {
237
252
  skip_rationale: false
238
253
  }
239
254
  }));
240
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
255
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
241
256
  act(() => {
242
257
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
243
258
  fireEvent.contextMenu(contextMenuWrapper);
@@ -295,7 +310,7 @@ describe('HitContextMenu', () => {
295
310
  createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
296
311
  ];
297
312
  mockDispatchApi.mockResolvedValue({ items: mockActions });
298
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
313
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
299
314
  act(() => {
300
315
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
301
316
  fireEvent.contextMenu(contextMenuWrapper);
@@ -339,7 +354,7 @@ describe('HitContextMenu', () => {
339
354
  });
340
355
  it('should disable custom actions menu when no actions are available', async () => {
341
356
  mockDispatchApi.mockResolvedValueOnce({ items: [] });
342
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
357
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
343
358
  act(() => {
344
359
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
345
360
  fireEvent.contextMenu(contextMenuWrapper);
@@ -381,7 +396,7 @@ describe('HitContextMenu', () => {
381
396
  skip_rationale: true
382
397
  }
383
398
  }));
384
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
399
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
385
400
  act(() => {
386
401
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
387
402
  fireEvent.contextMenu(contextMenuWrapper);
@@ -449,7 +464,7 @@ describe('HitContextMenu', () => {
449
464
  it('should call executeAction with action_id and hit query', async () => {
450
465
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
451
466
  mockDispatchApi.mockResolvedValue({ items: mockActions });
452
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
467
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
453
468
  act(() => {
454
469
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
455
470
  fireEvent.contextMenu(contextMenuWrapper);
@@ -502,7 +517,7 @@ describe('HitContextMenu', () => {
502
517
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
503
518
  keys: ['howler.detection', 'event.id']
504
519
  }));
505
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
520
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
506
521
  });
507
522
  it('should render exclusion submenu with template keys', async () => {
508
523
  act(() => {
@@ -550,7 +565,7 @@ describe('HitContextMenu', () => {
550
565
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
551
566
  keys: ['howler.outline.indicators']
552
567
  }));
553
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
568
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
554
569
  act(() => {
555
570
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
556
571
  fireEvent.contextMenu(contextMenuWrapper);
@@ -593,7 +608,7 @@ describe('HitContextMenu', () => {
593
608
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
594
609
  keys: []
595
610
  }));
596
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
611
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
597
612
  act(() => {
598
613
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
599
614
  fireEvent.contextMenu(contextMenuWrapper);
@@ -605,7 +620,7 @@ describe('HitContextMenu', () => {
605
620
  });
606
621
  it('should skip null field values in exclusion menu', async () => {
607
622
  act(() => {
608
- mockHitContext.hits['test-hit-1'].event = {};
623
+ mockRecordContext.records['test-hit-1'].event = {};
609
624
  });
610
625
  act(() => {
611
626
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -628,7 +643,7 @@ describe('HitContextMenu', () => {
628
643
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
629
644
  keys: ['howler.detection', 'event.id']
630
645
  }));
631
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
646
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
632
647
  });
633
648
  it('should render inclusion submenu with template keys', async () => {
634
649
  act(() => {
@@ -676,7 +691,7 @@ describe('HitContextMenu', () => {
676
691
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
677
692
  keys: ['howler.outline.indicators']
678
693
  }));
679
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
694
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
680
695
  act(() => {
681
696
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
682
697
  fireEvent.contextMenu(contextMenuWrapper);
@@ -719,7 +734,7 @@ describe('HitContextMenu', () => {
719
734
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
720
735
  keys: []
721
736
  }));
722
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
737
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
723
738
  act(() => {
724
739
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
725
740
  fireEvent.contextMenu(contextMenuWrapper);
@@ -731,7 +746,7 @@ describe('HitContextMenu', () => {
731
746
  });
732
747
  it('should skip null field values in inclusion menu', async () => {
733
748
  act(() => {
734
- mockHitContext.hits['test-hit-1'].event = {};
749
+ mockRecordContext.records['test-hit-1'].event = {};
735
750
  });
736
751
  act(() => {
737
752
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -750,24 +765,24 @@ describe('HitContextMenu', () => {
750
765
  });
751
766
  });
752
767
  describe('Multiple Hit Selection', () => {
753
- it('should use selectedHits when current hit is included', async () => {
768
+ it('should use selectedRecords when current hit is included', async () => {
754
769
  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']);
770
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
771
+ mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
772
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
758
773
  mockGetSelectedId.mockReturnValue('hit-1');
759
774
  });
760
775
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
761
776
  await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
762
- // The component should use selectedHits for actions
777
+ // The component should use selectedRecords for actions
763
778
  // We can verify this indirectly through the useHitActions hook receiving the right data
764
779
  expect(screen.getByRole('menu')).toBeInTheDocument();
765
780
  expect(mockGetSelectedId).toHaveBeenCalled();
766
781
  });
767
- it('should use only current hit when not in selectedHits', async () => {
782
+ it('should use only current hit when not in selectedRecords', async () => {
768
783
  act(() => {
769
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
770
- mockHitContext.selectedHits.push(mockHitContext.hits['hit-1']);
784
+ mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
785
+ mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
771
786
  mockGetSelectedId.mockReturnValue('test-hit-1');
772
787
  });
773
788
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -786,12 +801,12 @@ describe('HitContextMenu', () => {
786
801
  });
787
802
  it('should call getMatchingAnalytic when hit has analytic', async () => {
788
803
  await waitFor(() => {
789
- expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
804
+ expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
790
805
  });
791
806
  });
792
807
  it('should call getMatchingTemplate when menu opens', async () => {
793
808
  await waitFor(() => {
794
- expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
809
+ expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
795
810
  });
796
811
  });
797
812
  it('should reset state when menu closes', async () => {
@@ -824,7 +839,7 @@ describe('HitContextMenu', () => {
824
839
  describe('Edge Cases and Error Handling', () => {
825
840
  it('should not crash when hit is null', async () => {
826
841
  act(() => {
827
- mockHitContext.hits = {};
842
+ mockRecordContext.records = {};
828
843
  });
829
844
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
830
845
  fireEvent.contextMenu(contextMenuWrapper);
@@ -835,7 +850,7 @@ describe('HitContextMenu', () => {
835
850
  });
836
851
  it('should not render exclusion menu when template is null', async () => {
837
852
  mockGetMatchingTemplate.mockResolvedValue(null);
838
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
853
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
839
854
  act(() => {
840
855
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
841
856
  fireEvent.contextMenu(contextMenuWrapper);
@@ -849,7 +864,7 @@ describe('HitContextMenu', () => {
849
864
  });
850
865
  it('should not render inclusion menu when template is null', async () => {
851
866
  mockGetMatchingTemplate.mockResolvedValue(null);
852
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
867
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
853
868
  act(() => {
854
869
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
855
870
  fireEvent.contextMenu(contextMenuWrapper);
@@ -863,7 +878,7 @@ describe('HitContextMenu', () => {
863
878
  });
864
879
  it('should handle API failure gracefully', async () => {
865
880
  mockDispatchApi.mockResolvedValue(null);
866
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
881
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
867
882
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
868
883
  fireEvent.contextMenu(contextMenuWrapper);
869
884
  await waitFor(() => {
@@ -874,7 +889,7 @@ describe('HitContextMenu', () => {
874
889
  });
875
890
  it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
876
891
  act(() => {
877
- mockHitContext.hits['test-hit-1'].howler.analytic = null;
892
+ mockRecordContext.records['test-hit-1'].howler.analytic = null;
878
893
  });
879
894
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
880
895
  fireEvent.contextMenu(contextMenuWrapper);
@@ -893,4 +908,140 @@ describe('HitContextMenu', () => {
893
908
  expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
894
909
  });
895
910
  });
911
+ describe('Add to Case Menu Item', () => {
912
+ it('should render "Add to Case" item in the menu', async () => {
913
+ act(() => {
914
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
915
+ fireEvent.contextMenu(contextMenuWrapper);
916
+ });
917
+ await waitFor(() => {
918
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
919
+ });
920
+ });
921
+ it('should enable "Add to Case" when a record is present', async () => {
922
+ act(() => {
923
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
924
+ fireEvent.contextMenu(contextMenuWrapper);
925
+ });
926
+ await waitFor(() => {
927
+ const menuItems = screen.getAllByRole('menuitem');
928
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
929
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'false');
930
+ });
931
+ });
932
+ it('should disable "Add to Case" when record is null', async () => {
933
+ act(() => {
934
+ mockRecordContext.records['test-hit-1'] = null;
935
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
936
+ fireEvent.contextMenu(contextMenuWrapper);
937
+ });
938
+ await waitFor(() => {
939
+ const menuItems = screen.getAllByRole('menuitem');
940
+ const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
941
+ expect(addToCaseItem).toHaveAttribute('aria-disabled', 'true');
942
+ });
943
+ });
944
+ it('should call showModal with an AddToCaseModal element when clicked', async () => {
945
+ act(() => {
946
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
947
+ fireEvent.contextMenu(contextMenuWrapper);
948
+ });
949
+ await waitFor(() => {
950
+ expect(screen.getByText('Add to Case')).toBeInTheDocument();
951
+ });
952
+ await act(async () => {
953
+ await user.click(screen.getByText('Add to Case'));
954
+ });
955
+ await waitFor(() => {
956
+ expect(mockShowModal).toHaveBeenCalledOnce();
957
+ expect(mockShowModal).toHaveBeenCalledWith(expect.objectContaining({ type: expect.any(Function) }), expect.objectContaining({ maxHeight: expect.any(String) }));
958
+ });
959
+ });
960
+ });
961
+ describe('Role-Based Action Permissions', () => {
962
+ afterEach(() => {
963
+ // Reset to default user with required roles
964
+ mockUseAppUser.mockReturnValue({
965
+ user: {
966
+ username: 'test-user',
967
+ roles: ['automation_basic', 'actionrunner_basic']
968
+ }
969
+ });
970
+ });
971
+ it('should disable actions menu when user lacks required roles', async () => {
972
+ mockUseAppUser.mockReturnValue({
973
+ user: {
974
+ username: 'test-user',
975
+ roles: ['user', 'viewer']
976
+ }
977
+ });
978
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
979
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
980
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
981
+ act(() => {
982
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
983
+ fireEvent.contextMenu(contextMenuWrapper);
984
+ });
985
+ await waitFor(() => {
986
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
987
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
988
+ });
989
+ });
990
+ it('should enable actions menu when user has automation_basic role', async () => {
991
+ mockUseAppUser.mockReturnValue({
992
+ user: {
993
+ username: 'test-user',
994
+ roles: ['automation_basic']
995
+ }
996
+ });
997
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
998
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
999
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1000
+ act(() => {
1001
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1002
+ fireEvent.contextMenu(contextMenuWrapper);
1003
+ });
1004
+ await waitFor(() => {
1005
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1006
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
1007
+ });
1008
+ });
1009
+ it('should enable actions menu when user has actionrunner_advanced role', async () => {
1010
+ mockUseAppUser.mockReturnValue({
1011
+ user: {
1012
+ username: 'test-user',
1013
+ roles: ['actionrunner_advanced']
1014
+ }
1015
+ });
1016
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
1017
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
1018
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1019
+ act(() => {
1020
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1021
+ fireEvent.contextMenu(contextMenuWrapper);
1022
+ });
1023
+ await waitFor(() => {
1024
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1025
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
1026
+ });
1027
+ });
1028
+ it('should still disable actions menu when user has roles but no actions available', async () => {
1029
+ mockUseAppUser.mockReturnValue({
1030
+ user: {
1031
+ username: 'test-user',
1032
+ roles: ['automation_advanced']
1033
+ }
1034
+ });
1035
+ mockDispatchApi.mockResolvedValue({ items: [] });
1036
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1037
+ act(() => {
1038
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1039
+ fireEvent.contextMenu(contextMenuWrapper);
1040
+ });
1041
+ await waitFor(() => {
1042
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1043
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
1044
+ });
1045
+ });
1046
+ });
896
1047
  });
@@ -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
  };