@cccsaurora/howler-ui 2.18.0-dev.736 → 2.18.0-dev.737

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 (273) 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 +7 -39
  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} +70 -51
  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/HitOutline.d.ts +0 -1
  44. package/components/elements/hit/HitOutline.js +3 -3
  45. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  46. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  47. package/components/elements/hit/HitRelated.d.ts +6 -0
  48. package/components/elements/hit/HitRelated.js +7 -0
  49. package/components/elements/hit/HitSummary.d.ts +1 -2
  50. package/components/elements/hit/HitSummary.js +5 -6
  51. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  52. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  53. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  54. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  55. package/components/elements/view/ViewTitle.d.ts +0 -1
  56. package/components/elements/view/ViewTitle.js +2 -9
  57. package/components/hooks/useHitActions.d.ts +1 -1
  58. package/components/hooks/useHitActions.js +4 -4
  59. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  60. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  61. package/components/hooks/useMyPreferences.js +1 -10
  62. package/components/hooks/useMySearch.js +2 -2
  63. package/components/hooks/useMySitemap.js +1 -4
  64. package/components/hooks/useMyTheme.js +2 -9
  65. package/components/hooks/useParamState.test.js +4 -3
  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 -89
  112. package/locales/fr/translation.json +3 -87
  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 +2 -19
  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 -247
  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 -62
  233. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  234. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  235. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  236. package/components/routes/cases/modals/ResolveModal.js +0 -115
  237. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  238. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  239. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  240. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  241. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  242. package/components/routes/observables/ObservableViewer.js +0 -27
  243. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  244. package/models/entities/generated/Case.d.ts +0 -28
  245. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  246. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  247. package/models/entities/generated/EmailParent.d.ts +0 -19
  248. package/models/entities/generated/Enrichments.d.ts +0 -7
  249. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  250. package/models/entities/generated/HttpResponse.d.ts +0 -11
  251. package/models/entities/generated/Item.d.ts +0 -9
  252. package/models/entities/generated/Observable.d.ts +0 -85
  253. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  254. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  255. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  256. package/models/entities/generated/ObservableFile.d.ts +0 -36
  257. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  258. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  259. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  260. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  261. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  262. package/models/entities/generated/ObservableSource.d.ts +0 -23
  263. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  264. package/models/entities/generated/ObservableTls.d.ts +0 -12
  265. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  266. package/models/entities/generated/Task.d.ts +0 -10
  267. package/utils/typeUtils.d.ts +0 -7
  268. package/utils/typeUtils.js +0 -27
  269. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  270. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  271. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  272. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  273. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,363 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
- import { act } from 'react';
4
- import { beforeEach, describe, expect, it, vi } from 'vitest';
5
- // ---------------------------------------------------------------------------
6
- // Mocks
7
- // ---------------------------------------------------------------------------
8
- vi.mock('components/elements/ContextMenu', () => ({
9
- default: ({ items, children }) => (_jsxs("div", { children: [children, items.map((item) => {
10
- if (item.kind === 'item') {
11
- return (_jsx("button", { id: item.id, onClick: item.onClick, children: item.label }, item.id));
12
- }
13
- if (item.kind === 'divider') {
14
- return _jsx("hr", {}, item.id);
15
- }
16
- return null;
17
- })] }))
18
- }));
19
- const mockDispatchApi = vi.hoisted(() => vi.fn());
20
- vi.mock('components/hooks/useMyApi', () => ({
21
- default: () => ({ dispatchApi: mockDispatchApi })
22
- }));
23
- const mockDel = vi.hoisted(() => vi.fn());
24
- const mockPatch = vi.hoisted(() => vi.fn());
25
- vi.mock('api', () => ({
26
- default: {
27
- v2: {
28
- case: {
29
- items: {
30
- del: (...args) => mockDel(...args),
31
- patch: (...args) => mockPatch(...args)
32
- }
33
- }
34
- }
35
- }
36
- }));
37
- const mockShowModal = vi.hoisted(() => vi.fn());
38
- vi.mock('components/app/providers/ModalProvider', async () => {
39
- const { createContext } = await import('react');
40
- return {
41
- ModalContext: createContext({ showModal: mockShowModal, close: vi.fn(), setContent: vi.fn() })
42
- };
43
- });
44
- vi.mock('components/routes/cases/modals/RenameItemModal', () => ({
45
- default: () => _jsx("div", { id: "rename-item-modal" })
46
- }));
47
- // ---------------------------------------------------------------------------
48
- // Imports (after mocks so that module registry picks up the stubs)
49
- // ---------------------------------------------------------------------------
50
- import CaseFolderContextMenu, { collectAllLeaves, getOpenUrl } from './CaseFolderContextMenu';
51
- // ---------------------------------------------------------------------------
52
- // Fixtures
53
- // ---------------------------------------------------------------------------
54
- const mockCase = { case_id: 'case-1', title: 'Test Case', items: [] };
55
- const hitLeaf = { type: 'hit', value: 'hit-123', path: 'folder/hit-item' };
56
- const referenceLeaf = { type: 'reference', value: 'https://example.com', path: 'folder/ref-item' };
57
- const observableLeaf = { type: 'observable', value: 'obs-456', path: 'folder/obs-item' };
58
- const caseLeaf = { type: 'case', value: 'nested-case-id', path: 'folder/case-item' };
59
- const tableLeaf = { type: 'table', value: 'table-789', path: 'folder/table-item' };
60
- const leadLeaf = { type: 'lead', value: 'lead-999', path: 'folder/lead-item' };
61
- const renderMenu = (props) => render(_jsx(CaseFolderContextMenu, { _case: mockCase, ...props, children: _jsx("div", { id: "child", children: "child" }) }));
62
- // ---------------------------------------------------------------------------
63
- // Setup
64
- // ---------------------------------------------------------------------------
65
- beforeEach(() => {
66
- mockDel.mockClear();
67
- mockPatch.mockClear();
68
- mockDispatchApi.mockClear();
69
- mockShowModal.mockClear();
70
- mockDispatchApi.mockImplementation((p) => p);
71
- mockDel.mockResolvedValue(mockCase);
72
- mockPatch.mockResolvedValue(mockCase);
73
- vi.spyOn(window, 'open').mockReturnValue(null);
74
- });
75
- // ---------------------------------------------------------------------------
76
- // Unit tests for exported utilities
77
- // ---------------------------------------------------------------------------
78
- describe('collectAllLeaves', () => {
79
- it('returns leaves at the root level', () => {
80
- const tree = { path: '', leaves: [hitLeaf, referenceLeaf] };
81
- expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
82
- });
83
- it('returns leaves from nested subfolders', () => {
84
- const tree = {
85
- path: '',
86
- leaves: [hitLeaf],
87
- folders: {
88
- subfolder: { path: 'subfolder', leaves: [referenceLeaf] }
89
- }
90
- };
91
- expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
92
- });
93
- it('returns leaves from deeply nested subfolders', () => {
94
- const tree = {
95
- path: '',
96
- leaves: [],
97
- folders: {
98
- level1: {
99
- path: 'level1',
100
- leaves: [hitLeaf],
101
- folders: {
102
- level2: { path: 'level1/level2', leaves: [referenceLeaf] }
103
- }
104
- }
105
- }
106
- };
107
- const result = collectAllLeaves(tree);
108
- expect(result).toContain(hitLeaf);
109
- expect(result).toContain(referenceLeaf);
110
- });
111
- it('returns an empty array for an empty tree', () => {
112
- expect(collectAllLeaves({ path: '', leaves: [] })).toEqual([]);
113
- });
114
- });
115
- describe('getOpenUrl', () => {
116
- it('returns the value directly for a reference item', () => {
117
- expect(getOpenUrl(referenceLeaf)).toBe('https://example.com');
118
- });
119
- it('returns /hits/<id> for a hit item', () => {
120
- expect(getOpenUrl(hitLeaf)).toBe('/hits/hit-123');
121
- });
122
- it('returns /observables/<id> for an observable item', () => {
123
- expect(getOpenUrl(observableLeaf)).toBe('/observables/obs-456');
124
- });
125
- it('returns /cases/<id> for a case item', () => {
126
- expect(getOpenUrl(caseLeaf)).toBe('/cases/nested-case-id');
127
- });
128
- it('returns null for a table item', () => {
129
- expect(getOpenUrl(tableLeaf)).toBeNull();
130
- });
131
- it('returns null for a lead item', () => {
132
- expect(getOpenUrl(leadLeaf)).toBeNull();
133
- });
134
- it('returns null when value is undefined', () => {
135
- expect(getOpenUrl({ type: 'hit' })).toBeNull();
136
- });
137
- it('returns null when type is undefined', () => {
138
- expect(getOpenUrl({ value: 'something' })).toBeNull();
139
- });
140
- });
141
- // ---------------------------------------------------------------------------
142
- // Component tests
143
- // ---------------------------------------------------------------------------
144
- describe('CaseFolderContextMenu', () => {
145
- describe('renders children', () => {
146
- it('renders children content', () => {
147
- renderMenu({ leaf: hitLeaf });
148
- expect(screen.getByTestId('child')).toBeInTheDocument();
149
- });
150
- });
151
- describe('menu items for leaf types', () => {
152
- it('shows "Open item" and "Remove item" for a hit leaf', () => {
153
- renderMenu({ leaf: hitLeaf });
154
- expect(screen.getByTestId('open-item')).toBeInTheDocument();
155
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
156
- });
157
- it('shows "Open item" and "Remove item" for a reference leaf', () => {
158
- renderMenu({ leaf: referenceLeaf });
159
- expect(screen.getByTestId('open-item')).toBeInTheDocument();
160
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
161
- });
162
- it('shows "Open item" and "Remove item" for an observable leaf', () => {
163
- renderMenu({ leaf: observableLeaf });
164
- expect(screen.getByTestId('open-item')).toBeInTheDocument();
165
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
166
- });
167
- it('shows "Open item" and "Remove item" for a case leaf', () => {
168
- renderMenu({ leaf: caseLeaf });
169
- expect(screen.getByTestId('open-item')).toBeInTheDocument();
170
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
171
- });
172
- it('shows only "Remove item" for a table leaf (no open URL)', () => {
173
- renderMenu({ leaf: tableLeaf });
174
- expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
175
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
176
- });
177
- it('shows only "Remove item" for a lead leaf (no open URL)', () => {
178
- renderMenu({ leaf: leadLeaf });
179
- expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
180
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
181
- });
182
- it('labels the remove button "Remove item" for a leaf', () => {
183
- renderMenu({ leaf: hitLeaf });
184
- expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.item.remove');
185
- });
186
- it('shows a divider for all leaf types (between leaf actions and remove)', () => {
187
- const { container: withOpen } = renderMenu({ leaf: hitLeaf });
188
- expect(withOpen.querySelector('hr')).not.toBeNull();
189
- const { container: withoutOpen } = renderMenu({ leaf: tableLeaf });
190
- expect(withoutOpen.querySelector('hr')).not.toBeNull();
191
- const { container: withFolder } = renderMenu({ tree: { path: 'folder', leaves: [hitLeaf] } });
192
- expect(withFolder.querySelector('hr')).toBeNull();
193
- });
194
- });
195
- describe('menu items for folders', () => {
196
- const folderTree = { path: 'folder', leaves: [hitLeaf, referenceLeaf] };
197
- it('shows only "Remove folder" for a folder (no open URL)', () => {
198
- renderMenu({ tree: folderTree });
199
- expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
200
- expect(screen.getByTestId('remove-item')).toBeInTheDocument();
201
- });
202
- it('labels the remove button "Remove folder" for a tree', () => {
203
- renderMenu({ tree: folderTree });
204
- expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.folder.remove');
205
- });
206
- });
207
- describe('"Open item" action', () => {
208
- it('calls window.open with the hit URL', () => {
209
- renderMenu({ leaf: hitLeaf });
210
- act(() => {
211
- fireEvent.click(screen.getByTestId('open-item'));
212
- });
213
- expect(window.open).toHaveBeenCalledWith('/hits/hit-123', '_blank', 'noopener noreferrer');
214
- });
215
- it('calls window.open with the reference URL directly', () => {
216
- renderMenu({ leaf: referenceLeaf });
217
- act(() => {
218
- fireEvent.click(screen.getByTestId('open-item'));
219
- });
220
- expect(window.open).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener noreferrer');
221
- });
222
- it('calls window.open with the observable URL', () => {
223
- renderMenu({ leaf: observableLeaf });
224
- act(() => {
225
- fireEvent.click(screen.getByTestId('open-item'));
226
- });
227
- expect(window.open).toHaveBeenCalledWith('/observables/obs-456', '_blank', 'noopener noreferrer');
228
- });
229
- it('calls window.open with the case URL', () => {
230
- renderMenu({ leaf: caseLeaf });
231
- act(() => {
232
- fireEvent.click(screen.getByTestId('open-item'));
233
- });
234
- expect(window.open).toHaveBeenCalledWith('/cases/nested-case-id', '_blank', 'noopener noreferrer');
235
- });
236
- });
237
- describe('"Remove item" action for a leaf', () => {
238
- it('calls dispatchApi with the delete call for the leaf', async () => {
239
- renderMenu({ leaf: hitLeaf });
240
- act(() => {
241
- fireEvent.click(screen.getByTestId('remove-item'));
242
- });
243
- await waitFor(() => {
244
- expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123']);
245
- });
246
- });
247
- it('calls onUpdate with the updated case after the delete resolves', async () => {
248
- const onUpdate = vi.fn();
249
- renderMenu({ leaf: hitLeaf, onUpdate: onUpdate });
250
- act(() => {
251
- fireEvent.click(screen.getByTestId('remove-item'));
252
- });
253
- await waitFor(() => {
254
- expect(onUpdate).toHaveBeenCalledWith(mockCase);
255
- });
256
- });
257
- it('does not call the API when case_id is missing', () => {
258
- renderMenu({ _case: { title: 'No ID' }, leaf: hitLeaf });
259
- act(() => {
260
- fireEvent.click(screen.getByTestId('remove-item'));
261
- });
262
- expect(mockDel).not.toHaveBeenCalled();
263
- });
264
- it('skips items with no value', async () => {
265
- const noValueLeaf = { type: 'hit', path: 'folder/no-value' };
266
- renderMenu({ leaf: noValueLeaf });
267
- act(() => {
268
- fireEvent.click(screen.getByTestId('remove-item'));
269
- });
270
- await waitFor(() => {
271
- expect(mockDel).not.toHaveBeenCalled();
272
- });
273
- });
274
- });
275
- describe('"Rename item" action', () => {
276
- it('shows "Rename item" entry for a hit leaf', () => {
277
- renderMenu({ leaf: hitLeaf });
278
- expect(screen.getByTestId('rename-item')).toBeInTheDocument();
279
- });
280
- it('shows "Rename item" for a table leaf', () => {
281
- renderMenu({ leaf: tableLeaf });
282
- expect(screen.getByTestId('rename-item')).toBeInTheDocument();
283
- });
284
- it('does not show "Rename item" for a folder', () => {
285
- renderMenu({ tree: { path: 'folder', leaves: [hitLeaf] } });
286
- expect(screen.queryByTestId('rename-item')).not.toBeInTheDocument();
287
- });
288
- it('calls showModal when "Rename item" is clicked', () => {
289
- renderMenu({ leaf: hitLeaf });
290
- act(() => {
291
- fireEvent.click(screen.getByTestId('rename-item'));
292
- });
293
- expect(mockShowModal).toHaveBeenCalledTimes(1);
294
- });
295
- it('passes the current case and leaf to the rename modal', () => {
296
- const onUpdate = vi.fn();
297
- renderMenu({ leaf: hitLeaf, onUpdate: onUpdate });
298
- act(() => {
299
- fireEvent.click(screen.getByTestId('rename-item'));
300
- });
301
- const [modalElement] = mockShowModal.mock.calls[0];
302
- expect(modalElement.props._case).toBe(mockCase);
303
- expect(modalElement.props.leaf).toBe(hitLeaf);
304
- });
305
- it('works fine when onUpdate is not provided', () => {
306
- renderMenu({ leaf: hitLeaf });
307
- act(() => {
308
- fireEvent.click(screen.getByTestId('rename-item'));
309
- });
310
- expect(mockShowModal).toHaveBeenCalledTimes(1);
311
- });
312
- });
313
- describe('"Remove folder" action', () => {
314
- it('calls dispatchApi with all leaf values in a single batch call', async () => {
315
- const folderTree = { path: 'folder', leaves: [hitLeaf, referenceLeaf] };
316
- renderMenu({ tree: folderTree });
317
- act(() => {
318
- fireEvent.click(screen.getByTestId('remove-item'));
319
- });
320
- await waitFor(() => {
321
- expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123', 'https://example.com']);
322
- expect(mockDel).toHaveBeenCalledTimes(1);
323
- });
324
- });
325
- it('calls dispatchApi with leaves from nested subfolders in a single batch call', async () => {
326
- const nestedTree = {
327
- path: 'folder',
328
- leaves: [hitLeaf],
329
- folders: {
330
- subfolder: { path: 'folder/subfolder', leaves: [referenceLeaf] }
331
- }
332
- };
333
- renderMenu({ tree: nestedTree });
334
- act(() => {
335
- fireEvent.click(screen.getByTestId('remove-item'));
336
- });
337
- await waitFor(() => {
338
- expect(mockDel).toHaveBeenCalledWith('case-1', expect.arrayContaining(['hit-123', 'https://example.com']));
339
- expect(mockDel).toHaveBeenCalledTimes(1);
340
- });
341
- });
342
- it('calls onUpdate with the updated case after deletion', async () => {
343
- const onUpdate = vi.fn();
344
- const folderTree = { path: 'folder', leaves: [hitLeaf, referenceLeaf] };
345
- renderMenu({ tree: folderTree, onUpdate: onUpdate });
346
- act(() => {
347
- fireEvent.click(screen.getByTestId('remove-item'));
348
- });
349
- await waitFor(() => {
350
- expect(onUpdate).toHaveBeenCalledWith(mockCase);
351
- });
352
- });
353
- it('does not call the API or onUpdate for an empty folder', () => {
354
- const onUpdate = vi.fn();
355
- renderMenu({ tree: { path: 'folder', leaves: [] }, onUpdate: onUpdate });
356
- act(() => {
357
- fireEvent.click(screen.getByTestId('remove-item'));
358
- });
359
- expect(mockDel).not.toHaveBeenCalled();
360
- expect(onUpdate).not.toHaveBeenCalled();
361
- });
362
- });
363
- });
@@ -1,25 +0,0 @@
1
- import type { SvgIconProps } from '@mui/material';
2
- import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
3
- import { type FC } from 'react';
4
- import type { Tree } from './types';
5
- interface FolderEntryProps {
6
- caseId?: string | null;
7
- path: string;
8
- /** MUI `pl` value for indentation */
9
- indent: number;
10
- /** Text displayed as the entry label */
11
- label: string;
12
- /** MUI icon color token applied to the entry icon (default: 'inherit') */
13
- iconColor?: SvgIconProps['color'];
14
- /** MUI color token for the label Typography (default: 'text.secondary') */
15
- labelColor?: string;
16
- /** Whether the chevron is rotated 90° (expanded state) */
17
- chevronOpen?: boolean;
18
- /** When provided the entry renders as a react-router Link */
19
- to?: string;
20
- onClick?: () => void;
21
- itemType: string;
22
- entry?: Item | Tree;
23
- }
24
- declare const FolderEntry: FC<FolderEntryProps>;
25
- export default FolderEntry;
@@ -1,88 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useDraggable, useDroppable } from '@dnd-kit/core';
3
- import { CSS } from '@dnd-kit/utilities';
4
- import { BookRounded, CheckCircle, ChevronRight, Folder, Lightbulb, Link as LinkIcon, TableChart, Visibility } from '@mui/icons-material';
5
- import { alpha, Box, Stack, Typography, useTheme } from '@mui/material';
6
- import {} from 'react';
7
- import { Link, useLocation } from 'react-router-dom';
8
- // Static map: item type → MUI icon component (avoids re-creating closures on each render)
9
- const ICON_FOR_TYPE = {
10
- folder: Folder,
11
- case: BookRounded,
12
- observable: Visibility,
13
- hit: CheckCircle,
14
- table: TableChart,
15
- lead: Lightbulb,
16
- reference: LinkIcon
17
- };
18
- const FolderEntry = ({ caseId, path, indent, label, itemType, iconColor = 'disabled', labelColor = 'text.secondary', chevronOpen = false, to, onClick, entry }) => {
19
- const location = useLocation();
20
- const theme = useTheme();
21
- const isCase = itemType === 'case';
22
- const isFolder = itemType === 'folder';
23
- const dndId = `${caseId ?? ''}:${itemType}:${path}`;
24
- const { attributes, listeners, setNodeRef: setDraggableNodeRef, transform, isDragging } = useDraggable({
25
- id: dndId,
26
- data: {
27
- type: itemType,
28
- label,
29
- entry,
30
- caseId
31
- },
32
- disabled: !caseId
33
- });
34
- const { setNodeRef: setDroppableNodeRef, isOver } = useDroppable({
35
- id: dndId,
36
- disabled: !isFolder || isDragging || !caseId,
37
- data: {
38
- path,
39
- caseId
40
- }
41
- });
42
- const isLink = to != null && !isDragging;
43
- const active = decodeURIComponent(location.pathname) === to;
44
- const Icon = ICON_FOR_TYPE[itemType] ?? Folder;
45
- return (_jsxs(Stack, { ref: el => {
46
- setDroppableNodeRef(el);
47
- setDraggableNodeRef(el);
48
- }, direction: "row", pl: indent, style: { transform: CSS.Transform.toString(transform), opacity: isDragging ? 0 : undefined }, sx: [
49
- {
50
- cursor: 'pointer',
51
- overflow: 'visible',
52
- color: `${theme.palette.text.secondary} !important`,
53
- textDecoration: 'none',
54
- background: 'transparent',
55
- position: 'relative',
56
- ...(isLink && { borderRight: '3px solid transparent' })
57
- },
58
- isLink &&
59
- active && {
60
- background: alpha(theme.palette.grey[600], 0.15),
61
- borderRightColor: theme.palette.primary.main
62
- }
63
- ], onClick: onClick, ...attributes, ...listeners, ...(isLink && {
64
- component: Link,
65
- to,
66
- target: itemType === 'reference' ? '_blank' : undefined,
67
- rel: itemType === 'reference' ? 'noopener noreferrer' : undefined
68
- }), children: [_jsx(Box, { sx: [
69
- {
70
- position: 'absolute',
71
- top: 0,
72
- bottom: 0,
73
- left: 0,
74
- right: 0,
75
- border: '2px dashed transparent',
76
- borderRadius: '5px',
77
- transition: theme.transitions.create('border-color')
78
- },
79
- isOver && caseId && { borderColor: theme.palette.primary.main }
80
- ] }), _jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
81
- !(isCase || isFolder) && { opacity: 0 },
82
- {
83
- transition: theme.transitions.create('transform', { duration: 100 }),
84
- transform: chevronOpen ? 'rotate(90deg)' : 'rotate(0deg)'
85
- }
86
- ] }), _jsx(Icon, { fontSize: "small", color: iconColor }), _jsx(Typography, { variant: "caption", color: labelColor, sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: label })] }));
87
- };
88
- export default FolderEntry;