@cccsaurora/howler-ui 2.18.0-dev.766 → 2.18.0-dev.771

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 (297) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/facet/hit.d.ts +3 -1
  4. package/api/search/facet/index.d.ts +1 -3
  5. package/api/search/index.d.ts +1 -2
  6. package/api/search/index.js +1 -2
  7. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  8. package/components/app/App.js +8 -36
  9. package/components/app/hooks/useMatchers.d.ts +1 -1
  10. package/components/app/hooks/useMatchers.js +11 -23
  11. package/components/app/hooks/useMatchers.test.js +22 -22
  12. package/components/app/hooks/useTitle.js +3 -3
  13. package/components/app/providers/FavouritesProvider.js +2 -2
  14. package/components/app/providers/HitProvider.d.ts +22 -0
  15. package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
  16. package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
  17. package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
  18. package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +71 -52
  19. package/components/app/providers/ModalProvider.d.ts +0 -1
  20. package/components/app/providers/ParameterProvider.d.ts +2 -9
  21. package/components/app/providers/ParameterProvider.js +240 -165
  22. package/components/app/providers/ParameterProvider.test.js +94 -346
  23. package/components/app/providers/UserListProvider.js +8 -28
  24. package/components/elements/PluginTypography.d.ts +1 -2
  25. package/components/elements/PluginTypography.js +2 -3
  26. package/components/elements/UserList.d.ts +2 -5
  27. package/components/elements/UserList.js +8 -18
  28. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  29. package/components/elements/display/ChipPopper.d.ts +1 -1
  30. package/components/elements/display/HowlerCard.js +1 -1
  31. package/components/elements/display/Modal.js +0 -2
  32. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  33. package/components/elements/display/icons/BundleButton.js +32 -0
  34. package/components/elements/hit/HitActions.js +4 -4
  35. package/components/elements/hit/HitBanner.d.ts +0 -1
  36. package/components/elements/hit/HitBanner.js +49 -29
  37. package/components/elements/hit/HitCard.d.ts +0 -2
  38. package/components/elements/hit/HitCard.js +7 -7
  39. package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
  40. package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
  41. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  42. package/components/elements/hit/HitLabels.js +2 -2
  43. package/components/elements/hit/HitLinks.js +1 -1
  44. package/components/elements/hit/HitOutline.d.ts +0 -1
  45. package/components/elements/hit/HitOutline.js +3 -3
  46. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  47. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  48. package/components/elements/hit/HitRelated.d.ts +6 -0
  49. package/components/elements/hit/HitRelated.js +7 -0
  50. package/components/elements/hit/HitSummary.d.ts +1 -2
  51. package/components/elements/hit/HitSummary.js +5 -6
  52. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  53. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  54. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  55. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  56. package/components/elements/view/ViewTitle.d.ts +0 -1
  57. package/components/elements/view/ViewTitle.js +2 -9
  58. package/components/hooks/useHitActions.d.ts +1 -1
  59. package/components/hooks/useHitActions.js +4 -4
  60. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  61. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  62. package/components/hooks/useMyPreferences.js +1 -10
  63. package/components/hooks/useMySearch.js +2 -2
  64. package/components/hooks/useMySitemap.js +1 -4
  65. package/components/hooks/useMyTheme.js +2 -9
  66. package/components/routes/action/edit/ActionEditor.js +2 -2
  67. package/components/routes/action/view/ActionSearch.js +1 -1
  68. package/components/routes/advanced/QueryBuilder.js +1 -1
  69. package/components/routes/advanced/QueryEditor.js +3 -3
  70. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  71. package/components/routes/analytics/AnalyticDetails.js +2 -2
  72. package/components/routes/analytics/AnalyticSearch.js +1 -1
  73. package/components/routes/dossiers/DossierEditor.js +2 -2
  74. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  75. package/components/routes/help/ApiDocumentation.js +1 -1
  76. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  77. package/components/routes/help/BundleDocumentation.js +12 -0
  78. package/components/routes/help/HitBannerDocumentation.js +0 -1
  79. package/components/routes/help/HitDocumentation.js +3 -1
  80. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  81. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  82. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  83. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  84. package/components/routes/hits/search/BundleScroller.d.ts +2 -0
  85. package/components/routes/hits/search/BundleScroller.js +6 -0
  86. package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
  87. package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
  88. package/components/routes/hits/search/HitContextMenu.js +227 -0
  89. package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
  90. package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
  91. package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
  92. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  93. package/components/routes/hits/search/InformationPane.js +60 -47
  94. package/components/routes/hits/search/LayoutSettings.js +3 -3
  95. package/components/routes/hits/search/QuerySettings.js +1 -2
  96. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  97. package/components/routes/hits/search/SearchPane.js +49 -26
  98. package/components/routes/hits/search/ViewLink.js +3 -3
  99. package/components/routes/hits/search/ViewLink.test.js +8 -8
  100. package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
  101. package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
  102. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  103. package/components/routes/hits/search/grid/HitGrid.js +18 -20
  104. package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
  105. package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
  106. package/components/routes/hits/view/HitViewer.js +13 -12
  107. package/components/routes/home/ViewCard.js +41 -47
  108. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  109. package/components/routes/overviews/OverviewViewer.js +2 -2
  110. package/components/routes/views/ViewComposer.js +19 -46
  111. package/locales/en/translation.json +3 -109
  112. package/locales/fr/translation.json +3 -107
  113. package/models/WithMetadata.d.ts +1 -2
  114. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  115. package/models/entities/generated/Hit.d.ts +0 -1
  116. package/models/entities/generated/Howler.d.ts +4 -0
  117. package/models/entities/generated/Rule.d.ts +10 -2
  118. package/models/entities/generated/Threat.d.ts +2 -2
  119. package/models/entities/generated/View.d.ts +0 -1
  120. package/package.json +3 -21
  121. package/plugins/clue/components/ClueTypography.js +2 -2
  122. package/plugins/clue/utils.d.ts +1 -2
  123. package/tests/mocks.d.ts +1 -11
  124. package/tests/mocks.js +7 -12
  125. package/tests/server-handlers.js +1 -6
  126. package/tests/utils.d.ts +0 -4
  127. package/tests/utils.js +0 -20
  128. package/utils/constants.d.ts +3 -4
  129. package/utils/constants.js +0 -6
  130. package/utils/hitFunctions.d.ts +1 -2
  131. package/utils/hitFunctions.js +4 -4
  132. package/utils/viewUtils.js +0 -3
  133. package/api/search/case.d.ts +0 -4
  134. package/api/search/case.js +0 -8
  135. package/api/v2/case/index.d.ts +0 -8
  136. package/api/v2/case/index.js +0 -20
  137. package/api/v2/case/items.d.ts +0 -6
  138. package/api/v2/case/items.js +0 -18
  139. package/api/v2/index.d.ts +0 -4
  140. package/api/v2/index.js +0 -6
  141. package/api/v2/search/facet.d.ts +0 -3
  142. package/api/v2/search/facet.js +0 -12
  143. package/api/v2/search/index.d.ts +0 -5
  144. package/api/v2/search/index.js +0 -24
  145. package/components/app/providers/RecordProvider.d.ts +0 -23
  146. package/components/elements/ContextMenu.d.ts +0 -56
  147. package/components/elements/ContextMenu.js +0 -109
  148. package/components/elements/ContextMenu.test.js +0 -215
  149. package/components/elements/ObjectDetails.d.ts +0 -6
  150. package/components/elements/case/CaseCard.d.ts +0 -12
  151. package/components/elements/case/CaseCard.js +0 -42
  152. package/components/elements/case/CasePreview.d.ts +0 -6
  153. package/components/elements/case/CasePreview.js +0 -17
  154. package/components/elements/case/StatusIcon.d.ts +0 -5
  155. package/components/elements/case/StatusIcon.js +0 -13
  156. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
  157. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  158. package/components/elements/hit/related/RelatedRecords.js +0 -63
  159. package/components/elements/observable/ObservableCard.d.ts +0 -6
  160. package/components/elements/observable/ObservableCard.js +0 -22
  161. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  162. package/components/elements/observable/ObservablePreview.js +0 -12
  163. package/components/elements/record/RecordContextMenu.js +0 -256
  164. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  165. package/components/elements/record/RecordRelated.d.ts +0 -7
  166. package/components/elements/record/RecordRelated.js +0 -34
  167. package/components/hooks/useRelatedRecords.d.ts +0 -13
  168. package/components/hooks/useRelatedRecords.js +0 -32
  169. package/components/routes/cases/CaseViewer.d.ts +0 -2
  170. package/components/routes/cases/CaseViewer.js +0 -22
  171. package/components/routes/cases/Cases.d.ts +0 -2
  172. package/components/routes/cases/Cases.js +0 -148
  173. package/components/routes/cases/constants.d.ts +0 -6
  174. package/components/routes/cases/constants.js +0 -6
  175. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  176. package/components/routes/cases/detail/AlertPanel.js +0 -33
  177. package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
  178. package/components/routes/cases/detail/CaseAssets.js +0 -104
  179. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  180. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  181. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  182. package/components/routes/cases/detail/CaseDashboard.js +0 -66
  183. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  184. package/components/routes/cases/detail/CaseDetails.js +0 -61
  185. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  186. package/components/routes/cases/detail/CaseOverview.js +0 -43
  187. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  188. package/components/routes/cases/detail/CaseSidebar.js +0 -107
  189. package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
  190. package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
  191. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  192. package/components/routes/cases/detail/CaseTask.js +0 -57
  193. package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
  194. package/components/routes/cases/detail/CaseTimeline.js +0 -106
  195. package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
  196. package/components/routes/cases/detail/CaseTimeline.test.js +0 -320
  197. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  198. package/components/routes/cases/detail/ItemPage.js +0 -99
  199. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  200. package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
  201. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  202. package/components/routes/cases/detail/TaskPanel.js +0 -52
  203. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
  204. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
  205. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  206. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
  207. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  208. package/components/routes/cases/detail/assets/Asset.js +0 -12
  209. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  210. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  211. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
  212. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
  213. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
  214. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
  215. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  216. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
  217. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  218. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
  219. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
  220. package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
  221. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
  222. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
  223. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
  224. package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
  225. package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
  226. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  227. package/components/routes/cases/detail/sidebar/utils.js +0 -29
  228. package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
  229. package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
  230. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  231. package/components/routes/cases/hooks/useCase.js +0 -51
  232. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  233. package/components/routes/cases/modals/AddToCaseModal.js +0 -59
  234. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +0 -1
  235. package/components/routes/cases/modals/AddToCaseModal.test.js +0 -313
  236. package/components/routes/cases/modals/CaseRecordRow.d.ts +0 -9
  237. package/components/routes/cases/modals/CaseRecordRow.js +0 -15
  238. package/components/routes/cases/modals/CreateCaseModal.d.ts +0 -7
  239. package/components/routes/cases/modals/CreateCaseModal.js +0 -55
  240. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +0 -1
  241. package/components/routes/cases/modals/CreateCaseModal.test.js +0 -358
  242. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  243. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  244. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  245. package/components/routes/cases/modals/ResolveModal.js +0 -115
  246. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  247. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  248. package/components/routes/cases/modals/hooks.d.ts +0 -7
  249. package/components/routes/cases/modals/hooks.js +0 -44
  250. package/components/routes/cases/modals/types.d.ts +0 -5
  251. package/components/routes/cases/search/CaseAssigneeFilter.d.ts +0 -6
  252. package/components/routes/cases/search/CaseAssigneeFilter.js +0 -33
  253. package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +0 -1
  254. package/components/routes/cases/search/CaseAssigneeFilter.test.js +0 -127
  255. package/components/routes/cases/search/CaseDateFilter.d.ts +0 -13
  256. package/components/routes/cases/search/CaseDateFilter.js +0 -26
  257. package/components/routes/cases/search/CaseDateFilter.test.d.ts +0 -1
  258. package/components/routes/cases/search/CaseDateFilter.test.js +0 -115
  259. package/components/routes/cases/search/CaseStatusFilter.d.ts +0 -6
  260. package/components/routes/cases/search/CaseStatusFilter.js +0 -13
  261. package/components/routes/cases/search/CaseStatusFilter.test.d.ts +0 -1
  262. package/components/routes/cases/search/CaseStatusFilter.test.js +0 -86
  263. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  264. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  265. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  266. package/components/routes/observables/ObservableViewer.js +0 -27
  267. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  268. package/models/entities/generated/Case.d.ts +0 -28
  269. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  270. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  271. package/models/entities/generated/EmailParent.d.ts +0 -19
  272. package/models/entities/generated/Enrichments.d.ts +0 -7
  273. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  274. package/models/entities/generated/HttpResponse.d.ts +0 -11
  275. package/models/entities/generated/Item.d.ts +0 -9
  276. package/models/entities/generated/Observable.d.ts +0 -85
  277. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  278. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  279. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  280. package/models/entities/generated/ObservableFile.d.ts +0 -36
  281. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  282. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  283. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  284. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  285. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  286. package/models/entities/generated/ObservableSource.d.ts +0 -23
  287. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  288. package/models/entities/generated/ObservableTls.d.ts +0 -12
  289. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  290. package/models/entities/generated/Task.d.ts +0 -10
  291. package/utils/typeUtils.d.ts +0 -7
  292. package/utils/typeUtils.js +0 -27
  293. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  294. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  295. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  296. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  297. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,358 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- /// <reference types="vitest" />
3
- import { act, render, screen, waitFor } from '@testing-library/react';
4
- import userEvent, {} from '@testing-library/user-event';
5
- import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
- import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
- import i18n from '@cccsaurora/howler-ui/i18n';
8
- import { I18nextProvider } from 'react-i18next';
9
- import { createMockHit, createMockObservable } from '@cccsaurora/howler-ui/tests/utils';
10
- import { beforeEach, describe, expect, it, vi } from 'vitest';
11
- import CreateCaseModal from './CreateCaseModal';
12
- // ---------------------------------------------------------------------------
13
- // Hoisted mocks
14
- // ---------------------------------------------------------------------------
15
- const mockDispatchApi = vi.hoisted(() => vi.fn());
16
- const mockClose = vi.hoisted(() => vi.fn());
17
- vi.mock('components/hooks/useMyApi', () => ({
18
- default: () => ({ dispatchApi: mockDispatchApi })
19
- }));
20
- vi.mock('components/elements/hit/elements/EscalationChip', () => ({
21
- default: () => null
22
- }));
23
- let mockSetContent = null;
24
- vi.mock('components/elements/MarkdownEditor', () => ({
25
- default: ({ content, setContent }) => {
26
- mockSetContent = setContent;
27
- return _jsx("textarea", { id: "markdown-editor", value: content, onChange: ev => setContent(ev.target.value) });
28
- }
29
- }));
30
- vi.mock('api', () => ({
31
- default: {
32
- v2: {
33
- case: {
34
- post: vi.fn().mockReturnValue('case-post-request'),
35
- items: {
36
- post: vi.fn().mockReturnValue('items-post-request')
37
- }
38
- }
39
- }
40
- }
41
- }));
42
- // ---------------------------------------------------------------------------
43
- // Fixtures
44
- // ---------------------------------------------------------------------------
45
- const MOCK_HIT_1 = createMockHit({
46
- howler: { id: 'hit-001', analytic: 'AnalyticOne', status: 'open' }
47
- });
48
- const MOCK_HIT_2 = createMockHit({
49
- howler: { id: 'hit-002', analytic: 'AnalyticTwo', status: 'open' }
50
- });
51
- const MOCK_OBSERVABLE = createMockObservable({
52
- howler: { id: 'obs-001' }
53
- });
54
- const MOCK_CONFIG = {
55
- lookups: {
56
- 'howler.escalation': ['normal', 'focus', 'crisis']
57
- }
58
- };
59
- // ---------------------------------------------------------------------------
60
- // Wrapper
61
- // ---------------------------------------------------------------------------
62
- const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: { config: MOCK_CONFIG, setConfig: vi.fn() }, children: _jsx(ModalContext.Provider, { value: { close: mockClose, open: vi.fn(), setContent: vi.fn() }, children: children }) }) }));
63
- // ---------------------------------------------------------------------------
64
- // Helpers
65
- // ---------------------------------------------------------------------------
66
- const renderModal = (records) => render(_jsx(CreateCaseModal, { records: records }), { wrapper: Wrapper });
67
- const fillCaseMetadata = async (user, { title = 'My Case', summary = 'A summary' } = {}) => {
68
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.title')), title);
69
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.summary')), summary);
70
- };
71
- // ---------------------------------------------------------------------------
72
- // Tests
73
- // ---------------------------------------------------------------------------
74
- describe('CreateCaseModal', () => {
75
- let user;
76
- beforeEach(() => {
77
- user = userEvent.setup();
78
- vi.clearAllMocks();
79
- // Default: case creation returns a case, item post resolves
80
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
81
- });
82
- // -------------------------------------------------------------------------
83
- // Initial render
84
- // -------------------------------------------------------------------------
85
- describe('initial render', () => {
86
- it('shows the modal title', () => {
87
- renderModal([MOCK_HIT_1]);
88
- expect(screen.getByText(i18n.t('modal.cases.create_case'))).toBeInTheDocument();
89
- });
90
- it('renders a cancel button', () => {
91
- renderModal([MOCK_HIT_1]);
92
- expect(screen.getByRole('button', { name: i18n.t('cancel') })).toBeInTheDocument();
93
- });
94
- it('renders a confirm button', () => {
95
- renderModal([MOCK_HIT_1]);
96
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeInTheDocument();
97
- });
98
- it('confirm button is disabled before required fields are filled', () => {
99
- renderModal([MOCK_HIT_1]);
100
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
101
- });
102
- it('renders case title, summary, and escalation inputs', () => {
103
- renderModal([MOCK_HIT_1]);
104
- expect(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.title'))).toBeInTheDocument();
105
- expect(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.summary'))).toBeInTheDocument();
106
- expect(screen.getByText(i18n.t('modal.cases.create_case.overview'))).toBeInTheDocument();
107
- });
108
- });
109
- // -------------------------------------------------------------------------
110
- // Per-record rows
111
- // -------------------------------------------------------------------------
112
- describe('per-record rows', () => {
113
- it('renders a row for each record', () => {
114
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
115
- expect(screen.getByText(MOCK_HIT_1.howler.analytic)).toBeInTheDocument();
116
- expect(screen.getByText(MOCK_HIT_2.howler.analytic)).toBeInTheDocument();
117
- });
118
- it('renders a row for an observable record', () => {
119
- renderModal([MOCK_OBSERVABLE]);
120
- expect(screen.getByText('Observable')).toBeInTheDocument();
121
- });
122
- it('pre-populates the title for a hit with analytic and id', () => {
123
- renderModal([MOCK_HIT_1]);
124
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
125
- expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
126
- });
127
- it('pre-populates the title for an observable with Observable and id', () => {
128
- renderModal([MOCK_OBSERVABLE]);
129
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
130
- expect(titleInput).toHaveValue(`Observable (${MOCK_OBSERVABLE.howler.id})`);
131
- });
132
- it('shows the alert placement section label when there are records', () => {
133
- renderModal([MOCK_HIT_1]);
134
- expect(screen.getByText(i18n.t('modal.cases.create_case.items_section'))).toBeInTheDocument();
135
- });
136
- it('does not show the alert placement section when records is empty', () => {
137
- renderModal([]);
138
- expect(screen.queryByText(i18n.t('modal.cases.create_case.items_section'))).not.toBeInTheDocument();
139
- });
140
- it('shows the full path preview when a title is set', async () => {
141
- renderModal([MOCK_HIT_1]);
142
- const expectedTitle = `${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`;
143
- await waitFor(() => {
144
- expect(screen.getByText(i18n.t('modal.cases.add_to_case.full_path', { path: expectedTitle }))).toBeInTheDocument();
145
- });
146
- });
147
- it('shows combined path/title in the full path preview', async () => {
148
- renderModal([MOCK_HIT_1]);
149
- const pathInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path'));
150
- await user.type(pathInput, 'folder');
151
- const expectedFull = `folder/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`;
152
- await waitFor(() => {
153
- expect(screen.getByText(i18n.t('modal.cases.add_to_case.full_path', { path: expectedFull }))).toBeInTheDocument();
154
- });
155
- });
156
- });
157
- // -------------------------------------------------------------------------
158
- // Validation
159
- // -------------------------------------------------------------------------
160
- describe('validation', () => {
161
- it('enables confirm after title and summary are filled', async () => {
162
- renderModal([MOCK_HIT_1]);
163
- await fillCaseMetadata(user);
164
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeEnabled();
165
- });
166
- it('disables confirm when case title is empty', async () => {
167
- renderModal([MOCK_HIT_1]);
168
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.summary')), 'A summary');
169
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
170
- });
171
- it('disables confirm when summary is empty', async () => {
172
- renderModal([MOCK_HIT_1]);
173
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.create_case.title')), 'My Case');
174
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
175
- });
176
- it('disables confirm when any item title is cleared', async () => {
177
- renderModal([MOCK_HIT_1]);
178
- await fillCaseMetadata(user);
179
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
180
- await user.clear(titleInput);
181
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
182
- });
183
- it('enables confirm without records (case-only creation)', async () => {
184
- renderModal([]);
185
- await fillCaseMetadata(user);
186
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeEnabled();
187
- });
188
- it('disables confirm when a folder path starts with /', async () => {
189
- renderModal([MOCK_HIT_1]);
190
- await fillCaseMetadata(user);
191
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path')), '/leading');
192
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
193
- });
194
- it('disables confirm when a folder path ends with /', async () => {
195
- renderModal([MOCK_HIT_1]);
196
- await fillCaseMetadata(user);
197
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path')), 'trailing/');
198
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
199
- });
200
- });
201
- // -------------------------------------------------------------------------
202
- // Cancel
203
- // -------------------------------------------------------------------------
204
- describe('cancel button', () => {
205
- it('calls close when cancel is clicked', async () => {
206
- renderModal([MOCK_HIT_1]);
207
- await user.click(screen.getByRole('button', { name: i18n.t('cancel') }));
208
- expect(mockClose).toHaveBeenCalledTimes(1);
209
- });
210
- });
211
- // -------------------------------------------------------------------------
212
- // Submission
213
- // -------------------------------------------------------------------------
214
- describe('form submission', () => {
215
- it('calls case.post with title, summary, and closes', async () => {
216
- const api = (await import('api')).default;
217
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
218
- renderModal([MOCK_HIT_1]);
219
- await fillCaseMetadata(user, { title: 'Test Case', summary: 'Test summary' });
220
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
221
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
222
- expect(api.v2.case.post).toHaveBeenCalledWith(expect.objectContaining({ title: 'Test Case', summary: 'Test summary' }));
223
- });
224
- it('includes overview in case.post when filled', async () => {
225
- const api = (await import('api')).default;
226
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
227
- renderModal([]);
228
- await fillCaseMetadata(user);
229
- act(() => mockSetContent?.('## Overview\nSome detail'));
230
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
231
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
232
- expect(api.v2.case.post).toHaveBeenCalledWith(expect.objectContaining({ overview: '## Overview\nSome detail' }));
233
- });
234
- it('does not include overview when left blank', async () => {
235
- const api = (await import('api')).default;
236
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
237
- renderModal([]);
238
- await fillCaseMetadata(user);
239
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
240
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
241
- const callArg = vi.mocked(api.v2.case.post).mock.calls[0][0];
242
- expect(callArg).not.toHaveProperty('overview');
243
- });
244
- it('includes escalation when selected', async () => {
245
- const api = (await import('api')).default;
246
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
247
- renderModal([]);
248
- await fillCaseMetadata(user);
249
- const combobox = screen.getByRole('combobox');
250
- await user.click(combobox);
251
- const option = await screen.findByRole('option', { name: 'crisis' });
252
- await user.click(option);
253
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
254
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
255
- expect(api.v2.case.post).toHaveBeenCalledWith(expect.objectContaining({ escalation: 'crisis' }));
256
- });
257
- it('calls items.post for each record after case creation', async () => {
258
- const api = (await import('api')).default;
259
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
260
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
261
- await fillCaseMetadata(user);
262
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
263
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
264
- // 1 case.post + 2 items.post
265
- expect(mockDispatchApi).toHaveBeenCalledTimes(3);
266
- expect(api.v2.case.items.post).toHaveBeenCalledTimes(2);
267
- });
268
- it('uses the default title as path for items', async () => {
269
- const api = (await import('api')).default;
270
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
271
- renderModal([MOCK_HIT_1]);
272
- await fillCaseMetadata(user);
273
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
274
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
275
- expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({
276
- path: `${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`,
277
- value: MOCK_HIT_1.howler.id,
278
- type: 'hit'
279
- }));
280
- });
281
- it('uses a custom edited item title in the path', async () => {
282
- const api = (await import('api')).default;
283
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
284
- renderModal([MOCK_HIT_1]);
285
- await fillCaseMetadata(user);
286
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
287
- await user.clear(titleInput);
288
- await user.type(titleInput, 'Custom Item Name');
289
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
290
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
291
- expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({ path: 'Custom Item Name' }));
292
- });
293
- it('combines folder path and title when a folder path is provided', async () => {
294
- const api = (await import('api')).default;
295
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
296
- renderModal([MOCK_HIT_1]);
297
- await fillCaseMetadata(user);
298
- await user.type(screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path')), 'investigations');
299
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
300
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
301
- expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({
302
- path: `investigations/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`
303
- }));
304
- });
305
- it('uses observable __index for observable records', async () => {
306
- const api = (await import('api')).default;
307
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
308
- renderModal([MOCK_OBSERVABLE]);
309
- await fillCaseMetadata(user);
310
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
311
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
312
- expect(api.v2.case.items.post).toHaveBeenCalledWith('new-case-id', expect.objectContaining({ type: 'observable' }));
313
- });
314
- it('does not call items.post when there are no records', async () => {
315
- const api = (await import('api')).default;
316
- mockDispatchApi.mockResolvedValue({ case_id: 'new-case-id' });
317
- renderModal([]);
318
- await fillCaseMetadata(user);
319
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
320
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
321
- expect(api.v2.case.items.post).not.toHaveBeenCalled();
322
- });
323
- it('does not close if case creation returns no case_id', async () => {
324
- mockDispatchApi.mockResolvedValue(null);
325
- renderModal([]);
326
- await fillCaseMetadata(user);
327
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
328
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
329
- expect(mockClose).not.toHaveBeenCalled();
330
- });
331
- });
332
- // -------------------------------------------------------------------------
333
- // Multiple records
334
- // -------------------------------------------------------------------------
335
- describe('multiple records', () => {
336
- it('renders independent title inputs for each record', () => {
337
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
338
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
339
- expect(titleInputs).toHaveLength(2);
340
- expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
341
- expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
342
- });
343
- it('editing one record title does not affect the other', async () => {
344
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
345
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
346
- await user.clear(titleInputs[0]);
347
- await user.type(titleInputs[0], 'Edited Title');
348
- expect(titleInputs[0]).toHaveValue('Edited Title');
349
- expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
350
- });
351
- it('mixed hit and observable records each get correct default titles', () => {
352
- renderModal([MOCK_HIT_1, MOCK_OBSERVABLE]);
353
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
354
- expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
355
- expect(titleInputs[1]).toHaveValue(`Observable (${MOCK_OBSERVABLE.howler.id})`);
356
- });
357
- });
358
- });
@@ -1,9 +0,0 @@
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 } from 'react';
4
- declare const RenameItemModal: FC<{
5
- _case: Case;
6
- leaf: Item;
7
- onRenamed?: (updatedCase: Case) => void;
8
- }>;
9
- export default RenameItemModal;
@@ -1,48 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Stack, TextField, Typography } from '@mui/material';
3
- import api from '@cccsaurora/howler-ui/api';
4
- import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
5
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
- import { useContext, useMemo, useState } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- const RenameItemModal = ({ _case, leaf, onRenamed }) => {
9
- const { t } = useTranslation();
10
- const { dispatchApi } = useMyApi();
11
- const { close } = useContext(ModalContext);
12
- const currentPath = leaf.path ?? '';
13
- const lastSlash = currentPath.lastIndexOf('/');
14
- const folderPrefix = lastSlash >= 0 ? currentPath.slice(0, lastSlash) : '';
15
- const currentName = lastSlash >= 0 ? currentPath.slice(lastSlash + 1) : currentPath;
16
- const [name, setName] = useState(currentName);
17
- const newPath = folderPrefix ? `${folderPrefix}/${name}` : name;
18
- const existingPaths = useMemo(() => new Set((_case.items ?? []).filter(item => item.value !== leaf.value).map(item => item.path)), [_case.items, leaf.value]);
19
- const nameError = useMemo(() => {
20
- if (!name.trim()) {
21
- return t('modal.cases.rename_item.error.empty');
22
- }
23
- if (name.includes('/')) {
24
- return t('modal.cases.rename_item.error.slash');
25
- }
26
- if (existingPaths.has(newPath)) {
27
- return t('modal.cases.rename_item.error.taken');
28
- }
29
- return null;
30
- }, [name, newPath, existingPaths, t]);
31
- const isValid = !nameError;
32
- const onSubmit = async () => {
33
- if (!isValid || !_case.case_id || !leaf.value) {
34
- return;
35
- }
36
- const updatedCase = await dispatchApi(api.v2.case.items.patch(_case.case_id, leaf.value, newPath));
37
- if (updatedCase) {
38
- onRenamed?.(updatedCase);
39
- close();
40
- }
41
- };
42
- return (_jsxs(Stack, { spacing: 2, p: 2, sx: { minWidth: 'min(600px, 60vw)' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.rename_item') }), folderPrefix && (_jsx(Typography, { variant: "body2", color: "textSecondary", children: t('modal.cases.rename_item.folder_path', { path: folderPrefix }) })), _jsx(TextField, { size: "small", label: t('modal.cases.rename_item.new_name'), value: name, onChange: ev => setName(ev.target.value), error: !!nameError, helperText: nameError ?? ' ', fullWidth: true, autoFocus: true, onKeyDown: ev => {
43
- if (ev.key === 'Enter' && isValid) {
44
- onSubmit();
45
- }
46
- } }), _jsxs(Stack, { direction: "row", justifyContent: "flex-end", spacing: 1, children: [_jsx(Button, { onClick: close, color: "error", variant: "outlined", children: t('button.cancel') }), _jsx(Button, { onClick: onSubmit, color: "success", variant: "outlined", disabled: !isValid, children: t('button.confirm') })] })] }));
47
- };
48
- export default RenameItemModal;
@@ -1,7 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const ResolveModal: FC<{
4
- case: Case;
5
- onConfirm: () => void;
6
- }>;
7
- export default ResolveModal;
@@ -1,115 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { KeyboardArrowDown, OpenInNew } from '@mui/icons-material';
3
- import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Divider, IconButton, LinearProgress, Skeleton, Stack, TextField, Typography } from '@mui/material';
4
- import api from '@cccsaurora/howler-ui/api';
5
- import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
- import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
- import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
8
- import AnalyticLink from '@cccsaurora/howler-ui/components/elements/hit/elements/AnalyticLink';
9
- import EscalationChip from '@cccsaurora/howler-ui/components/elements/hit/elements/EscalationChip';
10
- import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
11
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
12
- import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
13
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
14
- import { isNil, uniq } from 'lodash-es';
15
- import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
16
- import { useTranslation } from 'react-i18next';
17
- import { Link } from 'react-router-dom';
18
- import { useContextSelector } from 'use-context-selector';
19
- import useCase from '../hooks/useCase';
20
- const HitEntry = ({ hit, checked, onChange }) => {
21
- if (!hit) {
22
- return _jsx(Skeleton, { variant: "rounded", height: "40px", width: "100%" });
23
- }
24
- return (_jsxs(Accordion, { sx: { flexShrink: 0, px: 0, py: 0 }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), sx: {
25
- px: 1,
26
- py: 0,
27
- minHeight: '48px !important',
28
- '& > *': {
29
- margin: '0 !important'
30
- }
31
- }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, pr: 1, width: "100%", children: [!isNil(checked) && (_jsx(Checkbox, { size: "small", checked: checked, onClick: e => {
32
- onChange?.();
33
- e.preventDefault();
34
- e.stopPropagation();
35
- } })), _jsx(AnalyticLink, { hit: hit, compressed: true, alignSelf: "center" }), _jsx(EscalationChip, { hit: hit, layout: HitLayout.DENSE }), _jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, size: "small", color: "primary" }), _jsx("div", { style: { flex: 1 } }), _jsx(IconButton, { size: "small", component: Link, to: `/hits/${hit.howler.id}`, children: _jsx(OpenInNew, { fontSize: "small" }) })] }) }), _jsx(AccordionDetails, { children: _jsx(HitCard, { id: hit.howler.id, layout: HitLayout.NORMAL, elevation: 0 }) })] }, hit.howler.id));
36
- };
37
- const ResolveModal = ({ case: _case, onConfirm }) => {
38
- const { t } = useTranslation();
39
- const { dispatchApi } = useMyApi();
40
- const { close } = useContext(ModalContext);
41
- const { config } = useContext(ApiConfigContext);
42
- const { update: updateCase } = useCase({ case: _case });
43
- const [loading, setLoading] = useState(true);
44
- const [rationale, setRationale] = useState('');
45
- const [assessment, setAssessment] = useState(null);
46
- const [selectedHitIds, setSelectedHitIds] = useState(new Set());
47
- const hitIds = useMemo(() => uniq((_case?.items ?? [])
48
- .filter(item => item.type === 'hit')
49
- .map(item => item.value)
50
- .filter(Boolean)), [_case?.items]);
51
- const loadRecords = useContextSelector(RecordContext, ctx => ctx.loadRecords);
52
- const records = useContextSelector(RecordContext, ctx => ctx.records);
53
- const hits = useMemo(() => hitIds.map(id => records[id]).filter(Boolean), [hitIds, records]);
54
- const selectedHits = useMemo(() => hits.filter(hit => selectedHitIds.has(hit.howler.id)), [hits, selectedHitIds]);
55
- const { assess } = useHitActions(selectedHits);
56
- const unresolvedHits = useMemo(() => hitIds.filter(id => {
57
- const record = records[id];
58
- if (!record) {
59
- // Treat missing records as unresolved until they are loaded
60
- return true;
61
- }
62
- return record.howler.status !== 'resolved';
63
- }), [hitIds, records]);
64
- const handleConfirm = async () => {
65
- setLoading(true);
66
- try {
67
- await assess(assessment, true, rationale);
68
- setSelectedHitIds(new Set());
69
- }
70
- finally {
71
- setLoading(false);
72
- }
73
- };
74
- const handleToggleHit = useCallback((hitId) => {
75
- setSelectedHitIds(prev => {
76
- const next = new Set(prev);
77
- if (next.has(hitId)) {
78
- next.delete(hitId);
79
- }
80
- else {
81
- next.add(hitId);
82
- }
83
- return next;
84
- });
85
- }, []);
86
- useEffect(() => {
87
- (async () => {
88
- try {
89
- const result = await dispatchApi(api.search.hit.post({
90
- query: `howler.id:(${hitIds.join(' OR ')})`,
91
- metadata: ['analytic']
92
- }));
93
- loadRecords(result.items);
94
- }
95
- finally {
96
- setLoading(false);
97
- }
98
- })();
99
- }, [dispatchApi, hitIds, loadRecords]);
100
- useEffect(() => {
101
- if (loading || unresolvedHits.length > 0) {
102
- return;
103
- }
104
- updateCase({ status: 'resolved' }).then(() => {
105
- onConfirm();
106
- close();
107
- });
108
- }, [close, loading, onConfirm, unresolvedHits.length, updateCase]);
109
- return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: 'min(1000px, 60vw)', maxHeight: '100%', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.resolve') }), _jsx(Typography, { children: t('modal.cases.resolve.description') }), _jsxs(Stack, { spacing: 1, overflow: "auto", width: "100%", flex: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Box, { flex: 1, children: _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.rationale.label'), "aria-label": t('modal.rationale.label'), value: rationale, onChange: ev => setRationale(ev.target.value) }) }), _jsx(Box, { flex: 1, children: _jsx(Autocomplete, { size: "small", value: assessment, onChange: (_ev, _assessment) => setAssessment(_assessment), options: config.lookups['howler.assessment'], disablePortal: true, renderInput: params => (_jsx(TextField, { ...params, placeholder: t('hit.details.actions.assessment'), "aria-label": t('hit.details.actions.assessment'), fullWidth: true })) }) })] }), _jsxs(Stack, { position: "relative", children: [_jsx(Divider, {}), _jsx(LinearProgress, { sx: { opacity: +loading } })] }), hits
110
- .filter(hit => unresolvedHits.includes(hit.howler.id))
111
- .map(hit => (_jsx(HitEntry, { hit: hit, checked: selectedHitIds.has(hit.howler.id), onChange: () => handleToggleHit(hit.howler.id) }, hit.howler.id))), _jsxs(Accordion, { variant: "outlined", children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), children: t('modal.cases.alerts.resolved') }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 1, children: hits
112
- .filter(hit => !unresolvedHits.includes(hit.howler.id))
113
- .map(hit => (_jsx(HitEntry, { hit: hit }, hit.howler.id))) }) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: loading || !assessment || !rationale || selectedHitIds.size === 0, startIcon: loading ? _jsx(CircularProgress, { size: 16, color: "inherit" }) : undefined, onClick: handleConfirm, children: t('confirm') })] })] }));
114
- };
115
- export default ResolveModal;
@@ -1 +0,0 @@
1
- export {};