@cccsaurora/howler-ui 2.18.0-dev.758 → 2.18.0-dev.765

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 (284) 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 -100
  112. package/locales/fr/translation.json +3 -98
  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 +1 -18
  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 -3
  129. package/utils/hitFunctions.d.ts +1 -2
  130. package/utils/hitFunctions.js +4 -4
  131. package/utils/viewUtils.js +0 -3
  132. package/api/search/case.d.ts +0 -4
  133. package/api/search/case.js +0 -8
  134. package/api/v2/case/index.d.ts +0 -8
  135. package/api/v2/case/index.js +0 -20
  136. package/api/v2/case/items.d.ts +0 -6
  137. package/api/v2/case/items.js +0 -18
  138. package/api/v2/index.d.ts +0 -4
  139. package/api/v2/index.js +0 -6
  140. package/api/v2/search/facet.d.ts +0 -3
  141. package/api/v2/search/facet.js +0 -12
  142. package/api/v2/search/index.d.ts +0 -5
  143. package/api/v2/search/index.js +0 -24
  144. package/components/app/providers/RecordProvider.d.ts +0 -23
  145. package/components/elements/ContextMenu.d.ts +0 -56
  146. package/components/elements/ContextMenu.js +0 -109
  147. package/components/elements/ContextMenu.test.js +0 -215
  148. package/components/elements/ObjectDetails.d.ts +0 -6
  149. package/components/elements/case/CaseCard.d.ts +0 -12
  150. package/components/elements/case/CaseCard.js +0 -42
  151. package/components/elements/case/CasePreview.d.ts +0 -6
  152. package/components/elements/case/CasePreview.js +0 -17
  153. package/components/elements/case/StatusIcon.d.ts +0 -5
  154. package/components/elements/case/StatusIcon.js +0 -13
  155. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
  156. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  157. package/components/elements/hit/related/RelatedRecords.js +0 -63
  158. package/components/elements/observable/ObservableCard.d.ts +0 -6
  159. package/components/elements/observable/ObservableCard.js +0 -22
  160. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  161. package/components/elements/observable/ObservablePreview.js +0 -12
  162. package/components/elements/record/RecordContextMenu.js +0 -256
  163. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  164. package/components/elements/record/RecordRelated.d.ts +0 -7
  165. package/components/elements/record/RecordRelated.js +0 -34
  166. package/components/hooks/useRelatedRecords.d.ts +0 -13
  167. package/components/hooks/useRelatedRecords.js +0 -32
  168. package/components/routes/cases/CaseViewer.d.ts +0 -2
  169. package/components/routes/cases/CaseViewer.js +0 -22
  170. package/components/routes/cases/Cases.d.ts +0 -2
  171. package/components/routes/cases/Cases.js +0 -101
  172. package/components/routes/cases/constants.d.ts +0 -5
  173. package/components/routes/cases/constants.js +0 -5
  174. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  175. package/components/routes/cases/detail/AlertPanel.js +0 -33
  176. package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
  177. package/components/routes/cases/detail/CaseAssets.js +0 -104
  178. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  179. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  180. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  181. package/components/routes/cases/detail/CaseDashboard.js +0 -66
  182. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  183. package/components/routes/cases/detail/CaseDetails.js +0 -61
  184. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  185. package/components/routes/cases/detail/CaseOverview.js +0 -43
  186. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  187. package/components/routes/cases/detail/CaseSidebar.js +0 -107
  188. package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
  189. package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
  190. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  191. package/components/routes/cases/detail/CaseTask.js +0 -57
  192. package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
  193. package/components/routes/cases/detail/CaseTimeline.js +0 -106
  194. package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
  195. package/components/routes/cases/detail/CaseTimeline.test.js +0 -227
  196. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  197. package/components/routes/cases/detail/ItemPage.js +0 -99
  198. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  199. package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
  200. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  201. package/components/routes/cases/detail/TaskPanel.js +0 -52
  202. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
  203. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
  204. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  205. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
  206. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  207. package/components/routes/cases/detail/assets/Asset.js +0 -12
  208. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  209. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  210. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
  211. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
  212. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
  213. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
  214. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  215. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
  216. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  217. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
  218. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
  219. package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
  220. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
  221. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
  222. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
  223. package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
  224. package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
  225. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  226. package/components/routes/cases/detail/sidebar/utils.js +0 -29
  227. package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
  228. package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
  229. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  230. package/components/routes/cases/hooks/useCase.js +0 -51
  231. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  232. package/components/routes/cases/modals/AddToCaseModal.js +0 -59
  233. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +0 -1
  234. package/components/routes/cases/modals/AddToCaseModal.test.js +0 -313
  235. package/components/routes/cases/modals/CaseRecordRow.d.ts +0 -9
  236. package/components/routes/cases/modals/CaseRecordRow.js +0 -15
  237. package/components/routes/cases/modals/CreateCaseModal.d.ts +0 -7
  238. package/components/routes/cases/modals/CreateCaseModal.js +0 -55
  239. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +0 -1
  240. package/components/routes/cases/modals/CreateCaseModal.test.js +0 -358
  241. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  242. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  243. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  244. package/components/routes/cases/modals/ResolveModal.js +0 -115
  245. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  246. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  247. package/components/routes/cases/modals/hooks.d.ts +0 -7
  248. package/components/routes/cases/modals/hooks.js +0 -44
  249. package/components/routes/cases/modals/types.d.ts +0 -5
  250. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  251. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  252. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  253. package/components/routes/observables/ObservableViewer.js +0 -27
  254. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  255. package/models/entities/generated/Case.d.ts +0 -28
  256. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  257. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  258. package/models/entities/generated/EmailParent.d.ts +0 -19
  259. package/models/entities/generated/Enrichments.d.ts +0 -7
  260. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  261. package/models/entities/generated/HttpResponse.d.ts +0 -11
  262. package/models/entities/generated/Item.d.ts +0 -9
  263. package/models/entities/generated/Observable.d.ts +0 -85
  264. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  265. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  266. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  267. package/models/entities/generated/ObservableFile.d.ts +0 -36
  268. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  269. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  270. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  271. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  272. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  273. package/models/entities/generated/ObservableSource.d.ts +0 -23
  274. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  275. package/models/entities/generated/ObservableTls.d.ts +0 -12
  276. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  277. package/models/entities/generated/Task.d.ts +0 -10
  278. package/utils/typeUtils.d.ts +0 -7
  279. package/utils/typeUtils.js +0 -27
  280. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  281. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  282. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  283. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  284. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,313 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- /// <reference types="vitest" />
3
- import { render, screen, waitFor } from '@testing-library/react';
4
- import userEvent, {} from '@testing-library/user-event';
5
- import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
- import i18n from '@cccsaurora/howler-ui/i18n';
7
- import { I18nextProvider } from 'react-i18next';
8
- import { createMockCase, createMockHit, createMockObservable } from '@cccsaurora/howler-ui/tests/utils';
9
- import { beforeEach, describe, expect, it, vi } from 'vitest';
10
- import AddToCaseModal from './AddToCaseModal';
11
- // ---------------------------------------------------------------------------
12
- // Hoisted mocks
13
- // ---------------------------------------------------------------------------
14
- const mockDispatchApi = vi.hoisted(() => vi.fn());
15
- const mockClose = vi.hoisted(() => vi.fn());
16
- vi.mock('components/hooks/useMyApi', () => ({
17
- default: () => ({ dispatchApi: mockDispatchApi })
18
- }));
19
- vi.mock('components/elements/hit/elements/EscalationChip', () => ({
20
- default: () => null
21
- }));
22
- vi.mock('components/elements/case/CaseCard', () => ({
23
- default: ({ case: c }) => _jsx("div", { children: c.title ?? c.case_id })
24
- }));
25
- vi.mock('api', () => ({
26
- default: {
27
- search: {
28
- case: { post: vi.fn().mockReturnValue('search-case-request') }
29
- },
30
- v2: {
31
- case: {
32
- items: { post: vi.fn().mockReturnValue('items-post-request') }
33
- }
34
- }
35
- }
36
- }));
37
- // ---------------------------------------------------------------------------
38
- // Fixtures
39
- // ---------------------------------------------------------------------------
40
- const CASE_A = createMockCase({ case_id: 'case-a', title: 'Case Alpha', items: [] });
41
- const CASE_B = createMockCase({
42
- case_id: 'case-b',
43
- title: 'Case Beta',
44
- items: [{ path: 'folder/subfolder/item', type: 'hit', value: 'x', visible: true }]
45
- });
46
- const MOCK_HIT_1 = createMockHit({
47
- howler: { id: 'hit-001', analytic: 'AnalyticOne', status: 'open' }
48
- });
49
- const MOCK_HIT_2 = createMockHit({
50
- howler: { id: 'hit-002', analytic: 'AnalyticTwo', status: 'open' }
51
- });
52
- const MOCK_OBSERVABLE = createMockObservable({
53
- howler: { id: 'obs-001' }
54
- });
55
- // ---------------------------------------------------------------------------
56
- // Wrapper
57
- // ---------------------------------------------------------------------------
58
- const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ModalContext.Provider, { value: { close: mockClose, open: vi.fn(), setContent: vi.fn() }, children: children }) }));
59
- // ---------------------------------------------------------------------------
60
- // Helpers
61
- // ---------------------------------------------------------------------------
62
- const renderModal = (records) => render(_jsx(AddToCaseModal, { records: records }), { wrapper: Wrapper });
63
- const selectCase = async (user, caseTitle) => {
64
- const combobox = screen.getAllByRole('combobox')[0];
65
- await user.click(combobox);
66
- const option = await screen.findByRole('option', { name: caseTitle });
67
- await user.click(option);
68
- };
69
- // ---------------------------------------------------------------------------
70
- // Tests
71
- // ---------------------------------------------------------------------------
72
- describe('AddToCaseModal', () => {
73
- let user;
74
- beforeEach(() => {
75
- user = userEvent.setup();
76
- vi.clearAllMocks();
77
- mockDispatchApi.mockResolvedValue({ items: [CASE_A, CASE_B] });
78
- });
79
- // -------------------------------------------------------------------------
80
- // Initial render
81
- // -------------------------------------------------------------------------
82
- describe('initial render', () => {
83
- it('shows the modal title', async () => {
84
- renderModal([MOCK_HIT_1]);
85
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
86
- expect(screen.getByText(i18n.t('modal.cases.add_to_case'))).toBeInTheDocument();
87
- });
88
- it('renders cancel and confirm buttons', async () => {
89
- renderModal([MOCK_HIT_1]);
90
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
91
- expect(screen.getByRole('button', { name: i18n.t('cancel') })).toBeInTheDocument();
92
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeInTheDocument();
93
- });
94
- it('confirm is disabled before a case is selected', async () => {
95
- renderModal([MOCK_HIT_1]);
96
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
97
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
98
- });
99
- it('fetches cases on mount', async () => {
100
- const api = (await import('api')).default;
101
- renderModal([MOCK_HIT_1]);
102
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
103
- expect(api.search.case.post).toHaveBeenCalledWith({ query: 'case_id:*', rows: 100 });
104
- });
105
- });
106
- // -------------------------------------------------------------------------
107
- // Default item titles
108
- // -------------------------------------------------------------------------
109
- describe('default item titles', () => {
110
- it('pre-populates title for a hit with analytic and id', async () => {
111
- renderModal([MOCK_HIT_1]);
112
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
113
- await selectCase(user, 'Case Alpha');
114
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
115
- expect(titleInput).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
116
- });
117
- it('pre-populates title for an observable with Observable and id', async () => {
118
- renderModal([MOCK_OBSERVABLE]);
119
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
120
- await selectCase(user, 'Case Alpha');
121
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
122
- expect(titleInput).toHaveValue(`Observable (${MOCK_OBSERVABLE.howler.id})`);
123
- });
124
- it('title input is editable', async () => {
125
- renderModal([MOCK_HIT_1]);
126
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
127
- await selectCase(user, 'Case Alpha');
128
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
129
- await user.clear(titleInput);
130
- await user.type(titleInput, 'Custom Title');
131
- expect(titleInput).toHaveValue('Custom Title');
132
- });
133
- });
134
- // -------------------------------------------------------------------------
135
- // Multiple records
136
- // -------------------------------------------------------------------------
137
- describe('multiple records', () => {
138
- it('renders a row for each hit record', async () => {
139
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
140
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
141
- await selectCase(user, 'Case Alpha');
142
- expect(screen.getByText(MOCK_HIT_1.howler.analytic)).toBeInTheDocument();
143
- expect(screen.getByText(MOCK_HIT_2.howler.analytic)).toBeInTheDocument();
144
- });
145
- it('renders independent title inputs for each record', async () => {
146
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
147
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
148
- await selectCase(user, 'Case Alpha');
149
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
150
- expect(titleInputs).toHaveLength(2);
151
- expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
152
- expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
153
- });
154
- it('editing one record title does not affect the other', async () => {
155
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
156
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
157
- await selectCase(user, 'Case Alpha');
158
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
159
- await user.clear(titleInputs[0]);
160
- await user.type(titleInputs[0], 'Edited');
161
- expect(titleInputs[0]).toHaveValue('Edited');
162
- expect(titleInputs[1]).toHaveValue(`${MOCK_HIT_2.howler.analytic} (${MOCK_HIT_2.howler.id})`);
163
- });
164
- it('mixed hit and observable records each get correct default titles', async () => {
165
- renderModal([MOCK_HIT_1, MOCK_OBSERVABLE]);
166
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
167
- await selectCase(user, 'Case Alpha');
168
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
169
- expect(titleInputs[0]).toHaveValue(`${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`);
170
- expect(titleInputs[1]).toHaveValue(`Observable (${MOCK_OBSERVABLE.howler.id})`);
171
- });
172
- });
173
- // -------------------------------------------------------------------------
174
- // Folder path options
175
- // -------------------------------------------------------------------------
176
- describe('folder path options', () => {
177
- it('shows folder options derived from the selected case items', async () => {
178
- renderModal([MOCK_HIT_1]);
179
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
180
- await selectCase(user, 'Case Beta');
181
- const pathInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path'));
182
- await user.click(pathInputs[0]);
183
- expect(await screen.findByRole('option', { name: 'folder' })).toBeInTheDocument();
184
- expect(screen.getByRole('option', { name: 'folder/subfolder' })).toBeInTheDocument();
185
- });
186
- it('shows the full path preview when a path and title are set', async () => {
187
- renderModal([MOCK_HIT_1]);
188
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
189
- await selectCase(user, 'Case Beta');
190
- const pathInput = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path'))[0];
191
- await user.type(pathInput, 'myfolder');
192
- const expectedFull = `myfolder/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`;
193
- await waitFor(() => {
194
- expect(screen.getByText(i18n.t('modal.cases.add_to_case.full_path', { path: expectedFull }))).toBeInTheDocument();
195
- });
196
- });
197
- });
198
- // -------------------------------------------------------------------------
199
- // Validation
200
- // -------------------------------------------------------------------------
201
- describe('validation', () => {
202
- it('enables confirm after a case is selected and titles are filled', async () => {
203
- renderModal([MOCK_HIT_1]);
204
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
205
- await selectCase(user, 'Case Alpha');
206
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeEnabled();
207
- });
208
- it('disables confirm when an item title is cleared', async () => {
209
- renderModal([MOCK_HIT_1]);
210
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
211
- await selectCase(user, 'Case Alpha');
212
- const titleInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
213
- await user.clear(titleInput);
214
- expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
215
- });
216
- });
217
- // -------------------------------------------------------------------------
218
- // Cancel
219
- // -------------------------------------------------------------------------
220
- describe('cancel button', () => {
221
- it('calls close when cancel is clicked', async () => {
222
- renderModal([MOCK_HIT_1]);
223
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
224
- await user.click(screen.getByRole('button', { name: i18n.t('cancel') }));
225
- expect(mockClose).toHaveBeenCalledTimes(1);
226
- });
227
- });
228
- // -------------------------------------------------------------------------
229
- // Submission — single record
230
- // -------------------------------------------------------------------------
231
- describe('form submission — single record', () => {
232
- beforeEach(() => {
233
- mockDispatchApi
234
- .mockResolvedValueOnce({ items: [CASE_A, CASE_B] }) // case list fetch
235
- .mockResolvedValue(undefined); // items.post
236
- });
237
- it('calls items.post with the correct arguments and closes', async () => {
238
- const api = (await import('api')).default;
239
- renderModal([MOCK_HIT_1]);
240
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
241
- await selectCase(user, 'Case Alpha');
242
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
243
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
244
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({
245
- path: `${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`,
246
- value: MOCK_HIT_1.howler.id,
247
- type: 'hit'
248
- }));
249
- });
250
- it('combines folder path and title in the submitted path', async () => {
251
- const api = (await import('api')).default;
252
- renderModal([MOCK_HIT_1]);
253
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
254
- await selectCase(user, 'Case Alpha');
255
- const pathInput = screen.getByPlaceholderText(i18n.t('modal.cases.add_to_case.select_path'));
256
- await user.type(pathInput, 'investigations');
257
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
258
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
259
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({
260
- path: `investigations/${MOCK_HIT_1.howler.analytic} (${MOCK_HIT_1.howler.id})`
261
- }));
262
- });
263
- it('uses observable __index for observable records', async () => {
264
- const api = (await import('api')).default;
265
- renderModal([MOCK_OBSERVABLE]);
266
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
267
- await selectCase(user, 'Case Alpha');
268
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
269
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
270
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ type: 'observable' }));
271
- });
272
- });
273
- // -------------------------------------------------------------------------
274
- // Submission — multiple records
275
- // -------------------------------------------------------------------------
276
- describe('form submission — multiple records', () => {
277
- beforeEach(() => {
278
- mockDispatchApi.mockResolvedValueOnce({ items: [CASE_A, CASE_B] }).mockResolvedValue(undefined);
279
- });
280
- it('calls items.post once per record', async () => {
281
- const api = (await import('api')).default;
282
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
283
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
284
- await selectCase(user, 'Case Alpha');
285
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
286
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
287
- expect(api.v2.case.items.post).toHaveBeenCalledTimes(2);
288
- });
289
- it('submits the correct value for each record', async () => {
290
- const api = (await import('api')).default;
291
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
292
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
293
- await selectCase(user, 'Case Alpha');
294
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
295
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
296
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ value: MOCK_HIT_1.howler.id }));
297
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ value: MOCK_HIT_2.howler.id }));
298
- });
299
- it('uses an independently edited title for each record', async () => {
300
- const api = (await import('api')).default;
301
- renderModal([MOCK_HIT_1, MOCK_HIT_2]);
302
- await waitFor(() => expect(mockDispatchApi).toHaveBeenCalled());
303
- await selectCase(user, 'Case Alpha');
304
- const titleInputs = screen.getAllByPlaceholderText(i18n.t('modal.cases.add_to_case.title'));
305
- await user.clear(titleInputs[0]);
306
- await user.type(titleInputs[0], 'First Item');
307
- await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
308
- await waitFor(() => expect(mockClose).toHaveBeenCalled());
309
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ path: 'First Item', value: MOCK_HIT_1.howler.id }));
310
- expect(api.v2.case.items.post).toHaveBeenCalledWith('case-a', expect.objectContaining({ value: MOCK_HIT_2.howler.id }));
311
- });
312
- });
313
- });
@@ -1,9 +0,0 @@
1
- import type { FC } from 'react';
2
- import type { RecordEntry } from './types';
3
- declare const CaseRecordRow: FC<{
4
- entry: RecordEntry;
5
- folderOptions?: string[];
6
- onTitleChange: (title: string) => void;
7
- onPathChange: (path: string) => void;
8
- }>;
9
- export default CaseRecordRow;
@@ -1,15 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { KeyboardArrowDown } from '@mui/icons-material';
3
- import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Chip, Stack, TextField, Typography } from '@mui/material';
4
- import EscalationChip from '@cccsaurora/howler-ui/components/elements/hit/elements/EscalationChip';
5
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
6
- import { useTranslation } from 'react-i18next';
7
- import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
8
- const CaseRecordRow = ({ entry, folderOptions = [], onTitleChange, onPathChange }) => {
9
- const { t } = useTranslation();
10
- const { record, path, title } = entry;
11
- const fullPath = path ? `${path}/${title}` : title;
12
- const pathError = path.startsWith('/') || path.endsWith('/');
13
- return (_jsxs(Accordion, { variant: "outlined", defaultExpanded: true, sx: { flexShrink: 0 }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, {}), sx: { px: 1, minHeight: '48px !important', '& > *': { margin: '0 !important' } }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, width: "100%", children: [_jsx(Typography, { variant: "body2", fontWeight: 500, sx: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: isHit(record) ? record.howler.analytic : 'Observable' }), isHit(record) && _jsx(EscalationChip, { hit: record, layout: HitLayout.DENSE }), isHit(record) && _jsx(Chip, { label: record.howler.status, size: "small", color: "primary", sx: { flexShrink: 0 } }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { flexShrink: 0 }, children: record.howler.id })] }) }), _jsx(AccordionDetails, { children: _jsxs(Stack, { spacing: 1, children: [_jsx(Autocomplete, { freeSolo: true, disablePortal: true, options: folderOptions, value: path, onInputChange: (_ev, newVal) => onPathChange(newVal), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_path'), fullWidth: true, error: pathError, helperText: pathError ? t('modal.cases.add_to_case.path_invalid') : undefined })) }), _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.cases.add_to_case.title'), value: title, onChange: ev => onTitleChange(ev.target.value) }), title && (_jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.add_to_case.full_path', { path: fullPath }) }))] }) })] }));
14
- };
15
- export default CaseRecordRow;
@@ -1,7 +0,0 @@
1
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
- import { type FC } from 'react';
4
- declare const CreateCaseModal: FC<{
5
- records: (Hit | Observable)[];
6
- }>;
7
- export default CreateCaseModal;
@@ -1,55 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Autocomplete, Button, CircularProgress, Divider, 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 MarkdownEditor from '@cccsaurora/howler-ui/components/elements/MarkdownEditor';
6
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
- import { useContext, useMemo, useState } from 'react';
8
- import { useTranslation } from 'react-i18next';
9
- import CaseRecordRow from './CaseRecordRow';
10
- import { useRecordEntries } from './hooks';
11
- const ESCALATIONS = ['normal', 'focus', 'crisis'];
12
- const CreateCaseModal = ({ records }) => {
13
- const { t } = useTranslation();
14
- const { dispatchApi } = useMyApi();
15
- const { close } = useContext(ModalContext);
16
- const [caseTitle, setCaseTitle] = useState('');
17
- const [summary, setSummary] = useState('');
18
- const [overview, setOverview] = useState('');
19
- const [escalation, setEscalation] = useState(null);
20
- const [submitting, setSubmitting] = useState(false);
21
- const [entries, updateEntry] = useRecordEntries(records);
22
- const isValid = useMemo(() => !!caseTitle.trim() &&
23
- !!summary.trim() &&
24
- entries.every(e => !!e.title.trim() && !e.path.startsWith('/') && !e.path.endsWith('/')), [caseTitle, summary, entries]);
25
- const onSubmit = async () => {
26
- if (!isValid) {
27
- return;
28
- }
29
- setSubmitting(true);
30
- try {
31
- const newCase = await dispatchApi(api.v2.case.post({
32
- title: caseTitle.trim(),
33
- summary: summary.trim(),
34
- ...(overview.trim() ? { overview: overview.trim() } : {}),
35
- ...(escalation ? { escalation } : {})
36
- }));
37
- if (newCase?.case_id) {
38
- for (const entry of entries) {
39
- const fullPath = entry.path ? `${entry.path}/${entry.title}` : entry.title;
40
- await dispatchApi(api.v2.case.items.post(newCase.case_id, {
41
- path: fullPath,
42
- value: entry.record.howler.id,
43
- type: entry.record.__index
44
- }));
45
- }
46
- close();
47
- }
48
- }
49
- finally {
50
- setSubmitting(false);
51
- }
52
- };
53
- return (_jsxs(Stack, { spacing: 2, p: 2, sx: { minWidth: 'min(800px, 60vw)', maxHeight: '90vh', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.create_case') }), _jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.cases.create_case.title'), value: caseTitle, onChange: ev => setCaseTitle(ev.target.value) }), _jsx(TextField, { size: "small", fullWidth: true, placeholder: t('modal.cases.create_case.summary'), value: summary, onChange: ev => setSummary(ev.target.value) }), _jsx(Autocomplete, { options: ESCALATIONS, value: escalation, disablePortal: true, onChange: (_ev, val) => setEscalation(val), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.create_case.escalation'), fullWidth: true })) }), _jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.create_case.overview') }), _jsx(MarkdownEditor, { content: overview, setContent: setOverview, height: "200px", fontSize: 14 })] })] }), entries.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Divider, { children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.create_case.items_section') }) }), _jsx(Stack, { spacing: 1, overflow: "auto", flex: 1, children: entries.map((entry, i) => (_jsx(CaseRecordRow, { entry: entry, onTitleChange: val => updateEntry(i, 'title', val), onPathChange: val => updateEntry(i, 'path', val) }, entry.record.howler.id))) })] })) : (_jsx("div", { style: { flex: 1 } })), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, disabled: submitting, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: !isValid || submitting, startIcon: submitting ? _jsx(CircularProgress, { size: 16, color: "inherit" }) : undefined, onClick: onSubmit, children: t('confirm') })] })] }));
54
- };
55
- export default CreateCaseModal;