@cccsaurora/howler-ui 2.19.0-dev.839 → 2.19.0-dev.842

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 (328) 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 +41 -8
  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 +29 -47
  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} +98 -43
  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/action/edit/ActionEditor.js +2 -2
  114. package/components/routes/action/view/ActionSearch.js +1 -1
  115. package/components/routes/advanced/QueryBuilder.js +1 -1
  116. package/components/routes/advanced/QueryEditor.js +3 -3
  117. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  118. package/components/routes/analytics/AnalyticDetails.js +2 -2
  119. package/components/routes/analytics/AnalyticSearch.js +1 -1
  120. package/components/routes/cases/CaseViewer.d.ts +2 -0
  121. package/components/routes/cases/CaseViewer.js +44 -0
  122. package/components/routes/cases/CaseViewer.test.d.ts +1 -0
  123. package/components/routes/cases/CaseViewer.test.js +133 -0
  124. package/components/routes/cases/Cases.d.ts +2 -0
  125. package/components/routes/cases/Cases.js +148 -0
  126. package/components/routes/cases/constants.d.ts +6 -0
  127. package/components/routes/cases/constants.js +6 -0
  128. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  129. package/components/routes/cases/detail/AlertPanel.js +33 -0
  130. package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
  131. package/components/routes/cases/detail/CaseAssets.js +104 -0
  132. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  133. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  134. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  135. package/components/routes/cases/detail/CaseDashboard.js +66 -0
  136. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  137. package/components/routes/cases/detail/CaseDetails.js +70 -0
  138. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  139. package/components/routes/cases/detail/CaseOverview.js +43 -0
  140. package/components/routes/cases/detail/CaseRules.d.ts +7 -0
  141. package/components/routes/cases/detail/CaseRules.js +57 -0
  142. package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
  143. package/components/routes/cases/detail/CaseRules.test.js +221 -0
  144. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  145. package/components/routes/cases/detail/CaseSidebar.js +107 -0
  146. package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
  147. package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
  148. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  149. package/components/routes/cases/detail/CaseTask.js +66 -0
  150. package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
  151. package/components/routes/cases/detail/CaseTimeline.js +106 -0
  152. package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
  153. package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
  154. package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
  155. package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
  156. package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
  157. package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
  158. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  159. package/components/routes/cases/detail/ItemPage.js +95 -0
  160. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  161. package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
  162. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  163. package/components/routes/cases/detail/TaskPanel.js +52 -0
  164. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
  165. package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
  166. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  167. package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
  168. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  169. package/components/routes/cases/detail/assets/Asset.js +12 -0
  170. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  171. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  172. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
  173. package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
  174. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
  175. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
  176. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  177. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
  178. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  179. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
  180. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
  181. package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
  182. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
  183. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
  184. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
  185. package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
  186. package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
  187. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  188. package/components/routes/cases/detail/sidebar/utils.js +29 -0
  189. package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
  190. package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
  191. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  192. package/components/routes/cases/hooks/useCase.js +69 -0
  193. package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
  194. package/components/routes/cases/hooks/useCase.test.js +141 -0
  195. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  196. package/components/routes/cases/modals/AddToCaseModal.js +59 -0
  197. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
  198. package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
  199. package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
  200. package/components/routes/cases/modals/CaseRecordRow.js +15 -0
  201. package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
  202. package/components/routes/cases/modals/CreateCaseModal.js +55 -0
  203. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
  204. package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
  205. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  206. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  207. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  208. package/components/routes/cases/modals/ResolveModal.js +115 -0
  209. package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
  210. package/components/routes/cases/modals/ResolveModal.test.js +394 -0
  211. package/components/routes/cases/modals/hooks.d.ts +7 -0
  212. package/components/routes/cases/modals/hooks.js +44 -0
  213. package/components/routes/cases/modals/types.d.ts +5 -0
  214. package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
  215. package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
  216. package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
  217. package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
  218. package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
  219. package/components/routes/cases/search/CaseDateFilter.js +26 -0
  220. package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
  221. package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
  222. package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
  223. package/components/routes/cases/search/CaseStatusFilter.js +13 -0
  224. package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
  225. package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
  226. package/components/routes/dossiers/DossierEditor.js +2 -2
  227. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  228. package/components/routes/help/ApiDocumentation.js +1 -1
  229. package/components/routes/help/HitBannerDocumentation.js +1 -0
  230. package/components/routes/help/HitDocumentation.js +1 -3
  231. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  232. package/components/routes/hits/search/InformationPane.js +50 -63
  233. package/components/routes/hits/search/LayoutSettings.js +3 -3
  234. package/components/routes/hits/search/QuerySettings.js +2 -1
  235. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  236. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  237. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  238. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  239. package/components/routes/hits/search/SearchPane.js +26 -49
  240. package/components/routes/hits/search/ViewLink.js +3 -3
  241. package/components/routes/hits/search/ViewLink.test.js +8 -8
  242. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  243. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  244. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  245. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  246. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  247. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  248. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  249. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  250. package/components/routes/hits/view/HitViewer.js +12 -13
  251. package/components/routes/home/ViewCard.js +47 -41
  252. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  253. package/components/routes/observables/ObservableViewer.js +27 -0
  254. package/components/routes/overviews/OverviewViewer.js +2 -2
  255. package/components/routes/views/ViewComposer.js +46 -19
  256. package/locales/en/translation.json +122 -3
  257. package/locales/fr/translation.json +120 -3
  258. package/models/WithMetadata.d.ts +2 -1
  259. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  260. package/models/entities/generated/Case.d.ts +28 -0
  261. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  262. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  263. package/models/entities/generated/EmailParent.d.ts +19 -0
  264. package/models/entities/generated/Enrichments.d.ts +7 -0
  265. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  266. package/models/entities/generated/Hit.d.ts +1 -0
  267. package/models/entities/generated/Howler.d.ts +0 -5
  268. package/models/entities/generated/HttpResponse.d.ts +11 -0
  269. package/models/entities/generated/Item.d.ts +9 -0
  270. package/models/entities/generated/Observable.d.ts +85 -0
  271. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  272. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  273. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  274. package/models/entities/generated/ObservableFile.d.ts +36 -0
  275. package/models/entities/generated/ObservableHowler.d.ts +42 -0
  276. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  277. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  278. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  279. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  280. package/models/entities/generated/ObservableSource.d.ts +23 -0
  281. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  282. package/models/entities/generated/ObservableTls.d.ts +12 -0
  283. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  284. package/models/entities/generated/Rule.d.ts +6 -9
  285. package/models/entities/generated/Task.d.ts +10 -0
  286. package/models/entities/generated/Threat.d.ts +2 -2
  287. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  288. package/models/entities/generated/View.d.ts +1 -0
  289. package/models/socket/CaseUpdate.d.ts +5 -0
  290. package/models/socket/ViewersUpdate.d.ts +4 -0
  291. package/package.json +21 -1
  292. package/plugins/clue/components/ClueTypography.js +2 -2
  293. package/plugins/clue/utils.d.ts +2 -1
  294. package/tests/mocks.d.ts +11 -1
  295. package/tests/mocks.js +12 -7
  296. package/tests/server-handlers.js +6 -1
  297. package/tests/utils.d.ts +4 -0
  298. package/tests/utils.js +20 -0
  299. package/utils/constants.d.ts +4 -3
  300. package/utils/constants.js +6 -0
  301. package/utils/hitFunctions.d.ts +2 -1
  302. package/utils/hitFunctions.js +4 -4
  303. package/utils/socketUtils.d.ts +14 -0
  304. package/utils/socketUtils.js +17 -1
  305. package/utils/socketUtils.test.d.ts +1 -0
  306. package/utils/socketUtils.test.js +59 -0
  307. package/utils/typeUtils.d.ts +7 -0
  308. package/utils/typeUtils.js +27 -0
  309. package/utils/viewUtils.js +3 -0
  310. package/components/app/providers/HitProvider.d.ts +0 -22
  311. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  312. package/components/elements/display/icons/BundleButton.js +0 -32
  313. package/components/elements/hit/HitRelated.d.ts +0 -6
  314. package/components/elements/hit/HitRelated.js +0 -7
  315. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  316. package/components/routes/help/BundleDocumentation.js +0 -12
  317. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  318. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  319. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  320. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  321. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  322. package/components/routes/hits/search/BundleScroller.js +0 -6
  323. package/components/routes/hits/search/HitContextMenu.js +0 -239
  324. /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
  325. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
  326. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  327. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  328. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -0,0 +1,268 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, NoteAdd, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks/useAppUser';
5
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
6
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
7
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
8
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
9
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
10
+ import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
11
+ import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
12
+ import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
13
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
14
+ import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
15
+ import AddToCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/AddToCaseModal';
16
+ import CreateCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/CreateCaseModal';
17
+ import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
18
+ import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
19
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
20
+ import { useTranslation } from 'react-i18next';
21
+ import { usePluginStore } from 'react-pluggable';
22
+ import { useContextSelector } from 'use-context-selector';
23
+ import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
24
+ import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
25
+ import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
26
+ /**
27
+ * Order in which action types should be displayed in the context menu
28
+ */
29
+ const ORDER = ['assessment', 'vote', 'action'];
30
+ /**
31
+ * Icon mapping for different action types
32
+ */
33
+ const ICON_MAP = {
34
+ assessment: _jsx(Assignment, {}),
35
+ vote: _jsx(HowToVote, {}),
36
+ action: _jsx(Edit, {})
37
+ };
38
+ /**
39
+ * Context menu component for hit operations.
40
+ * Provides quick access to common hit actions including assessment, voting,
41
+ * transitions, and exclusion filters based on template fields.
42
+ */
43
+ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
44
+ const { t } = useTranslation();
45
+ const { dispatchApi } = useMyApi();
46
+ const { executeAction } = useMyActionFunctions();
47
+ const appUser = useAppUser();
48
+ const { config } = useContext(ApiConfigContext);
49
+ const { showModal } = useContext(ModalContext);
50
+ const pluginStore = usePluginStore();
51
+ const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
52
+ const query = useContextSelector(ParameterContext, ctx => ctx?.query);
53
+ const setQuery = useContextSelector(ParameterContext, ctx => ctx?.setQuery);
54
+ const [id, setId] = useState(null);
55
+ const record = useContextSelector(RecordContext, ctx => ctx.records[id]);
56
+ const selectedRecords = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
57
+ const [analytic, setAnalytic] = useState(null);
58
+ const [template, setTemplate] = useState(null);
59
+ const [actions, setActions] = useState([]);
60
+ const records = useMemo(() => selectedRecords.some(_record => _record.howler.id === record?.howler.id)
61
+ ? selectedRecords
62
+ : record
63
+ ? [record]
64
+ : [], [record, selectedRecords]);
65
+ const hits = useMemo(() => records.filter(isHit), [records]);
66
+ const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
67
+ /**
68
+ * Checks if the current user has permission to run actions.
69
+ * Users must have one of the automation or actionrunner roles, or be an admin.
70
+ */
71
+ const canRunActions = useCallback(() => {
72
+ const roles = ['admin', 'automation_advanced', 'automation_basic', 'actionrunner_advanced', 'actionrunner_basic'];
73
+ return roles.some((role) => appUser.user?.roles?.includes(role));
74
+ }, [appUser.user?.roles]);
75
+ /**
76
+ * Called by ContextMenu after the menu is positioned and opened.
77
+ * Identifies the clicked record and fetches available actions.
78
+ */
79
+ const onOpen = useCallback(async (event) => {
80
+ const _id = getSelectedId(event);
81
+ setId(_id);
82
+ // TODO: Bumping the number of rows is a temporary fix - we'll need to improve this.
83
+ const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*', rows: 100, sort: 'name asc' }), {
84
+ throwError: false
85
+ }))?.items;
86
+ if (_actions) {
87
+ setActions(_actions);
88
+ }
89
+ }, [dispatchApi, getSelectedId]);
90
+ const rowStatus = useMemo(() => ({
91
+ assessment: canAssess,
92
+ vote: canVote
93
+ }), [canAssess, canVote]);
94
+ const pluginActions = howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.actions`, records));
95
+ /**
96
+ * Generates grouped action entries for the context menu.
97
+ * Combines transitions, plugin actions, votes, and assessments based on permissions.
98
+ */
99
+ const entries = useMemo(() => {
100
+ let _actions = [...availableTransitions, ...pluginActions];
101
+ if (canVote) {
102
+ _actions = [
103
+ ..._actions,
104
+ ...VOTE_OPTIONS.map(option => ({ ...option, actionFunction: () => vote(option.name.toLowerCase()) }))
105
+ ];
106
+ }
107
+ if (config.lookups?.['howler.assessment'] && canAssess) {
108
+ _actions = [
109
+ ..._actions,
110
+ ...config.lookups['howler.assessment']
111
+ .filter(_assessment => analytic?.triage_settings?.valid_assessments
112
+ ? analytic.triage_settings?.valid_assessments.includes(_assessment)
113
+ : true)
114
+ .sort((a, b) => +TOP_ROW.includes(b) - +TOP_ROW.includes(a))
115
+ .map(assessment => ({
116
+ type: 'assessment',
117
+ name: assessment,
118
+ actionFunction: async () => {
119
+ await assess(assessment, analytic?.triage_settings?.skip_rationale);
120
+ }
121
+ }))
122
+ ];
123
+ }
124
+ return Object.entries(groupBy(_actions, 'type')).sort(([a], [b]) => ORDER.indexOf(a) - ORDER.indexOf(b));
125
+ }, [analytic, assess, availableTransitions, canAssess, canVote, config.lookups, vote, pluginActions]);
126
+ // Load analytic and template data when a hit is selected
127
+ useEffect(() => {
128
+ if (!record?.howler.analytic) {
129
+ return;
130
+ }
131
+ getMatchingAnalytic(record).then(setAnalytic);
132
+ getMatchingTemplate(record).then(setTemplate);
133
+ // eslint-disable-next-line react-hooks/exhaustive-deps
134
+ }, [record]);
135
+ /**
136
+ * Builds the declarative items structure for the ContextMenu component.
137
+ */
138
+ const items = useMemo(() => {
139
+ const result = [
140
+ {
141
+ kind: 'item',
142
+ id: 'open-record',
143
+ icon: _jsx(OpenInNew, {}),
144
+ label: t(`${record?.__index ?? 'hit'}.open`),
145
+ disabled: !record,
146
+ to: `/${record?.__index}s/${record?.howler.id}`
147
+ }
148
+ ];
149
+ if (isHit(record)) {
150
+ result.push({
151
+ kind: 'item',
152
+ id: 'open-analytic',
153
+ icon: _jsx(QueryStats, {}),
154
+ label: t('analytic.open'),
155
+ disabled: !analytic,
156
+ to: `/analytics/${analytic?.analytic_id}`
157
+ });
158
+ result.push({ kind: 'divider', id: 'actions-divider' });
159
+ for (const [type, typeItems] of entries) {
160
+ result.push({
161
+ kind: 'submenu',
162
+ id: type,
163
+ icon: ICON_MAP[type] ?? _jsx(Terminal, {}),
164
+ label: t(`hit.details.actions.${type}`),
165
+ disabled: rowStatus[type] === false,
166
+ items: typeItems.map(a => ({
167
+ key: a.name,
168
+ label: a.i18nKey ? t(a.i18nKey) : capitalize(a.name),
169
+ onClick: a.actionFunction
170
+ }))
171
+ });
172
+ }
173
+ result.push({
174
+ kind: 'submenu',
175
+ id: 'actions',
176
+ icon: _jsx(SettingsSuggest, {}),
177
+ label: t('route.actions.change'),
178
+ disabled: actions.length < 1 || !canRunActions(),
179
+ items: actions.map(action => ({
180
+ key: action.action_id,
181
+ label: action.name,
182
+ onClick: () => executeAction(action.action_id, `howler.id:${record?.howler.id}`)
183
+ }))
184
+ });
185
+ if (!isEmpty(template?.keys ?? []) && setQuery) {
186
+ result.push({ kind: 'divider', id: 'filter-divider' });
187
+ result.push({
188
+ kind: 'submenu',
189
+ id: 'excludes',
190
+ icon: _jsx(RemoveCircleOutline, {}),
191
+ label: t('hit.panel.exclude'),
192
+ items: (template?.keys ?? []).flatMap(key => {
193
+ let newQuery = '';
194
+ if (query !== DEFAULT_QUERY) {
195
+ newQuery = `(${query}) AND `;
196
+ }
197
+ const value = get(record, key);
198
+ if (!value) {
199
+ return [];
200
+ }
201
+ else if (Array.isArray(value)) {
202
+ const sanitizedValues = value
203
+ .map(toString)
204
+ .filter(val => !!val)
205
+ .map(val => `"${sanitizeLuceneQuery(val)}"`);
206
+ if (sanitizedValues.length < 1) {
207
+ return [];
208
+ }
209
+ newQuery += `-${key}:(${sanitizedValues.join(' OR ')})`;
210
+ }
211
+ else {
212
+ newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
213
+ }
214
+ return [{ key, label: key, onClick: () => setQuery(newQuery) }];
215
+ })
216
+ });
217
+ result.push({
218
+ kind: 'submenu',
219
+ id: 'includes',
220
+ icon: _jsx(AddCircleOutline, {}),
221
+ label: t('hit.panel.include'),
222
+ items: (template?.keys ?? []).flatMap(key => {
223
+ let newQuery = `(${query}) AND `;
224
+ const value = get(record, key);
225
+ if (!value) {
226
+ return [];
227
+ }
228
+ else if (Array.isArray(value)) {
229
+ const sanitizedValues = value
230
+ .map(toString)
231
+ .filter(val => !!val)
232
+ .map(val => `"${sanitizeLuceneQuery(val)}"`);
233
+ if (sanitizedValues.length < 1) {
234
+ return [];
235
+ }
236
+ newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
237
+ }
238
+ else {
239
+ newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
240
+ }
241
+ return [{ key, label: key, onClick: () => setQuery(newQuery) }];
242
+ })
243
+ });
244
+ }
245
+ }
246
+ result.push({ kind: 'divider', id: 'add-to-case-divider' });
247
+ result.push({
248
+ kind: 'item',
249
+ id: 'add-to-case',
250
+ icon: _jsx(CreateNewFolder, {}),
251
+ label: t('modal.cases.add_to_case'),
252
+ disabled: !record,
253
+ onClick: () => showModal(_jsx(AddToCaseModal, { records: records }), { maxHeight: '90vh' })
254
+ });
255
+ result.push({
256
+ kind: 'item',
257
+ id: 'create-case',
258
+ icon: _jsx(NoteAdd, {}),
259
+ label: t('modal.cases.create_case'),
260
+ disabled: !record,
261
+ onClick: () => showModal(_jsx(CreateCaseModal, { records: records }), { maxHeight: '90vh' })
262
+ });
263
+ return result;
264
+ // eslint-disable-next-line react-hooks/exhaustive-deps
265
+ }, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction, showModal, records]);
266
+ return (_jsx(ContextMenu, { id: "contextMenu", Component: Component, onOpen: onOpen, onClose: () => setAnalytic(null), items: items, children: children }));
267
+ };
268
+ export default RecordContextMenu;
@@ -57,6 +57,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
57
57
  getMatchingTemplate: mockGetMatchingTemplate
58
58
  }))
59
59
  }));
60
+ const mockShowModal = vi.fn();
60
61
  const mockDispatchApi = vi.fn();
61
62
  vi.mock('components/hooks/useMyApi', () => ({
62
63
  default: vi.fn(() => ({
@@ -82,6 +83,9 @@ vi.mock('plugins/store', () => ({
82
83
  plugins: ['plugin1']
83
84
  }
84
85
  }));
86
+ vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
87
+ default: () => null
88
+ }));
85
89
  // Mock MUI components
86
90
  vi.mock('@mui/material', async () => {
87
91
  const actual = await vi.importActual('@mui/material');
@@ -101,13 +105,14 @@ vi.mock('@mui/material', async () => {
101
105
  });
102
106
  // Import component after mocks
103
107
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
104
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
108
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
105
109
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
110
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
106
111
  import i18n from '@cccsaurora/howler-ui/i18n';
107
112
  import { I18nextProvider } from 'react-i18next';
108
113
  import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
109
114
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
110
- import HitContextMenu from './HitContextMenu';
115
+ import RecordContextMenu from './RecordContextMenu';
111
116
  const mockGetSelectedId = vi.fn(() => 'test-hit-1');
112
117
  const mockConfig = {
113
118
  lookups: {
@@ -115,16 +120,16 @@ const mockConfig = {
115
120
  }
116
121
  };
117
122
  const mockApiContext = { config: mockConfig };
118
- const mockHitContext = {
119
- hits: {
123
+ const mockRecordContext = {
124
+ records: {
120
125
  'test-hit-1': createMockHit()
121
126
  },
122
- selectedHits: []
127
+ selectedRecords: []
123
128
  };
124
129
  const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
125
130
  // Test wrapper
126
131
  const Wrapper = ({ children }) => {
127
- 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 }) }) }) }) }));
128
133
  };
129
134
  describe('HitContextMenu', () => {
130
135
  let user;
@@ -132,11 +137,11 @@ describe('HitContextMenu', () => {
132
137
  beforeEach(() => {
133
138
  user = userEvent.setup();
134
139
  vi.clearAllMocks();
135
- mockHitContext.selectedHits.length = 0;
136
- mockHitContext.hits['test-hit-1'] = createMockHit();
140
+ mockRecordContext.selectedRecords.length = 0;
141
+ mockRecordContext.records['test-hit-1'] = createMockHit();
137
142
  mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
138
143
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
139
- 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;
140
145
  });
141
146
  describe('Context Menu Initialization', () => {
142
147
  it('should open menu on right-click', async () => {
@@ -200,13 +205,13 @@ describe('HitContextMenu', () => {
200
205
  });
201
206
  it('should disable "Open Hit" when hit is null', async () => {
202
207
  act(() => {
203
- mockHitContext.hits['test-hit-1'] = null;
208
+ mockRecordContext.records['test-hit-1'] = null;
204
209
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
205
210
  fireEvent.contextMenu(contextMenuWrapper);
206
211
  });
207
212
  await waitFor(() => {
208
213
  const menuItems = screen.getAllByRole('menuitem');
209
- const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
214
+ const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
210
215
  expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
211
216
  });
212
217
  });
@@ -247,7 +252,7 @@ describe('HitContextMenu', () => {
247
252
  skip_rationale: false
248
253
  }
249
254
  }));
250
- 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" }) }) }));
251
256
  act(() => {
252
257
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
253
258
  fireEvent.contextMenu(contextMenuWrapper);
@@ -305,7 +310,7 @@ describe('HitContextMenu', () => {
305
310
  createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
306
311
  ];
307
312
  mockDispatchApi.mockResolvedValue({ items: mockActions });
308
- 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" }) }) }));
309
314
  act(() => {
310
315
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
311
316
  fireEvent.contextMenu(contextMenuWrapper);
@@ -349,7 +354,7 @@ describe('HitContextMenu', () => {
349
354
  });
350
355
  it('should disable custom actions menu when no actions are available', async () => {
351
356
  mockDispatchApi.mockResolvedValueOnce({ items: [] });
352
- 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" }) }) }));
353
358
  act(() => {
354
359
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
355
360
  fireEvent.contextMenu(contextMenuWrapper);
@@ -391,7 +396,7 @@ describe('HitContextMenu', () => {
391
396
  skip_rationale: true
392
397
  }
393
398
  }));
394
- 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" }) }) }));
395
400
  act(() => {
396
401
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
397
402
  fireEvent.contextMenu(contextMenuWrapper);
@@ -459,7 +464,7 @@ describe('HitContextMenu', () => {
459
464
  it('should call executeAction with action_id and hit query', async () => {
460
465
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
461
466
  mockDispatchApi.mockResolvedValue({ items: mockActions });
462
- 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" }) }) }));
463
468
  act(() => {
464
469
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
465
470
  fireEvent.contextMenu(contextMenuWrapper);
@@ -512,7 +517,7 @@ describe('HitContextMenu', () => {
512
517
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
513
518
  keys: ['howler.detection', 'event.id']
514
519
  }));
515
- 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" }) }) }));
516
521
  });
517
522
  it('should render exclusion submenu with template keys', async () => {
518
523
  act(() => {
@@ -560,7 +565,7 @@ describe('HitContextMenu', () => {
560
565
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
561
566
  keys: ['howler.outline.indicators']
562
567
  }));
563
- 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" }) }) }));
564
569
  act(() => {
565
570
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
566
571
  fireEvent.contextMenu(contextMenuWrapper);
@@ -603,7 +608,7 @@ describe('HitContextMenu', () => {
603
608
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
604
609
  keys: []
605
610
  }));
606
- 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" }) }) }));
607
612
  act(() => {
608
613
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
609
614
  fireEvent.contextMenu(contextMenuWrapper);
@@ -615,7 +620,7 @@ describe('HitContextMenu', () => {
615
620
  });
616
621
  it('should skip null field values in exclusion menu', async () => {
617
622
  act(() => {
618
- mockHitContext.hits['test-hit-1'].event = {};
623
+ mockRecordContext.records['test-hit-1'].event = {};
619
624
  });
620
625
  act(() => {
621
626
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -638,7 +643,7 @@ describe('HitContextMenu', () => {
638
643
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
639
644
  keys: ['howler.detection', 'event.id']
640
645
  }));
641
- 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" }) }) }));
642
647
  });
643
648
  it('should render inclusion submenu with template keys', async () => {
644
649
  act(() => {
@@ -686,7 +691,7 @@ describe('HitContextMenu', () => {
686
691
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
687
692
  keys: ['howler.outline.indicators']
688
693
  }));
689
- 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" }) }) }));
690
695
  act(() => {
691
696
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
692
697
  fireEvent.contextMenu(contextMenuWrapper);
@@ -729,7 +734,7 @@ describe('HitContextMenu', () => {
729
734
  mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
730
735
  keys: []
731
736
  }));
732
- 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" }) }) }));
733
738
  act(() => {
734
739
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
735
740
  fireEvent.contextMenu(contextMenuWrapper);
@@ -741,7 +746,7 @@ describe('HitContextMenu', () => {
741
746
  });
742
747
  it('should skip null field values in inclusion menu', async () => {
743
748
  act(() => {
744
- mockHitContext.hits['test-hit-1'].event = {};
749
+ mockRecordContext.records['test-hit-1'].event = {};
745
750
  });
746
751
  act(() => {
747
752
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -760,24 +765,24 @@ describe('HitContextMenu', () => {
760
765
  });
761
766
  });
762
767
  describe('Multiple Hit Selection', () => {
763
- it('should use selectedHits when current hit is included', async () => {
768
+ it('should use selectedRecords when current hit is included', async () => {
764
769
  act(() => {
765
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
766
- mockHitContext.hits['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
767
- 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']);
768
773
  mockGetSelectedId.mockReturnValue('hit-1');
769
774
  });
770
775
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
771
776
  await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
772
- // The component should use selectedHits for actions
777
+ // The component should use selectedRecords for actions
773
778
  // We can verify this indirectly through the useHitActions hook receiving the right data
774
779
  expect(screen.getByRole('menu')).toBeInTheDocument();
775
780
  expect(mockGetSelectedId).toHaveBeenCalled();
776
781
  });
777
- it('should use only current hit when not in selectedHits', async () => {
782
+ it('should use only current hit when not in selectedRecords', async () => {
778
783
  act(() => {
779
- mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
780
- 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']);
781
786
  mockGetSelectedId.mockReturnValue('test-hit-1');
782
787
  });
783
788
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
@@ -796,12 +801,12 @@ describe('HitContextMenu', () => {
796
801
  });
797
802
  it('should call getMatchingAnalytic when hit has analytic', async () => {
798
803
  await waitFor(() => {
799
- expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
804
+ expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
800
805
  });
801
806
  });
802
807
  it('should call getMatchingTemplate when menu opens', async () => {
803
808
  await waitFor(() => {
804
- expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
809
+ expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
805
810
  });
806
811
  });
807
812
  it('should reset state when menu closes', async () => {
@@ -834,7 +839,7 @@ describe('HitContextMenu', () => {
834
839
  describe('Edge Cases and Error Handling', () => {
835
840
  it('should not crash when hit is null', async () => {
836
841
  act(() => {
837
- mockHitContext.hits = {};
842
+ mockRecordContext.records = {};
838
843
  });
839
844
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
840
845
  fireEvent.contextMenu(contextMenuWrapper);
@@ -845,7 +850,7 @@ describe('HitContextMenu', () => {
845
850
  });
846
851
  it('should not render exclusion menu when template is null', async () => {
847
852
  mockGetMatchingTemplate.mockResolvedValue(null);
848
- 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" }) }) }));
849
854
  act(() => {
850
855
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
851
856
  fireEvent.contextMenu(contextMenuWrapper);
@@ -859,7 +864,7 @@ describe('HitContextMenu', () => {
859
864
  });
860
865
  it('should not render inclusion menu when template is null', async () => {
861
866
  mockGetMatchingTemplate.mockResolvedValue(null);
862
- 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" }) }) }));
863
868
  act(() => {
864
869
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
865
870
  fireEvent.contextMenu(contextMenuWrapper);
@@ -873,7 +878,7 @@ describe('HitContextMenu', () => {
873
878
  });
874
879
  it('should handle API failure gracefully', async () => {
875
880
  mockDispatchApi.mockResolvedValue(null);
876
- 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" }) }) }));
877
882
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
878
883
  fireEvent.contextMenu(contextMenuWrapper);
879
884
  await waitFor(() => {
@@ -884,7 +889,7 @@ describe('HitContextMenu', () => {
884
889
  });
885
890
  it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
886
891
  act(() => {
887
- mockHitContext.hits['test-hit-1'].howler.analytic = null;
892
+ mockRecordContext.records['test-hit-1'].howler.analytic = null;
888
893
  });
889
894
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
890
895
  fireEvent.contextMenu(contextMenuWrapper);
@@ -903,6 +908,56 @@ describe('HitContextMenu', () => {
903
908
  expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
904
909
  });
905
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
+ });
906
961
  describe('Role-Based Action Permissions', () => {
907
962
  afterEach(() => {
908
963
  // Reset to default user with required roles
@@ -922,7 +977,7 @@ describe('HitContextMenu', () => {
922
977
  });
923
978
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
924
979
  mockDispatchApi.mockResolvedValue({ items: mockActions });
925
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
980
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
926
981
  act(() => {
927
982
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
928
983
  fireEvent.contextMenu(contextMenuWrapper);
@@ -941,7 +996,7 @@ describe('HitContextMenu', () => {
941
996
  });
942
997
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
943
998
  mockDispatchApi.mockResolvedValue({ items: mockActions });
944
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
999
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
945
1000
  act(() => {
946
1001
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
947
1002
  fireEvent.contextMenu(contextMenuWrapper);
@@ -960,7 +1015,7 @@ describe('HitContextMenu', () => {
960
1015
  });
961
1016
  const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
962
1017
  mockDispatchApi.mockResolvedValue({ items: mockActions });
963
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1018
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
964
1019
  act(() => {
965
1020
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
966
1021
  fireEvent.contextMenu(contextMenuWrapper);
@@ -978,7 +1033,7 @@ describe('HitContextMenu', () => {
978
1033
  }
979
1034
  });
980
1035
  mockDispatchApi.mockResolvedValue({ items: [] });
981
- rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1036
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
982
1037
  act(() => {
983
1038
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
984
1039
  fireEvent.contextMenu(contextMenuWrapper);