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

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 +5 -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,170 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ vi.mock('use-context-selector', async () => {
4
+ const actual = await vi.importActual('use-context-selector');
5
+ return {
6
+ ...actual,
7
+ createContext,
8
+ useContextSelector: (_context, selector) => selector(useContext(_context))
9
+ };
10
+ });
11
+ vi.mock('react-i18next', () => ({
12
+ useTranslation: () => ({ t: (key) => key })
13
+ }));
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
16
+ import dayjs from 'dayjs';
17
+ import minMax from 'dayjs/plugin/minMax';
18
+ import {} from 'react';
19
+ dayjs.extend(minMax);
20
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
21
+ // Import after mocks
22
+ import CustomSpan from './CustomSpan';
23
+ const mockSetCustomSpan = vi.fn();
24
+ const defaultCtx = {
25
+ span: 'date.range.custom',
26
+ startDate: undefined,
27
+ endDate: undefined,
28
+ setCustomSpan: mockSetCustomSpan
29
+ };
30
+ const makeWrapper = (ctx) => function ({ children }) {
31
+ return (_jsx(ParameterContext.Provider, { value: { ...defaultCtx, ...ctx }, children: children }));
32
+ };
33
+ describe('CustomSpan', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+ describe('useEffect – date initialisation', () => {
38
+ it('should set both dates to defaults when both are null', async () => {
39
+ render(_jsx(CustomSpan, {}), {
40
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: undefined, endDate: undefined })
41
+ });
42
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
43
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
44
+ const startDate = dayjs(start);
45
+ const endDate = dayjs(end);
46
+ expect(startDate.isBefore(endDate)).toBe(true);
47
+ expect(dayjs().diff(startDate, 'hour')).toBeGreaterThanOrEqual(47);
48
+ expect(dayjs().diff(startDate, 'hour')).toBeLessThanOrEqual(49);
49
+ expect(dayjs().diff(endDate, 'hour')).toBeGreaterThanOrEqual(23);
50
+ expect(dayjs().diff(endDate, 'hour')).toBeLessThanOrEqual(25);
51
+ });
52
+ it('should set default start date when only endDate is provided', async () => {
53
+ // Use a recent end date that's after the default start (now - 2 days)
54
+ const existingEnd = dayjs().subtract(1, 'hour').toISOString();
55
+ render(_jsx(CustomSpan, {}), {
56
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: undefined, endDate: existingEnd })
57
+ });
58
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
59
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
60
+ expect(end).toBe(existingEnd);
61
+ expect(dayjs(start).isBefore(dayjs(end))).toBe(true);
62
+ });
63
+ it('should set default end date when only startDate is provided', async () => {
64
+ // Use a recent start date that's before the default end (now - 1 day)
65
+ const existingStart = dayjs().subtract(3, 'day').toISOString();
66
+ render(_jsx(CustomSpan, {}), {
67
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: existingStart, endDate: undefined })
68
+ });
69
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
70
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
71
+ expect(start).toBe(existingStart);
72
+ expect(dayjs(end).isAfter(dayjs(start))).toBe(true);
73
+ });
74
+ it('should NOT call setCustomSpan when both dates are already set', () => {
75
+ render(_jsx(CustomSpan, {}), {
76
+ wrapper: makeWrapper({
77
+ span: 'date.range.custom',
78
+ startDate: dayjs().subtract(3, 'day').toISOString(),
79
+ endDate: dayjs().subtract(1, 'hour').toISOString()
80
+ })
81
+ });
82
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
83
+ });
84
+ it('should NOT call setCustomSpan when span is not custom', () => {
85
+ render(_jsx(CustomSpan, {}), {
86
+ wrapper: makeWrapper({ span: 'date.range.1.month', startDate: undefined, endDate: undefined })
87
+ });
88
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
89
+ });
90
+ it('should NOT call setCustomSpan when span is undefined', () => {
91
+ render(_jsx(CustomSpan, {}), {
92
+ wrapper: makeWrapper({ span: undefined, startDate: undefined, endDate: undefined })
93
+ });
94
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
95
+ });
96
+ it('should ensure startDate is before endDate even if existing startDate is after default endDate', async () => {
97
+ const futureStart = dayjs().add(1, 'hour').toISOString();
98
+ render(_jsx(CustomSpan, {}), {
99
+ wrapper: makeWrapper({ span: 'date.range.custom', startDate: futureStart, endDate: undefined })
100
+ });
101
+ await waitFor(() => expect(mockSetCustomSpan).toHaveBeenCalledOnce());
102
+ const [start, end] = mockSetCustomSpan.mock.calls[0];
103
+ expect(dayjs(start).isBefore(dayjs(end))).toBe(true);
104
+ });
105
+ });
106
+ describe('rendering', () => {
107
+ it('should render nothing when span is not custom', () => {
108
+ const { container } = render(_jsx(CustomSpan, {}), {
109
+ wrapper: makeWrapper({ span: 'date.range.1.month' })
110
+ });
111
+ expect(container.innerHTML).toBe('');
112
+ });
113
+ it('should render nothing when span is undefined', () => {
114
+ const { container } = render(_jsx(CustomSpan, {}), {
115
+ wrapper: makeWrapper({ span: undefined })
116
+ });
117
+ expect(container.innerHTML).toBe('');
118
+ });
119
+ it('should render two date pickers when span is custom', () => {
120
+ render(_jsx(CustomSpan, {}), {
121
+ wrapper: makeWrapper({
122
+ span: 'date.range.custom',
123
+ startDate: dayjs().subtract(3, 'day').toISOString(),
124
+ endDate: dayjs().subtract(1, 'hour').toISOString()
125
+ })
126
+ });
127
+ expect(screen.getByLabelText('date.select.start')).toBeInTheDocument();
128
+ expect(screen.getByLabelText('date.select.end')).toBeInTheDocument();
129
+ });
130
+ it('should render pickers for any span ending with "custom"', () => {
131
+ render(_jsx(CustomSpan, {}), {
132
+ wrapper: makeWrapper({
133
+ span: 'my.prefix.custom',
134
+ startDate: dayjs().subtract(3, 'day').toISOString(),
135
+ endDate: dayjs().subtract(1, 'hour').toISOString()
136
+ })
137
+ });
138
+ expect(screen.getByLabelText('date.select.start')).toBeInTheDocument();
139
+ expect(screen.getByLabelText('date.select.end')).toBeInTheDocument();
140
+ });
141
+ });
142
+ describe('onChange handlers', () => {
143
+ it('should not call setCustomSpan when both dates are present on render', () => {
144
+ render(_jsx(CustomSpan, {}), {
145
+ wrapper: makeWrapper({
146
+ span: 'date.range.custom',
147
+ startDate: dayjs().subtract(3, 'day').toISOString(),
148
+ endDate: dayjs().subtract(1, 'hour').toISOString()
149
+ })
150
+ });
151
+ const startPicker = screen.getByLabelText('date.select.start');
152
+ expect(startPicker).toBeInTheDocument();
153
+ expect(mockSetCustomSpan).not.toHaveBeenCalled();
154
+ });
155
+ it('should render start picker with fallback value when startDate is null', async () => {
156
+ render(_jsx(CustomSpan, {}), {
157
+ wrapper: makeWrapper({
158
+ span: 'date.range.custom',
159
+ startDate: undefined,
160
+ endDate: dayjs().subtract(1, 'hour').toISOString()
161
+ })
162
+ });
163
+ // When startDate is null, the picker's value falls back to dayjs().subtract(1, 'days')
164
+ const startInput = screen.getByLabelText('date.select.start');
165
+ expect(startInput).toBeInTheDocument();
166
+ // The input should have a value (the fallback), not be empty
167
+ expect(startInput.value).not.toBe('');
168
+ });
169
+ });
170
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("react").NamedExoticComponent<{}>;
2
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FilterList } from '@mui/icons-material';
3
+ import { Autocomplete, TextField } from '@mui/material';
4
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
5
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { useContextSelector } from 'use-context-selector';
9
+ const FILTER_OPTIONS = [
10
+ { label: 'hit.search.index.hit', value: 'hit' },
11
+ { label: 'hit.search.index.observable', value: 'observable' }
12
+ ];
13
+ const IndexPicker = () => {
14
+ const { t } = useTranslation();
15
+ const indexes = useContextSelector(ParameterContext, ctx => ctx.indexes);
16
+ const setIndexes = useContextSelector(ParameterContext, ctx => ctx.setIndexes);
17
+ const selectedOptions = FILTER_OPTIONS.filter(opt => indexes.includes(opt.value));
18
+ return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: selectedOptions.map(opt => t(opt.label)).join(', '), minWidth: "225px", slotProps: { chip: { size: 'small' } }, children: _jsx(Autocomplete, { size: "small", multiple: true, options: FILTER_OPTIONS, value: selectedOptions, onChange: (_ev, values) => values.length > 0 && setIndexes(values.map(val => val.value)), isOptionEqualToValue: (opt, val) => opt.value === val.value, getOptionLabel: opt => t(opt.label), renderInput: params => _jsx(TextField, { ...params }) }) }));
19
+ };
20
+ export default memo(IndexPicker);
@@ -4,23 +4,22 @@ import { Code, Comment, DataObject, History, LinkSharp, QueryStats, ViewAgenda }
4
4
  import { Badge, Box, CardContent, Collapse, IconButton, Skeleton, Stack, Tab, Tabs, Tooltip, useMediaQuery, useTheme } from '@mui/material';
5
5
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
6
6
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
7
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
7
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
8
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
9
9
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
10
- import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
11
10
  import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
12
11
  import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
13
12
  import HitActions from '@cccsaurora/howler-ui/components/elements/hit/HitActions';
14
13
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
15
- import HitComments from '@cccsaurora/howler-ui/components/elements/hit/HitComments';
16
- import HitDetails from '@cccsaurora/howler-ui/components/elements/hit/HitDetails';
17
14
  import HitLabels from '@cccsaurora/howler-ui/components/elements/hit/HitLabels';
18
15
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
19
16
  import HitLinks from '@cccsaurora/howler-ui/components/elements/hit/HitLinks';
20
17
  import HitOutline from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
21
18
  import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOverview';
22
- import HitRelated from '@cccsaurora/howler-ui/components/elements/hit/HitRelated';
23
- import HitWorklog from '@cccsaurora/howler-ui/components/elements/hit/HitWorklog';
19
+ import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
20
+ import RecordComments from '@cccsaurora/howler-ui/components/elements/record/RecordComments';
21
+ import RecordRelated from '@cccsaurora/howler-ui/components/elements/record/RecordRelated';
22
+ import RecordWorklog from '@cccsaurora/howler-ui/components/elements/record/RecordWorklog';
24
23
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
25
24
  import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
26
25
  import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -44,8 +43,8 @@ const HitViewer = () => {
44
43
  const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
45
44
  const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
46
45
  const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
47
- const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
48
- const hit = useContextSelector(HitContext, ctx => ctx.hits[params.id]);
46
+ const getHit = useContextSelector(RecordContext, ctx => ctx.getRecord);
47
+ const hit = useContextSelector(RecordContext, ctx => ctx.records[params.id]);
49
48
  const [userIds, setUserIds] = useState(new Set());
50
49
  const users = useMyUserList(userIds);
51
50
  const [tab, setTab] = useState('details');
@@ -94,12 +93,12 @@ const HitViewer = () => {
94
93
  }
95
94
  return {
96
95
  overview: () => _jsx(HitOverview, { hit: hit }),
97
- details: () => _jsx(HitDetails, { hit: hit }),
98
- hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
96
+ details: () => _jsx(ObjectDetails, { obj: hit }),
97
+ hit_comments: () => _jsx(RecordComments, { record: hit, users: users }),
99
98
  hit_raw: () => _jsx(JSONViewer, { data: hit }),
100
99
  hit_data: () => _jsx(JSONViewer, { data: hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false }),
101
- hit_worklog: () => _jsx(HitWorklog, { hit: hit, users: users }),
102
- hit_related: () => _jsx(HitRelated, { hit: hit }),
100
+ hit_worklog: () => _jsx(RecordWorklog, { record: hit, users: users }),
101
+ hit_related: () => _jsx(RecordRelated, { record: hit }),
103
102
  ...Object.fromEntries(hit?.howler.dossier?.map((lead, index) => ['lead:' + index, () => _jsx(LeadRenderer, { lead: lead, hit: hit })]) ?? []),
104
103
  ...Object.fromEntries(dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => [
105
104
  `external-lead:${dossierIndex}:${leadIndex}`,
@@ -132,7 +131,7 @@ const HitViewer = () => {
132
131
  position: 'absolute',
133
132
  top: theme.spacing(2),
134
133
  right: theme.spacing(-6)
135
- }, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
134
+ }, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) }))] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
136
135
  // eslint-disable-next-line react/no-array-index-key
137
136
  , { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => (_jsx(Tab
138
137
  // eslint-disable-next-line react/no-array-index-key
@@ -64,7 +64,7 @@ const AddNewCard = ({ dashboard, addCard }) => {
64
64
  fetchAllAnalytics();
65
65
  }, []);
66
66
  const filteredAnalyticVisualizations = useMemo(() => {
67
- const existingAnalyticCards = dashboard.filter(_card => _card.type === "analytic");
67
+ const existingAnalyticCards = dashboard.filter(_card => _card.type === 'analytic');
68
68
  return VISUALIZATIONS.filter(viz => {
69
69
  return !existingAnalyticCards.some(_card => {
70
70
  const parsedConfig = JSON.parse(_card.config);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import React from 'react';
6
+ import { I18nextProvider } from 'react-i18next';
7
+ import { setupContextSelectorMock } from '@cccsaurora/howler-ui/tests/mocks';
8
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import AddNewCard from './AddNewCard';
10
+ setupContextSelectorMock();
11
+ const mockApiSearchAnalyticPost = vi.fn();
12
+ vi.mock('api', () => ({
13
+ default: {
14
+ search: {
15
+ analytic: {
16
+ post: (...args) => mockApiSearchAnalyticPost(...args)
17
+ }
18
+ }
19
+ }
20
+ }));
21
+ vi.mock('components/elements/addons/buttons/CustomButton', () => ({
22
+ default: ({ children, disabled, onClick, ...props }) => (_jsx("button", { disabled: disabled, onClick: onClick, ...props, children: children }))
23
+ }));
24
+ const mockFetchViews = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
25
+ vi.mock('components/app/providers/ViewProvider', () => ({
26
+ ViewContext: React.createContext({
27
+ views: {
28
+ 'view-1': {
29
+ view_id: 'view-1',
30
+ title: 'My View',
31
+ query: 'howler.status:open',
32
+ sort: 'event.created desc',
33
+ span: 'date.range.1.month',
34
+ type: 'personal',
35
+ owner: 'testuser',
36
+ settings: { advance_on_triage: false }
37
+ },
38
+ 'view-2': {
39
+ view_id: 'view-2',
40
+ title: 'Second View',
41
+ query: 'howler.status:open',
42
+ sort: 'event.created desc',
43
+ span: 'date.range.1.month',
44
+ type: 'personal',
45
+ owner: 'testuser',
46
+ settings: { advance_on_triage: false }
47
+ }
48
+ },
49
+ fetchViews: mockFetchViews
50
+ })
51
+ }));
52
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
53
+ describe('AddNewCard', () => {
54
+ const mockAddCard = vi.fn();
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ mockApiSearchAnalyticPost.mockResolvedValue({
58
+ items: [{ analytic_id: 'analytic-1', name: 'Test Analytic', description: 'Test description' }],
59
+ total: 1
60
+ });
61
+ });
62
+ it('should render the add card title', async () => {
63
+ render(_jsx(AddNewCard, { dashboard: [], addCard: mockAddCard }), { wrapper: Wrapper });
64
+ await waitFor(() => {
65
+ expect(screen.getByText('Add New Panel')).toBeInTheDocument();
66
+ });
67
+ });
68
+ it('should render the description', async () => {
69
+ render(_jsx(AddNewCard, { dashboard: [], addCard: mockAddCard }), { wrapper: Wrapper });
70
+ await waitFor(() => {
71
+ expect(screen.getByText(/Add an additional panel/)).toBeInTheDocument();
72
+ });
73
+ });
74
+ it('should have the create button disabled initially', async () => {
75
+ render(_jsx(AddNewCard, { dashboard: [], addCard: mockAddCard }), { wrapper: Wrapper });
76
+ await waitFor(() => {
77
+ expect(screen.getByRole('button', { name: /Create/i })).toBeDisabled();
78
+ });
79
+ });
80
+ it('should fetch analytics on mount', async () => {
81
+ render(_jsx(AddNewCard, { dashboard: [], addCard: mockAddCard }), { wrapper: Wrapper });
82
+ await waitFor(() => {
83
+ expect(mockApiSearchAnalyticPost).toHaveBeenCalledWith(expect.objectContaining({ query: '*:*' }));
84
+ });
85
+ });
86
+ });
@@ -3,74 +3,80 @@ import { OpenInNew } from '@mui/icons-material';
3
3
  import { Card, CardContent, IconButton, Skeleton, Stack, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
- import { useHitContextSelector } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
6
+ import { useRecordContextSelector } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
7
7
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
8
8
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
9
9
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
10
+ import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
11
+ import RecordContextMenu from '@cccsaurora/howler-ui/components/elements/record/RecordContextMenu';
10
12
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
- import HitContextMenu from '@cccsaurora/howler-ui/components/routes/hits/search/HitContextMenu';
12
13
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
14
  import { useTranslation } from 'react-i18next';
14
15
  import { Link, useNavigate } from 'react-router-dom';
15
16
  import { useContextSelector } from 'use-context-selector';
17
+ import { isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
16
18
  import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
17
- // Custom hook to select hits by IDs with proper memoization
18
- const useSelectHitsByIds = (hitIds) => {
19
- const hitIdsRef = useRef(hitIds);
19
+ // Custom hook to select records by IDs with proper memoization
20
+ const useSelectRecordsByIds = (recordIds) => {
21
+ const recordIdsRef = useRef(recordIds);
20
22
  const prevResultRef = useRef([]);
21
- const prevHitIdsRef = useRef([]);
22
- // Keep ref up to date with latest hitIds
23
- hitIdsRef.current = hitIds;
23
+ const prevRecordIdsRef = useRef([]);
24
+ // Keep ref up to date with latest recordIds
25
+ recordIdsRef.current = recordIds;
24
26
  const selector = useCallback(ctx => {
25
- const currentHitIds = hitIdsRef.current;
26
- // Fast path: if hitIds array didn't change, check if hit objects changed
27
- if (prevHitIdsRef.current.length === currentHitIds.length &&
28
- currentHitIds.every((id, i) => id === prevHitIdsRef.current[i])) {
29
- // HitIds unchanged - check if any hit objects changed by reference
30
- const anyHitChanged = currentHitIds.some((id, i) => ctx.hits[id] !== prevResultRef.current[i]);
31
- if (!anyHitChanged) {
27
+ const currentRecordIds = recordIdsRef.current;
28
+ // Fast path: if recordIds array didn't change, check if record objects changed
29
+ if (prevRecordIdsRef.current.length === currentRecordIds.length &&
30
+ currentRecordIds.every((id, i) => id === prevRecordIdsRef.current[i])) {
31
+ // RecordIds unchanged - check if any record objects changed by reference
32
+ const anyRecordChanged = currentRecordIds.some((id, i) => ctx.records[id] !== prevResultRef.current[i]);
33
+ if (!anyRecordChanged) {
32
34
  return prevResultRef.current;
33
35
  }
34
36
  }
35
37
  // Something changed - rebuild the array
36
- const currentHits = currentHitIds.map(id => ctx.hits[id]).filter(Boolean);
37
- prevHitIdsRef.current = currentHitIds;
38
- prevResultRef.current = currentHits;
39
- return currentHits;
38
+ const currentRecords = currentRecordIds.map(id => ctx.records[id]).filter(Boolean);
39
+ prevRecordIdsRef.current = currentRecordIds;
40
+ prevResultRef.current = currentRecords;
41
+ return currentRecords;
40
42
  }, []); // Empty deps - selector never changes
41
- return useHitContextSelector(selector);
43
+ return useRecordContextSelector(selector);
42
44
  };
43
45
  // Utility functions
44
46
  const normalize = (val) => (val == null ? '' : String(val));
45
47
  // Have to normalize the fields as websockets and api return null and undefined respectively. This causes false positives when comparing signatures if not normalized to a consistent value. We also stringify non-primitive values to ensure changes are detected.
46
- const createHitSignature = (hit) => {
47
- if (!hit)
48
+ const createRecordSignature = (record) => {
49
+ if (!record) {
48
50
  return '';
49
- return `${hit.howler?.id}:${normalize(hit.howler?.status)}:${normalize(hit.howler?.assignment)}:${normalize(hit.howler?.assessment)}`;
51
+ }
52
+ if (isObservable(record)) {
53
+ return record.howler?.id;
54
+ }
55
+ return `${record.howler?.id}:${normalize(record.howler?.status)}:${normalize(record.howler?.assignment)}:${normalize(record.howler?.assessment)}`;
50
56
  };
51
- const createSignatureFromHits = (hits) => {
52
- if (hits.length === 0)
57
+ const createSignatureFromRecords = (records) => {
58
+ if (records.length === 0)
53
59
  return '';
54
- return hits.map(createHitSignature).join('|');
60
+ return records.map(createRecordSignature).join('|');
55
61
  };
56
62
  const DEBOUNCE_TIME = 1000; // 1 second debounce for signature changes
57
63
  const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
58
64
  const navigate = useNavigate();
59
65
  const { t } = useTranslation();
60
66
  const { dispatchApi } = useMyApi();
61
- const [hitIds, setHitIds] = useState([]);
67
+ const [recordIds, setRecordIds] = useState([]);
62
68
  const [loading, setLoading] = useState(false);
63
69
  const debounceTimerRef = useRef(null);
64
70
  const isRefreshing = useRef(false);
65
71
  const lastSignature = useRef('');
66
72
  const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
67
73
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
68
- const loadHits = useHitContextSelector(ctx => ctx.loadHits);
74
+ const loadRecords = useRecordContextSelector(ctx => ctx.loadRecords);
69
75
  // Subscribe to hits from HitProvider cache based on current hitIds in the view
70
76
  // Uses memoized selector to avoid unnecessary re-renders on unrelated hit updates
71
- const hits = useSelectHitsByIds(hitIds);
77
+ const records = useSelectRecordsByIds(recordIds);
72
78
  // Create a stable signature that only changes when relevant fields change
73
- const hitsSignature = useMemo(() => createSignatureFromHits(hits), [hits]);
79
+ const recordsSignature = useMemo(() => createSignatureFromRecords(records), [records]);
74
80
  const refreshView = useCallback(async () => {
75
81
  if (!view?.query || isRefreshing.current) {
76
82
  onRefreshComplete?.();
@@ -78,21 +84,21 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
78
84
  }
79
85
  isRefreshing.current = true;
80
86
  try {
81
- const res = await dispatchApi(api.search.hit.post({
87
+ const res = await dispatchApi(api.v2.search.post(view.indexes, {
82
88
  query: view.query,
83
89
  rows: limit,
84
90
  metadata: ['analytic']
85
91
  }));
86
- const fetchedHits = res.items ?? [];
87
- loadHits(fetchedHits);
88
- setHitIds(fetchedHits.map(h => h.howler.id));
89
- lastSignature.current = createSignatureFromHits(fetchedHits);
92
+ const fetchedRecords = res.items ?? [];
93
+ loadRecords(fetchedRecords);
94
+ setRecordIds(fetchedRecords.map(r => r.howler.id));
95
+ lastSignature.current = createSignatureFromRecords(fetchedRecords);
90
96
  }
91
97
  finally {
92
98
  isRefreshing.current = false;
93
99
  onRefreshComplete?.();
94
100
  }
95
- }, [dispatchApi, limit, view?.query, loadHits, onRefreshComplete]);
101
+ }, [dispatchApi, limit, view?.query, view?.indexes, loadRecords, onRefreshComplete]);
96
102
  const debouncedRefresh = useCallback(() => {
97
103
  if (debounceTimerRef.current) {
98
104
  clearTimeout(debounceTimerRef.current);
@@ -125,16 +131,16 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
125
131
  }, [view?.query, limit, refreshView]);
126
132
  // Monitor hits currently in the view for changes that might affect query results
127
133
  useEffect(() => {
128
- if (!hitsSignature || hitIds.length === 0 || !lastSignature.current) {
129
- lastSignature.current = hitsSignature;
134
+ if (!recordsSignature || recordIds.length === 0 || !lastSignature.current) {
135
+ lastSignature.current = recordsSignature;
130
136
  return;
131
137
  }
132
138
  // Check if signature actually changed
133
- if (lastSignature.current === hitsSignature) {
139
+ if (lastSignature.current === recordsSignature) {
134
140
  return;
135
141
  }
136
142
  debouncedRefresh();
137
- }, [hitsSignature, hitIds, debouncedRefresh]);
143
+ }, [recordsSignature, recordIds, debouncedRefresh]);
138
144
  useEffect(() => {
139
145
  return () => {
140
146
  if (debounceTimerRef.current) {
@@ -151,6 +157,6 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
151
157
  }
152
158
  return selectedElement.id;
153
159
  }, []);
154
- return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate((h.howler.is_bundle ? '/bundles/' : '/hits/') + h.howler.id), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
160
+ return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", component: Link, disabled: !view, to: view ? buildViewUrl(view) : '', onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : records.length > 0 ? (_jsx(RecordContextMenu, { getSelectedId: getSelectedId, children: records.map((r) => (_jsx(Card, { id: r.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate(`/hits/${r.howler.id}`), children: _jsx(CardContent, { children: r.__index == 'hit' ? (_jsx(HitBanner, { layout: HitLayout.DENSE, hit: r })) : (_jsx(ObservableCard, { observable: r })) }) }, r.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
155
161
  };
156
162
  export default ViewCard;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { act, render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import React from 'react';
6
+ import { I18nextProvider } from 'react-i18next';
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import ViewRefresh, {} from './ViewRefresh';
9
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
10
+ describe('ViewRefresh', () => {
11
+ beforeEach(() => {
12
+ vi.useFakeTimers();
13
+ });
14
+ afterEach(() => {
15
+ vi.useRealTimers();
16
+ });
17
+ it('should render the refresh button', () => {
18
+ const onRefresh = vi.fn();
19
+ render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
20
+ expect(screen.getByRole('button')).toBeInTheDocument();
21
+ });
22
+ it('should show a progress indicator initially', () => {
23
+ const onRefresh = vi.fn();
24
+ render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
25
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
26
+ });
27
+ it('should trigger refresh when progress reaches 100%', async () => {
28
+ const onRefresh = vi.fn();
29
+ render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
30
+ // Progress increments by 1 every refreshRate*10ms = 300ms.
31
+ // Each increment triggers a re-render and a new setTimeout.
32
+ // We advance one tick at a time inside act to let React process each state update.
33
+ for (let i = 0; i < 101; i++) {
34
+ await act(async () => {
35
+ vi.advanceTimersByTime(300);
36
+ });
37
+ }
38
+ expect(onRefresh).toHaveBeenCalled();
39
+ });
40
+ it('should clear refreshing state when all cards report back via ref', async () => {
41
+ const onRefresh = vi.fn();
42
+ const ref = React.createRef();
43
+ render(_jsx(ViewRefresh, { ref: ref, refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
44
+ // Advance to 100% to trigger refresh
45
+ for (let i = 0; i < 101; i++) {
46
+ await act(async () => {
47
+ vi.advanceTimersByTime(300);
48
+ });
49
+ }
50
+ expect(onRefresh).toHaveBeenCalled();
51
+ // Simulate both cards completing
52
+ act(() => {
53
+ ref.current?.handleRefreshComplete();
54
+ ref.current?.handleRefreshComplete();
55
+ });
56
+ // After all cards complete, the progress should reset (button re-enabled)
57
+ expect(screen.getByRole('button')).not.toBeDisabled();
58
+ });
59
+ it('should trigger refresh via manual click', () => {
60
+ const onRefresh = vi.fn();
61
+ render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
62
+ // Click the refresh button directly using fireEvent (avoids userEvent timer issues)
63
+ const button = screen.getByRole('button');
64
+ act(() => {
65
+ button.click();
66
+ });
67
+ expect(onRefresh).toHaveBeenCalled();
68
+ });
69
+ it('should not call onRefresh when viewCardCount is 0 and button is clicked', () => {
70
+ const onRefresh = vi.fn();
71
+ render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 0, onRefresh: onRefresh }), { wrapper: Wrapper });
72
+ const button = screen.getByRole('button');
73
+ act(() => {
74
+ button.click();
75
+ });
76
+ // onRefresh should not be called because viewCardCount is 0
77
+ expect(onRefresh).not.toHaveBeenCalled();
78
+ });
79
+ });
@@ -0,0 +1,7 @@
1
+ import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
2
+ import { type FC } from 'react';
3
+ declare const ObservableViewer: FC<{
4
+ observable?: Observable;
5
+ observableId?: string;
6
+ }>;
7
+ export default ObservableViewer;