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

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 (468) hide show
  1. package/api/action/index.d.ts +5 -4
  2. package/api/action/index.js +8 -8
  3. package/api/analytic/index.d.ts +3 -2
  4. package/api/analytic/index.js +4 -4
  5. package/api/analytic/owner.d.ts +2 -1
  6. package/api/analytic/owner.js +2 -2
  7. package/api/analytic/rules.d.ts +2 -1
  8. package/api/analytic/rules.js +2 -2
  9. package/api/dossier/index.d.ts +4 -3
  10. package/api/dossier/index.js +6 -6
  11. package/api/hit/index.d.ts +3 -2
  12. package/api/hit/index.js +4 -4
  13. package/api/hit/labels.d.ts +3 -2
  14. package/api/hit/labels.js +4 -4
  15. package/api/hit/overwrite.d.ts +2 -1
  16. package/api/hit/overwrite.js +2 -2
  17. package/api/hit/transition.d.ts +2 -1
  18. package/api/hit/transition.js +2 -2
  19. package/api/index.d.ts +9 -4
  20. package/api/index.js +18 -10
  21. package/api/overview/index.d.ts +4 -3
  22. package/api/overview/index.js +6 -6
  23. package/api/search/case.d.ts +4 -0
  24. package/api/search/case.js +8 -0
  25. package/api/search/facet/hit.d.ts +1 -3
  26. package/api/search/facet/index.d.ts +3 -1
  27. package/api/search/index.d.ts +2 -1
  28. package/api/search/index.js +2 -1
  29. package/api/search/overview.d.ts +2 -2
  30. package/api/socket/index.d.ts +3 -0
  31. package/api/socket/index.js +6 -0
  32. package/api/socket/viewers.d.ts +2 -0
  33. package/api/socket/viewers.js +8 -0
  34. package/api/socket/viewers.test.js +44 -0
  35. package/api/template/index.d.ts +4 -3
  36. package/api/template/index.js +6 -6
  37. package/api/user/index.d.ts +2 -1
  38. package/api/user/index.js +2 -2
  39. package/api/v2/case/index.d.ts +9 -0
  40. package/api/v2/case/index.js +21 -0
  41. package/api/v2/case/items.d.ts +6 -0
  42. package/api/v2/case/items.js +18 -0
  43. package/api/v2/case/rules.d.ts +6 -0
  44. package/api/v2/case/rules.js +18 -0
  45. package/api/v2/index.d.ts +4 -0
  46. package/api/v2/index.js +6 -0
  47. package/api/v2/search/facet.d.ts +3 -0
  48. package/api/v2/search/facet.js +12 -0
  49. package/api/v2/search/index.d.ts +5 -0
  50. package/api/v2/search/index.js +24 -0
  51. package/api/view/index.d.ts +4 -3
  52. package/api/view/index.js +6 -6
  53. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  54. package/components/app/App.d.ts +1 -0
  55. package/components/app/App.js +64 -13
  56. package/components/app/hooks/useMatchers.d.ts +1 -1
  57. package/components/app/hooks/useMatchers.js +23 -11
  58. package/components/app/hooks/useMatchers.test.js +22 -22
  59. package/components/app/hooks/useTitle.js +5 -5
  60. package/components/app/providers/FavouritesProvider.js +2 -6
  61. package/components/app/providers/ModalProvider.d.ts +2 -0
  62. package/components/app/providers/ModalProvider.js +5 -1
  63. package/components/app/providers/ParameterProvider.d.ts +9 -2
  64. package/components/app/providers/ParameterProvider.js +165 -240
  65. package/components/app/providers/ParameterProvider.test.js +346 -94
  66. package/components/app/providers/RecordProvider.d.ts +23 -0
  67. package/components/app/providers/{HitProvider.js → RecordProvider.js} +42 -42
  68. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  69. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  70. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
  71. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  72. package/components/app/providers/SearchResponseProvider.js +81 -0
  73. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  74. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  75. package/components/app/providers/SocketProvider.d.ts +11 -2
  76. package/components/app/providers/SocketProvider.js +18 -5
  77. package/components/app/providers/UserListProvider.js +28 -8
  78. package/components/app/providers/ViewProvider.test.js +4 -1
  79. package/components/elements/ContextMenu.d.ts +56 -0
  80. package/components/elements/ContextMenu.js +109 -0
  81. package/components/elements/ContextMenu.test.d.ts +1 -0
  82. package/components/elements/ContextMenu.test.js +215 -0
  83. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  84. package/components/elements/ObjectDetails.d.ts +6 -0
  85. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  86. package/components/elements/PluginTypography.d.ts +2 -1
  87. package/components/elements/PluginTypography.js +3 -2
  88. package/components/elements/UserList.d.ts +5 -2
  89. package/components/elements/UserList.js +18 -8
  90. package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
  91. package/components/elements/addons/buttons/CustomButton.test.js +106 -0
  92. package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
  93. package/components/elements/addons/layout/FlexOne.test.js +30 -0
  94. package/components/elements/addons/lists/TuiListBase.js +4 -0
  95. package/components/elements/addons/lists/TuiListElement.js +6 -3
  96. package/components/elements/addons/lists/index.d.ts +1 -0
  97. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  98. package/components/elements/addons/search/SearchPagination.js +4 -4
  99. package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
  100. package/components/elements/addons/search/SearchPagination.test.js +80 -0
  101. package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
  102. package/components/elements/addons/search/SearchTotal.test.js +47 -0
  103. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  104. package/components/elements/case/CaseCard.d.ts +12 -0
  105. package/components/elements/case/CaseCard.js +42 -0
  106. package/components/elements/case/CasePreview.d.ts +6 -0
  107. package/components/elements/case/CasePreview.js +17 -0
  108. package/components/elements/case/StatusIcon.d.ts +5 -0
  109. package/components/elements/case/StatusIcon.js +13 -0
  110. package/components/elements/display/ChipPopper.d.ts +1 -1
  111. package/components/elements/display/ChipPopper.js +6 -5
  112. package/components/elements/display/DocumentationButton.test.d.ts +1 -0
  113. package/components/elements/display/DocumentationButton.test.js +84 -0
  114. package/components/elements/display/DynamicTabs.test.d.ts +1 -0
  115. package/components/elements/display/DynamicTabs.test.js +93 -0
  116. package/components/elements/display/HowlerAvatar.js +2 -1
  117. package/components/elements/display/HowlerCard.js +1 -1
  118. package/components/elements/display/HowlerCard.test.d.ts +1 -0
  119. package/components/elements/display/HowlerCard.test.js +51 -0
  120. package/components/elements/display/Image.test.d.ts +1 -0
  121. package/components/elements/display/Image.test.js +79 -0
  122. package/components/elements/display/ItemManager.d.ts +2 -2
  123. package/components/elements/display/ItemManager.js +1 -1
  124. package/components/elements/display/Markdown.js +9 -2
  125. package/components/elements/display/Modal.js +2 -0
  126. package/components/elements/display/QueryResultText.test.d.ts +1 -0
  127. package/components/elements/display/QueryResultText.test.js +48 -0
  128. package/components/elements/display/TextDivider.test.d.ts +1 -0
  129. package/components/elements/display/TextDivider.test.js +43 -0
  130. package/components/elements/display/TypingIndicator.test.d.ts +1 -0
  131. package/components/elements/display/TypingIndicator.test.js +33 -0
  132. package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
  133. package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
  134. package/components/elements/hit/HitActions.js +4 -4
  135. package/components/elements/hit/HitBanner.d.ts +1 -0
  136. package/components/elements/hit/HitBanner.js +34 -51
  137. package/components/elements/hit/HitCard.d.ts +2 -0
  138. package/components/elements/hit/HitCard.js +7 -7
  139. package/components/elements/hit/HitLabels.js +2 -2
  140. package/components/elements/hit/HitOutline.d.ts +1 -0
  141. package/components/elements/hit/HitOutline.js +3 -3
  142. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  143. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  144. package/components/elements/hit/HitSummary.d.ts +2 -1
  145. package/components/elements/hit/HitSummary.js +9 -7
  146. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  147. package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
  148. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  149. package/components/elements/hit/elements/Assigned.d.ts +2 -0
  150. package/components/elements/hit/elements/Assigned.js +21 -16
  151. package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
  152. package/components/elements/hit/elements/Assigned.test.js +65 -0
  153. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  154. package/components/elements/hit/related/RelatedRecords.js +63 -0
  155. package/components/elements/observable/ObservableCard.d.ts +6 -0
  156. package/components/elements/observable/ObservableCard.js +22 -0
  157. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  158. package/components/elements/observable/ObservablePreview.js +12 -0
  159. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  160. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  161. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  162. package/components/elements/record/RecordContextMenu.js +268 -0
  163. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  164. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +190 -39
  165. package/components/elements/record/RecordRelated.d.ts +7 -0
  166. package/components/elements/record/RecordRelated.js +34 -0
  167. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  168. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  169. package/components/elements/view/ViewTitle.d.ts +1 -0
  170. package/components/elements/view/ViewTitle.js +9 -2
  171. package/components/hooks/useHitActions.d.ts +1 -1
  172. package/components/hooks/useHitActions.js +4 -4
  173. package/components/hooks/useMyApi.d.ts +1 -1
  174. package/components/hooks/useMyPreferences.js +12 -2
  175. package/components/hooks/useMySearch.js +2 -2
  176. package/components/hooks/useMySitemap.js +4 -1
  177. package/components/hooks/useMyTheme.js +9 -2
  178. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  179. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  180. package/components/hooks/useRelatedRecords.d.ts +13 -0
  181. package/components/hooks/useRelatedRecords.js +32 -0
  182. package/components/routes/403.d.ts +3 -0
  183. package/components/routes/403.js +10 -0
  184. package/components/routes/403.test.d.ts +1 -0
  185. package/components/routes/403.test.js +27 -0
  186. package/components/routes/404.test.d.ts +1 -0
  187. package/components/routes/404.test.js +26 -0
  188. package/components/routes/ErrorBoundary.test.d.ts +1 -0
  189. package/components/routes/ErrorBoundary.test.js +43 -0
  190. package/components/routes/ErrorOccured.test.d.ts +1 -0
  191. package/components/routes/ErrorOccured.test.js +35 -0
  192. package/components/routes/Logout.test.d.ts +1 -0
  193. package/components/routes/Logout.test.js +59 -0
  194. package/components/routes/action/edit/ActionEditor.js +3 -3
  195. package/components/routes/action/useMyActionFunctions.js +9 -3
  196. package/components/routes/action/view/ActionDetails.js +15 -2
  197. package/components/routes/action/view/ActionSearch.js +32 -20
  198. package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
  199. package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
  200. package/components/routes/advanced/QueryBuilder.js +1 -1
  201. package/components/routes/advanced/QueryEditor.js +3 -3
  202. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  203. package/components/routes/analytics/AnalyticDetails.js +12 -8
  204. package/components/routes/analytics/AnalyticSearch.js +7 -7
  205. package/components/routes/cases/CaseViewer.d.ts +2 -0
  206. package/components/routes/cases/CaseViewer.js +44 -0
  207. package/components/routes/cases/CaseViewer.test.d.ts +1 -0
  208. package/components/routes/cases/CaseViewer.test.js +133 -0
  209. package/components/routes/cases/Cases.d.ts +2 -0
  210. package/components/routes/cases/Cases.js +147 -0
  211. package/components/routes/cases/constants.d.ts +6 -0
  212. package/components/routes/cases/constants.js +6 -0
  213. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  214. package/components/routes/cases/detail/AlertPanel.js +33 -0
  215. package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
  216. package/components/routes/cases/detail/CaseAssets.js +104 -0
  217. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  218. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  219. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  220. package/components/routes/cases/detail/CaseDashboard.js +66 -0
  221. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  222. package/components/routes/cases/detail/CaseDetails.js +70 -0
  223. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  224. package/components/routes/cases/detail/CaseOverview.js +43 -0
  225. package/components/routes/cases/detail/CaseRules.d.ts +7 -0
  226. package/components/routes/cases/detail/CaseRules.js +57 -0
  227. package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
  228. package/components/routes/cases/detail/CaseRules.test.js +221 -0
  229. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  230. package/components/routes/cases/detail/CaseSidebar.js +107 -0
  231. package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
  232. package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
  233. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  234. package/components/routes/cases/detail/CaseTask.js +66 -0
  235. package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
  236. package/components/routes/cases/detail/CaseTimeline.js +106 -0
  237. package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
  238. package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
  239. package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
  240. package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
  241. package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
  242. package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
  243. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  244. package/components/routes/cases/detail/ItemPage.js +95 -0
  245. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  246. package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
  247. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  248. package/components/routes/cases/detail/TaskPanel.js +52 -0
  249. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
  250. package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
  251. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  252. package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
  253. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  254. package/components/routes/cases/detail/assets/Asset.js +12 -0
  255. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  256. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  257. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
  258. package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
  259. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
  260. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
  261. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  262. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
  263. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  264. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
  265. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
  266. package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
  267. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
  268. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
  269. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
  270. package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
  271. package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
  272. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  273. package/components/routes/cases/detail/sidebar/utils.js +29 -0
  274. package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
  275. package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
  276. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  277. package/components/routes/cases/hooks/useCase.js +69 -0
  278. package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
  279. package/components/routes/cases/hooks/useCase.test.js +141 -0
  280. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  281. package/components/routes/cases/modals/AddToCaseModal.js +59 -0
  282. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
  283. package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
  284. package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
  285. package/components/routes/cases/modals/CaseRecordRow.js +15 -0
  286. package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
  287. package/components/routes/cases/modals/CreateCaseModal.js +55 -0
  288. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
  289. package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
  290. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  291. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  292. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  293. package/components/routes/cases/modals/ResolveModal.js +116 -0
  294. package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
  295. package/components/routes/cases/modals/ResolveModal.test.js +454 -0
  296. package/components/routes/cases/modals/hooks.d.ts +7 -0
  297. package/components/routes/cases/modals/hooks.js +44 -0
  298. package/components/routes/cases/modals/types.d.ts +5 -0
  299. package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
  300. package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
  301. package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
  302. package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
  303. package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
  304. package/components/routes/cases/search/CaseDateFilter.js +26 -0
  305. package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
  306. package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
  307. package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
  308. package/components/routes/cases/search/CaseStatusFilter.js +13 -0
  309. package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
  310. package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
  311. package/components/routes/dossiers/DossierEditor.js +2 -2
  312. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  313. package/components/routes/dossiers/Dossiers.js +25 -19
  314. package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
  315. package/components/routes/help/ApiDocumentation.js +1 -1
  316. package/components/routes/help/HitBannerDocumentation.js +1 -0
  317. package/components/routes/help/HitDocumentation.js +1 -3
  318. package/components/routes/help/markdown/en/client.md.js +1 -1
  319. package/components/routes/help/markdown/en/retention.md.js +1 -1
  320. package/components/routes/help/markdown/fr/client.md.js +1 -1
  321. package/components/routes/help/markdown/fr/retention.md.js +1 -1
  322. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  323. package/components/routes/hits/search/InformationPane.js +50 -63
  324. package/components/routes/hits/search/LayoutSettings.js +3 -3
  325. package/components/routes/hits/search/QuerySettings.js +2 -1
  326. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  327. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  328. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  329. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  330. package/components/routes/hits/search/SearchPane.js +26 -49
  331. package/components/routes/hits/search/ViewLink.js +3 -3
  332. package/components/routes/hits/search/ViewLink.test.js +8 -8
  333. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  334. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  335. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  336. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  337. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  338. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  339. package/components/routes/hits/search/shared/CustomSpan.js +7 -7
  340. package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
  341. package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
  342. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  343. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  344. package/components/routes/hits/view/HitViewer.js +12 -13
  345. package/components/routes/home/AddNewCard.js +1 -1
  346. package/components/routes/home/AddNewCard.test.d.ts +1 -0
  347. package/components/routes/home/AddNewCard.test.js +86 -0
  348. package/components/routes/home/ViewCard.js +47 -41
  349. package/components/routes/home/ViewRefresh.test.d.ts +1 -0
  350. package/components/routes/home/ViewRefresh.test.js +79 -0
  351. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  352. package/components/routes/observables/ObservableViewer.js +27 -0
  353. package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
  354. package/components/routes/overviews/OverviewCard.test.js +108 -0
  355. package/components/routes/overviews/OverviewViewer.js +18 -10
  356. package/components/routes/overviews/Overviews.js +25 -19
  357. package/components/routes/overviews/template/en.md.js +1 -1
  358. package/components/routes/overviews/template/fr.md.js +1 -1
  359. package/components/routes/settings/LocalSection.js +2 -1
  360. package/components/routes/settings/SettingsSection.test.d.ts +1 -0
  361. package/components/routes/settings/SettingsSection.test.js +19 -0
  362. package/components/routes/templates/TemplateCard.d.ts +3 -1
  363. package/components/routes/templates/TemplateCard.js +8 -4
  364. package/components/routes/templates/TemplateCard.test.d.ts +1 -0
  365. package/components/routes/templates/TemplateCard.test.js +115 -0
  366. package/components/routes/templates/TemplateViewer.js +62 -16
  367. package/components/routes/templates/Templates.js +35 -13
  368. package/components/routes/views/ViewComposer.js +46 -19
  369. package/components/routes/views/Views.js +21 -14
  370. package/locales/en/translation.json +146 -8
  371. package/locales/fr/translation.json +144 -8
  372. package/models/WithMetadata.d.ts +2 -1
  373. package/models/entities/generated/ApiType.d.ts +1 -1
  374. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  375. package/models/entities/generated/Case.d.ts +28 -0
  376. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  377. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  378. package/models/entities/generated/EmailParent.d.ts +19 -0
  379. package/models/entities/generated/Enrichments.d.ts +7 -0
  380. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  381. package/models/entities/generated/Hit.d.ts +2 -0
  382. package/models/entities/generated/Howler.d.ts +1 -5
  383. package/models/entities/generated/HttpResponse.d.ts +11 -0
  384. package/models/entities/generated/Item.d.ts +9 -0
  385. package/models/entities/generated/Observable.d.ts +85 -0
  386. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  387. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  388. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  389. package/models/entities/generated/ObservableFile.d.ts +36 -0
  390. package/models/entities/generated/ObservableHowler.d.ts +42 -0
  391. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  392. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  393. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  394. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  395. package/models/entities/generated/ObservableSource.d.ts +23 -0
  396. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  397. package/models/entities/generated/ObservableTls.d.ts +12 -0
  398. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  399. package/models/entities/generated/Rule.d.ts +6 -9
  400. package/models/entities/generated/Task.d.ts +10 -0
  401. package/models/entities/generated/Threat.d.ts +2 -2
  402. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  403. package/models/entities/generated/UserUser.d.ts +2 -0
  404. package/models/entities/generated/View.d.ts +1 -0
  405. package/models/socket/CaseUpdate.d.ts +5 -0
  406. package/models/socket/ViewersUpdate.d.ts +4 -0
  407. package/package.json +147 -132
  408. package/plugins/clue/components/ClueTypography.js +2 -2
  409. package/plugins/clue/utils.d.ts +2 -1
  410. package/rest/FetchClient.test.d.ts +1 -0
  411. package/rest/FetchClient.test.js +81 -0
  412. package/setupTests.js +5 -0
  413. package/tests/mocks.d.ts +11 -1
  414. package/tests/mocks.js +12 -7
  415. package/tests/server-handlers.js +6 -1
  416. package/tests/utils.d.ts +4 -0
  417. package/tests/utils.js +20 -0
  418. package/utils/Throttler.test.d.ts +1 -0
  419. package/utils/Throttler.test.js +135 -0
  420. package/utils/actionUtils.test.d.ts +1 -0
  421. package/utils/actionUtils.test.js +140 -0
  422. package/utils/constants.d.ts +5 -3
  423. package/utils/constants.js +7 -0
  424. package/utils/hitFunctions.d.ts +2 -1
  425. package/utils/hitFunctions.js +4 -4
  426. package/utils/hitFunctions.test.d.ts +1 -0
  427. package/utils/hitFunctions.test.js +52 -0
  428. package/utils/localStorage.test.d.ts +1 -0
  429. package/utils/localStorage.test.js +96 -0
  430. package/utils/menuUtils.js +1 -1
  431. package/utils/menuUtils.test.d.ts +1 -0
  432. package/utils/menuUtils.test.js +198 -0
  433. package/utils/sessionStorage.test.d.ts +1 -0
  434. package/utils/sessionStorage.test.js +81 -0
  435. package/utils/socketUtils.d.ts +14 -0
  436. package/utils/socketUtils.js +17 -1
  437. package/utils/socketUtils.test.d.ts +1 -0
  438. package/utils/socketUtils.test.js +59 -0
  439. package/utils/stringUtils.test.d.ts +1 -0
  440. package/utils/stringUtils.test.js +159 -0
  441. package/utils/typeUtils.d.ts +7 -0
  442. package/utils/typeUtils.js +27 -0
  443. package/utils/utils.test.d.ts +1 -0
  444. package/utils/utils.test.js +292 -0
  445. package/utils/viewUtils.js +3 -0
  446. package/utils/viewUtils.test.d.ts +1 -0
  447. package/utils/viewUtils.test.js +45 -0
  448. package/utils/xsrf.test.d.ts +1 -0
  449. package/utils/xsrf.test.js +41 -0
  450. package/components/app/providers/HitProvider.d.ts +0 -22
  451. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  452. package/components/elements/display/icons/BundleButton.js +0 -32
  453. package/components/elements/hit/HitRelated.d.ts +0 -6
  454. package/components/elements/hit/HitRelated.js +0 -7
  455. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  456. package/components/routes/help/BundleDocumentation.js +0 -12
  457. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  458. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  459. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  460. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  461. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  462. package/components/routes/hits/search/BundleScroller.js +0 -6
  463. package/components/routes/hits/search/HitContextMenu.js +0 -229
  464. /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
  465. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
  466. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  467. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  468. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -0,0 +1,20 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import { type FC } from 'react';
3
+ import type { Tree } from './types';
4
+ interface CaseFolderProps {
5
+ case: Case;
6
+ folder?: Tree;
7
+ name?: string;
8
+ step?: number;
9
+ /**
10
+ * The chain of `leaf.path` values for each case item traversed from the root
11
+ * case to reach this nested case. Empty at the top level.
12
+ *
13
+ * Example: case1 → case2 (path "cases/caseone") → case3 (path "cases/casetwo")
14
+ * gives parentCasePaths = ['cases/caseone', 'cases/casetwo'] inside case3.
15
+ */
16
+ parentCasePaths?: string[];
17
+ onItemUpdated?: (newCase: Case) => void;
18
+ }
19
+ declare const CaseFolder: FC<CaseFolderProps>;
20
+ export default CaseFolder;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Skeleton, Stack, useTheme } from '@mui/material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
5
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
+ import { useCallback, useMemo, useState } from 'react';
7
+ import { useParams } from 'react-router-dom';
8
+ import { useContextSelector } from 'use-context-selector';
9
+ import { ESCALATION_COLORS } from '@cccsaurora/howler-ui/utils/constants';
10
+ import CaseFolderContextMenu from './CaseFolderContextMenu';
11
+ import FolderEntry from './FolderEntry';
12
+ import { buildTree } from './utils';
13
+ const CaseFolder = ({ case: _case, folder, name, step = -1, parentCasePaths = [], onItemUpdated }) => {
14
+ const theme = useTheme();
15
+ const { dispatchApi } = useMyApi();
16
+ const params = useParams();
17
+ const [open, setOpen] = useState(true);
18
+ const [caseStates, setCaseStates] = useState({});
19
+ const records = useContextSelector(RecordContext, ctx => ctx.records);
20
+ const tree = useMemo(() => folder || buildTree(_case?.items), [folder, _case?.items]);
21
+ const rootCaseId = params.id;
22
+ // Returns the MUI colour token for the item's escalation, or undefined if none.
23
+ const getEscalationColor = (itemType, itemKey, leafId) => {
24
+ if (itemType === 'hit' && leafId) {
25
+ const color = ESCALATION_COLORS[records[leafId]?.howler?.escalation];
26
+ if (color)
27
+ return color;
28
+ }
29
+ if (itemType === 'case' && itemKey) {
30
+ const color = ESCALATION_COLORS[caseStates[itemKey]?.data?.escalation];
31
+ if (color)
32
+ return color;
33
+ }
34
+ return undefined;
35
+ };
36
+ const toggleCase = useCallback((item, itemKey) => {
37
+ const resolvedKey = itemKey || item.path || item.value;
38
+ if (!resolvedKey) {
39
+ return;
40
+ }
41
+ const prev = caseStates[resolvedKey] ?? { open: false, loading: false, data: null };
42
+ const shouldOpen = !prev.open;
43
+ const shouldFetch = shouldOpen && !!item.value && !prev.data && !prev.loading;
44
+ setCaseStates(current => ({ ...current, [resolvedKey]: { ...prev, open: shouldOpen, loading: shouldFetch } }));
45
+ if (!shouldFetch)
46
+ return;
47
+ dispatchApi(api.v2.case.get(item.value), { throwError: false })
48
+ .then(caseResponse => {
49
+ if (!caseResponse)
50
+ return;
51
+ setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], data: caseResponse } }));
52
+ })
53
+ .finally(() => {
54
+ setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], loading: false } }));
55
+ });
56
+ }, [caseStates, dispatchApi]);
57
+ return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsx(CaseFolderContextMenu, { _case: _case, tree: tree, onUpdate: onItemUpdated, children: _jsx(Box, { sx: {
58
+ transition: theme.transitions.create('background', { duration: 100 }),
59
+ background: 'transparent',
60
+ '&:hover': { background: theme.palette.grey[800] }
61
+ }, children: _jsx(FolderEntry, { caseId: _case.case_id === rootCaseId ? rootCaseId : null, path: tree.path, itemType: "folder", indent: step * 1.5, label: name, chevronOpen: open, onClick: () => setOpen(_open => !_open), entry: tree }) }) })), open && (_jsxs(_Fragment, { children: [Object.entries(tree.folders ?? {}).map(([path, subfolder]) => {
62
+ return (_jsx(CaseFolder, { name: path, case: _case, folder: subfolder, step: step + 1, parentCasePaths: parentCasePaths, onItemUpdated: onItemUpdated }, `${_case?.case_id}-${path}`));
63
+ }), tree.leaves?.map(leaf => {
64
+ const itemType = leaf.type?.toLowerCase();
65
+ const isCase = itemType === 'case';
66
+ const itemKey = leaf.path || leaf.value;
67
+ const nodeState = itemKey ? caseStates[itemKey] : null;
68
+ const isCaseOpen = !!nodeState?.open;
69
+ const isCaseLoading = !!nodeState?.loading;
70
+ const nestedCase = nodeState?.data ?? null;
71
+ const fullItemPath = [...parentCasePaths, leaf.path].filter(Boolean).join('/');
72
+ const itemTo = itemType !== 'reference' ? `/cases/${rootCaseId}${fullItemPath ? `/${fullItemPath}` : ''}` : leaf.value;
73
+ const escalationColor = getEscalationColor(itemType, itemKey, leaf.value);
74
+ const iconColor = escalationColor ?? 'inherit';
75
+ const leafColor = escalationColor ? `${escalationColor}.light` : 'text.secondary';
76
+ return (_jsx(CaseFolderContextMenu, { _case: _case, leaf: leaf, onUpdate: onItemUpdated, children: _jsxs(Stack, { children: [_jsx(Box, { sx: {
77
+ transition: theme.transitions.create('background', { duration: 100 }),
78
+ background: 'transparent',
79
+ '&:hover': { background: theme.palette.grey[800] }
80
+ }, children: _jsx(FolderEntry, { caseId: _case.case_id === rootCaseId ? rootCaseId : null, path: leaf.path, indent: step * 1.5 + 1, label: leaf.path?.split('/').pop() || leaf.value || '', itemType: itemType, iconColor: iconColor, labelColor: leafColor, chevronOpen: isCaseOpen, to: itemTo, onClick: () => isCase && toggleCase(leaf, itemKey), entry: leaf }) }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, parentCasePaths: [...parentCasePaths, leaf.path].filter(Boolean), onItemUpdated: onItemUpdated }))] }) }, `${_case?.case_id}-${leaf.value}-${leaf.path}`));
81
+ })] }))] }));
82
+ };
83
+ export default CaseFolder;
@@ -0,0 +1,295 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { act, createContext } from 'react';
5
+ import { setupContextSelectorMock, setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ // ---------------------------------------------------------------------------
8
+ // Mocks
9
+ // ---------------------------------------------------------------------------
10
+ setupContextSelectorMock();
11
+ setupReactRouterMock();
12
+ vi.mock('@dnd-kit/core', () => ({
13
+ useDraggable: () => ({
14
+ attributes: {},
15
+ listeners: {},
16
+ setNodeRef: vi.fn(),
17
+ transform: null,
18
+ isDragging: false,
19
+ active: null
20
+ }),
21
+ useDroppable: () => ({
22
+ setNodeRef: vi.fn(),
23
+ isOver: false
24
+ })
25
+ }));
26
+ vi.mock('@dnd-kit/utilities', () => ({
27
+ CSS: { Transform: { toString: () => '' } }
28
+ }));
29
+ const mockDispatchApi = vi.hoisted(() => vi.fn());
30
+ vi.mock('components/hooks/useMyApi', () => ({
31
+ default: () => ({ dispatchApi: mockDispatchApi })
32
+ }));
33
+ const mockGetCase = vi.hoisted(() => vi.fn());
34
+ vi.mock('api', () => ({
35
+ default: {
36
+ v2: {
37
+ case: {
38
+ get: (...args) => mockGetCase(...args),
39
+ items: { del: vi.fn(), patch: vi.fn() }
40
+ }
41
+ }
42
+ }
43
+ }));
44
+ // CaseFolderContextMenu — render children only; ignore menu entries for these tests
45
+ vi.mock('./CaseFolderContextMenu', () => ({
46
+ default: ({ children }) => _jsx("div", { children: children })
47
+ }));
48
+ vi.mock('components/app/providers/ModalProvider', async () => {
49
+ return {
50
+ ModalContext: createContext({ showModal: vi.fn(), close: vi.fn(), setContent: vi.fn() })
51
+ };
52
+ });
53
+ // RecordContext — supply a controllable records map
54
+ const mockRecords = vi.hoisted(() => ({ current: {} }));
55
+ vi.mock('components/app/providers/RecordProvider', async () => {
56
+ return {
57
+ RecordContext: createContext({ records: mockRecords.current })
58
+ };
59
+ });
60
+ // ---------------------------------------------------------------------------
61
+ // Imports after mocks
62
+ // ---------------------------------------------------------------------------
63
+ import { MemoryRouter, useParams } from 'react-router-dom';
64
+ import CaseFolder from './CaseFolder';
65
+ // ---------------------------------------------------------------------------
66
+ // Fixtures
67
+ // ---------------------------------------------------------------------------
68
+ const makeCase = (id, items = []) => ({
69
+ case_id: id,
70
+ title: `Case ${id}`,
71
+ items
72
+ });
73
+ const hitItem = (path, value = path) => ({ type: 'hit', value, path });
74
+ const caseItem = (path, value) => ({ type: 'case', value, path });
75
+ const refItem = (path, value) => ({ type: 'reference', value, path });
76
+ const renderFolder = (props, routeId) => {
77
+ vi.mocked(useParams).mockReturnValue({ id: routeId ?? props.case.case_id });
78
+ return render(_jsx(MemoryRouter, { children: _jsx(CaseFolder, { step: 0, ...props }) }));
79
+ };
80
+ // ---------------------------------------------------------------------------
81
+ // Setup
82
+ // ---------------------------------------------------------------------------
83
+ beforeEach(() => {
84
+ mockDispatchApi.mockReset();
85
+ mockGetCase.mockReset();
86
+ mockRecords.current = {};
87
+ });
88
+ // ---------------------------------------------------------------------------
89
+ // Tests
90
+ // ---------------------------------------------------------------------------
91
+ describe('CaseFolder', () => {
92
+ describe('flat leaves', () => {
93
+ it('renders a leaf label derived from path', () => {
94
+ renderFolder({ case: makeCase('c1', [hitItem('folder/my-hit', 'v1')]) });
95
+ expect(screen.getByText('my-hit')).toBeInTheDocument();
96
+ });
97
+ it('falls back to value when leaf has no path', () => {
98
+ const noPath = { type: 'hit', value: 'bare-value' };
99
+ // Items with no path are excluded from the tree entirely
100
+ renderFolder({ case: makeCase('c1', [noPath]) });
101
+ expect(screen.queryByText('bare-value')).not.toBeInTheDocument();
102
+ });
103
+ it('renders multiple leaves in the same folder', () => {
104
+ const items = [hitItem('folder/alpha', 'va'), hitItem('folder/beta', 'vb')];
105
+ renderFolder({ case: makeCase('c1', items) });
106
+ expect(screen.getByText('alpha')).toBeInTheDocument();
107
+ expect(screen.getByText('beta')).toBeInTheDocument();
108
+ });
109
+ });
110
+ describe('folder header', () => {
111
+ it('renders the folder name when name prop is provided', () => {
112
+ renderFolder({
113
+ case: makeCase('c1'),
114
+ name: 'documents',
115
+ folder: { path: 'documents', leaves: [hitItem('documents/item', 'v')] }
116
+ });
117
+ expect(screen.getByText('documents')).toBeInTheDocument();
118
+ });
119
+ it('does not render a folder header when name is omitted', () => {
120
+ // Use a flat item (no subfolder) so the only text rendered is the leaf label itself.
121
+ renderFolder({ case: makeCase('c1', [hitItem('top-item', 'v')]) });
122
+ // The leaf label is present but there is no wrapping folder header element
123
+ expect(screen.getByText('top-item')).toBeInTheDocument();
124
+ // queryByText with the exact folder-header label (which would come from the `name` prop) is absent
125
+ expect(screen.queryByText('somefolder')).not.toBeInTheDocument();
126
+ });
127
+ it('collapses children when the folder header is clicked', async () => {
128
+ const user = userEvent.setup();
129
+ renderFolder({
130
+ case: makeCase('c1'),
131
+ name: 'docs',
132
+ folder: { path: 'docs', leaves: [hitItem('docs/item', 'v')] }
133
+ });
134
+ expect(screen.getByText('item')).toBeInTheDocument();
135
+ await user.click(screen.getByText('docs'));
136
+ expect(screen.queryByText('item')).not.toBeInTheDocument();
137
+ });
138
+ it('expands children again after a second click', async () => {
139
+ const user = userEvent.setup();
140
+ renderFolder({
141
+ case: makeCase('c1'),
142
+ name: 'docs',
143
+ folder: { path: 'docs', leaves: [hitItem('docs/item', 'v')] }
144
+ });
145
+ await user.click(screen.getByText('docs'));
146
+ await user.click(screen.getByText('docs'));
147
+ expect(screen.getByText('item')).toBeInTheDocument();
148
+ });
149
+ });
150
+ describe('leaf link URLs', () => {
151
+ it('builds a /cases/<id>/<path> URL for a hit leaf', () => {
152
+ renderFolder({ case: makeCase('case-1', [hitItem('folder/my-hit', 'hit-id')]) });
153
+ const link = screen.getByText('my-hit').closest('a');
154
+ expect(link).toHaveAttribute('href', '/cases/case-1/folder/my-hit');
155
+ });
156
+ it('uses the leaf value directly as href for a reference item', () => {
157
+ renderFolder({ case: makeCase('case-1', [refItem('links/ext', 'https://example.com')]) });
158
+ const link = screen.getByText('ext').closest('a');
159
+ expect(link).toHaveAttribute('href', 'https://example.com');
160
+ });
161
+ it('omits the path segment when the leaf has no path (only value)', () => {
162
+ // A leaf item with value but no structural path still renders the root case URL
163
+ const items = [hitItem('top', 'top-val')];
164
+ renderFolder({ case: makeCase('case-1', items) });
165
+ const link = screen.getByText('top').closest('a');
166
+ expect(link).toHaveAttribute('href', '/cases/case-1/top');
167
+ });
168
+ });
169
+ describe('nested case paths', () => {
170
+ it('prepends parentCasePaths to the leaf URL', () => {
171
+ const leaf = hitItem('example/page', 'page-val');
172
+ renderFolder({
173
+ case: makeCase('case3', [leaf]),
174
+ parentCasePaths: ['cases/caseone', 'cases/casetwo']
175
+ }, 'case1');
176
+ const link = screen.getByText('page').closest('a');
177
+ expect(link).toHaveAttribute('href', '/cases/case1/cases/caseone/cases/casetwo/example/page');
178
+ });
179
+ it('produces the correct URL at one level of nesting', () => {
180
+ const leaf = hitItem('data/item', 'val');
181
+ renderFolder({
182
+ case: makeCase('case2', [leaf]),
183
+ parentCasePaths: ['cases/caseone']
184
+ }, 'case1');
185
+ const link = screen.getByText('item').closest('a');
186
+ expect(link).toHaveAttribute('href', '/cases/case1/cases/caseone/data/item');
187
+ });
188
+ });
189
+ describe('subfolders', () => {
190
+ it('renders subfolder names', () => {
191
+ renderFolder({
192
+ case: makeCase('c1', [hitItem('alpha/item', 'v')])
193
+ });
194
+ // The structural folder "alpha" is not rendered as a labelled header at root level,
195
+ // but its child leaf label "item" should be visible
196
+ expect(screen.getByText('item')).toBeInTheDocument();
197
+ });
198
+ it('renders nested subfolder children', () => {
199
+ renderFolder({
200
+ case: makeCase('c1', [hitItem('a/b/deep', 'v')])
201
+ });
202
+ expect(screen.getByText('deep')).toBeInTheDocument();
203
+ });
204
+ });
205
+ describe('nested case expansion', () => {
206
+ it('does not show nested case content before the case leaf is clicked', () => {
207
+ const items = [caseItem('cases/child', 'child-case-id')];
208
+ const _case = makeCase('root', items);
209
+ mockDispatchApi.mockResolvedValue(null);
210
+ renderFolder({ case: _case });
211
+ expect(screen.queryByText('nested-item')).not.toBeInTheDocument();
212
+ });
213
+ it('fetches the nested case when a case leaf is clicked', async () => {
214
+ const items = [caseItem('cases/child', 'child-case-id')];
215
+ const nestedCase = makeCase('child-case-id', [hitItem('nested/page', 'p')]);
216
+ mockGetCase.mockReturnValue(Promise.resolve(nestedCase));
217
+ mockDispatchApi.mockImplementation((p) => p);
218
+ renderFolder({ case: makeCase('root', items) });
219
+ act(() => {
220
+ screen.getByText('child').click();
221
+ });
222
+ await waitFor(() => {
223
+ expect(mockGetCase).toHaveBeenCalledWith('child-case-id');
224
+ });
225
+ });
226
+ it('renders the nested case items after the fetch resolves', async () => {
227
+ const nestedCase = makeCase('child-case-id', [hitItem('nested/page', 'p')]);
228
+ mockGetCase.mockReturnValue(Promise.resolve(nestedCase));
229
+ mockDispatchApi.mockImplementation((p) => p);
230
+ renderFolder({ case: makeCase('root', [caseItem('cases/child', 'child-case-id')]) });
231
+ act(() => {
232
+ screen.getByText('child').click();
233
+ });
234
+ await waitFor(() => {
235
+ expect(screen.getByText('page')).toBeInTheDocument();
236
+ });
237
+ });
238
+ it('builds the correct URL for a leaf inside a nested case', async () => {
239
+ const nestedCase = makeCase('child-case-id', [hitItem('data/item', 'val')]);
240
+ mockGetCase.mockReturnValue(Promise.resolve(nestedCase));
241
+ mockDispatchApi.mockImplementation((p) => p);
242
+ renderFolder({ case: makeCase('root', [caseItem('cases/child', 'child-case-id')]) });
243
+ act(() => {
244
+ screen.getByText('child').click();
245
+ });
246
+ await waitFor(() => {
247
+ const link = screen.getByText('item').closest('a');
248
+ expect(link).toHaveAttribute('href', '/cases/root/cases/child/data/item');
249
+ });
250
+ });
251
+ it('does not call the API a second time when a case leaf is toggled closed and re-opened', async () => {
252
+ const nestedCase = makeCase('child-case-id', [hitItem('nested/page', 'p')]);
253
+ mockGetCase.mockReturnValue(Promise.resolve(nestedCase));
254
+ mockDispatchApi.mockImplementation((p) => p);
255
+ renderFolder({ case: makeCase('root', [caseItem('cases/child', 'child-case-id')]) });
256
+ act(() => {
257
+ screen.getByText('child').click();
258
+ });
259
+ await waitFor(() => expect(screen.getByText('page')).toBeInTheDocument());
260
+ act(() => {
261
+ screen.getByText('child').click(); // close
262
+ });
263
+ act(() => {
264
+ screen.getByText('child').click(); // re-open
265
+ });
266
+ expect(mockGetCase).toHaveBeenCalledTimes(1);
267
+ });
268
+ it('hides nested case content after the case leaf is toggled closed', async () => {
269
+ const nestedCase = makeCase('child-case-id', [hitItem('nested/page', 'p')]);
270
+ mockGetCase.mockReturnValue(Promise.resolve(nestedCase));
271
+ mockDispatchApi.mockImplementation((p) => p);
272
+ renderFolder({ case: makeCase('root', [caseItem('cases/child', 'child-case-id')]) });
273
+ act(() => {
274
+ screen.getByText('child').click();
275
+ });
276
+ await waitFor(() => expect(screen.getByText('page')).toBeInTheDocument());
277
+ act(() => {
278
+ screen.getByText('child').click();
279
+ });
280
+ expect(screen.queryByText('page')).not.toBeInTheDocument();
281
+ });
282
+ });
283
+ describe('rootCaseId propagation', () => {
284
+ it('uses _case.case_id as the root when rootCaseId is not provided', () => {
285
+ renderFolder({ case: makeCase('my-case', [hitItem('folder/item', 'v')]) });
286
+ const link = screen.getByText('item').closest('a');
287
+ expect(link).toHaveAttribute('href', '/cases/my-case/folder/item');
288
+ });
289
+ it('uses the provided rootCaseId in URLs when given', () => {
290
+ renderFolder({ case: makeCase('nested-case', [hitItem('folder/item', 'v')]) }, 'root-case');
291
+ const link = screen.getByText('item').closest('a');
292
+ expect(link).toHaveAttribute('href', '/cases/root-case/folder/item');
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,34 @@
1
+ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
+ import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
3
+ import { type FC, type PropsWithChildren } from 'react';
4
+ import type { Tree } from './types';
5
+ /**
6
+ * Recursively collects all leaf items from a folder tree.
7
+ */
8
+ export declare const collectAllLeaves: (tree: Tree) => Item[];
9
+ /**
10
+ * Returns the URL to open for a given leaf item, or null if no URL applies.
11
+ * - reference: the item's value (an external URL)
12
+ * - hit: /hits/<id>
13
+ * - observable: /observables/<id>
14
+ * - case: /cases/<id>
15
+ * - table / lead: null (no dedicated detail page)
16
+ */
17
+ export declare const getOpenUrl: (leaf: Item) => string | null;
18
+ export interface CaseFolderContextMenuProps extends PropsWithChildren {
19
+ /** The case that owns the item(s). */
20
+ _case: Case;
21
+ /** Present when the context menu is for a single leaf item. */
22
+ leaf?: Item;
23
+ /** Present when the context menu is for a folder (all leaves within it will be removed). */
24
+ tree?: Tree;
25
+ /** Called after item(s) have been updated (renamed, removed). */
26
+ onUpdate?: (updatedCase: Case) => void;
27
+ }
28
+ /**
29
+ * Wraps its children with a right-click context menu providing:
30
+ * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
31
+ * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
32
+ */
33
+ declare const CaseFolderContextMenu: FC<CaseFolderContextMenuProps>;
34
+ export default CaseFolderContextMenu;
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Delete, DriveFileRenameOutline, OpenInNew } from '@mui/icons-material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
5
+ import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
6
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
+ import RenameItemModal from '@cccsaurora/howler-ui/components/routes/cases/modals/RenameItemModal';
8
+ import { useContext, useMemo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ /**
11
+ * Recursively collects all leaf items from a folder tree.
12
+ */
13
+ export const collectAllLeaves = (tree) => {
14
+ const result = [...(tree.leaves ?? [])];
15
+ for (const subfolder of Object.values(tree.folders ?? {})) {
16
+ result.push(...collectAllLeaves(subfolder));
17
+ }
18
+ return result;
19
+ };
20
+ /**
21
+ * Returns the URL to open for a given leaf item, or null if no URL applies.
22
+ * - reference: the item's value (an external URL)
23
+ * - hit: /hits/<id>
24
+ * - observable: /observables/<id>
25
+ * - case: /cases/<id>
26
+ * - table / lead: null (no dedicated detail page)
27
+ */
28
+ export const getOpenUrl = (leaf) => {
29
+ const type = leaf.type?.toLowerCase();
30
+ if (type === 'reference') {
31
+ return leaf.value ?? null;
32
+ }
33
+ if (type === 'hit') {
34
+ return leaf.value ? `/hits/${leaf.value}` : null;
35
+ }
36
+ if (type === 'observable') {
37
+ return leaf.value ? `/observables/${leaf.value}` : null;
38
+ }
39
+ if (type === 'case') {
40
+ return leaf.value ? `/cases/${leaf.value}` : null;
41
+ }
42
+ return null;
43
+ };
44
+ /**
45
+ * Wraps its children with a right-click context menu providing:
46
+ * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
47
+ * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
48
+ */
49
+ const CaseFolderContextMenu = ({ _case, leaf, tree, onUpdate, children }) => {
50
+ const { dispatchApi } = useMyApi();
51
+ const { t } = useTranslation();
52
+ const { showModal } = useContext(ModalContext);
53
+ const items = useMemo(() => {
54
+ const entries = [];
55
+ if (leaf) {
56
+ const openUrl = getOpenUrl(leaf);
57
+ if (openUrl) {
58
+ entries.push({
59
+ kind: 'item',
60
+ id: 'open-item',
61
+ label: t('page.cases.sidebar.item.open'),
62
+ icon: _jsx(OpenInNew, { fontSize: "small" }),
63
+ onClick: () => window.open(openUrl, '_blank', 'noopener noreferrer')
64
+ });
65
+ }
66
+ entries.push({
67
+ kind: 'item',
68
+ id: 'rename-item',
69
+ label: t('page.cases.sidebar.item.rename'),
70
+ icon: _jsx(DriveFileRenameOutline, { fontSize: "small" }),
71
+ onClick: () => showModal(_jsx(RenameItemModal, { _case: _case, leaf: leaf, onRenamed: onUpdate }), { height: null })
72
+ });
73
+ }
74
+ if (entries.length > 0) {
75
+ entries.push({ kind: 'divider', id: 'divider-remove' });
76
+ }
77
+ const isFolder = !leaf && !!tree;
78
+ entries.push({
79
+ kind: 'item',
80
+ id: 'remove-item',
81
+ label: isFolder ? t('page.cases.sidebar.folder.remove') : t('page.cases.sidebar.item.remove'),
82
+ icon: _jsx(Delete, { fontSize: "small" }),
83
+ onClick: () => {
84
+ if (!_case.case_id) {
85
+ return;
86
+ }
87
+ const itemsToDelete = leaf ? [leaf] : tree ? collectAllLeaves(tree) : [];
88
+ const values = itemsToDelete.filter(i => !!i.value).map(i => i.value);
89
+ if (!values.length) {
90
+ return;
91
+ }
92
+ dispatchApi(api.v2.case.items.del(_case.case_id, values), { throwError: false }).then(updatedCase => {
93
+ if (updatedCase) {
94
+ onUpdate?.(updatedCase);
95
+ }
96
+ });
97
+ }
98
+ });
99
+ return entries;
100
+ }, [_case, leaf, tree, dispatchApi, onUpdate, showModal, t]);
101
+ return _jsx(ContextMenu, { items: items, children: children });
102
+ };
103
+ export default CaseFolderContextMenu;