@cccsaurora/howler-ui 2.18.0 → 2.19.0-cases.861
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.
- package/api/index.d.ts +4 -0
- package/api/index.js +10 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/facet/hit.d.ts +1 -3
- package/api/search/facet/index.d.ts +3 -1
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/socket/index.d.ts +3 -0
- package/api/socket/index.js +6 -0
- package/api/socket/viewers.d.ts +2 -0
- package/api/socket/viewers.js +8 -0
- package/api/socket/viewers.test.js +44 -0
- package/api/v2/case/index.d.ts +9 -0
- package/api/v2/case/index.js +21 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.js +18 -0
- package/api/v2/case/rules.d.ts +6 -0
- package/api/v2/case/rules.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +52 -12
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +23 -11
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +5 -5
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +346 -94
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
- package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
- package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
- package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
- package/components/app/providers/SocketProvider.d.ts +11 -2
- package/components/app/providers/SocketProvider.js +18 -5
- package/components/app/providers/UserListProvider.js +28 -8
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -0
- package/components/elements/ContextMenu.test.d.ts +1 -0
- package/components/elements/ContextMenu.test.js +215 -0
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +18 -8
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +12 -0
- package/components/elements/case/CaseCard.js +42 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/ChipPopper.js +5 -5
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +2 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +1 -0
- package/components/elements/hit/HitBanner.js +34 -51
- package/components/elements/hit/HitCard.d.ts +2 -0
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +1 -0
- package/components/elements/hit/HitOutline.js +3 -3
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitSummary.d.ts +2 -1
- package/components/elements/hit/HitSummary.js +6 -5
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/elements/Assigned.js +6 -3
- package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
- package/components/elements/hit/elements/Assigned.test.js +65 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +6 -0
- package/components/elements/observable/ObservableCard.js +22 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
- package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
- package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
- package/components/elements/record/RecordContextMenu.js +268 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +190 -39
- package/components/elements/record/RecordRelated.d.ts +7 -0
- package/components/elements/record/RecordRelated.js +34 -0
- package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
- package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
- package/components/elements/view/ViewTitle.d.ts +1 -0
- package/components/elements/view/ViewTitle.js +9 -2
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
- package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/403.d.ts +3 -0
- package/components/routes/403.js +10 -0
- package/components/routes/action/edit/ActionEditor.js +3 -3
- package/components/routes/action/useMyActionFunctions.js +4 -1
- package/components/routes/action/view/ActionDetails.js +6 -1
- package/components/routes/action/view/ActionSearch.js +5 -4
- package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
- package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +44 -0
- package/components/routes/cases/CaseViewer.test.d.ts +1 -0
- package/components/routes/cases/CaseViewer.test.js +133 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +148 -0
- package/components/routes/cases/constants.d.ts +6 -0
- package/components/routes/cases/constants.js +6 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +33 -0
- package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
- package/components/routes/cases/detail/CaseAssets.js +104 -0
- package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseAssets.test.js +167 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +66 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +70 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseRules.d.ts +7 -0
- package/components/routes/cases/detail/CaseRules.js +57 -0
- package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseRules.test.js +221 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
- package/components/routes/cases/detail/CaseSidebar.js +107 -0
- package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +66 -0
- package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
- package/components/routes/cases/detail/CaseTimeline.js +106 -0
- package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
- package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
- package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +95 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
- package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
- package/components/routes/cases/detail/assets/Asset.js +12 -0
- package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
- package/components/routes/cases/detail/assets/Asset.test.js +72 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +29 -0
- package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +69 -0
- package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
- package/components/routes/cases/hooks/useCase.test.js +141 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +59 -0
- package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
- package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
- package/components/routes/cases/modals/CaseRecordRow.js +15 -0
- package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/CreateCaseModal.js +55 -0
- package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
- package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
- package/components/routes/cases/modals/RenameItemModal.js +48 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +115 -0
- package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
- package/components/routes/cases/modals/ResolveModal.test.js +394 -0
- package/components/routes/cases/modals/hooks.d.ts +7 -0
- package/components/routes/cases/modals/hooks.js +44 -0
- package/components/routes/cases/modals/types.d.ts +5 -0
- package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
- package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
- package/components/routes/cases/search/CaseDateFilter.js +26 -0
- package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
- package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseStatusFilter.js +13 -0
- package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/help/markdown/en/retention.md.js +1 -1
- package/components/routes/help/markdown/fr/retention.md.js +1 -1
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +50 -63
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
- package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
- package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
- package/components/routes/hits/search/SearchPane.js +26 -49
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +20 -18
- package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
- package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +12 -13
- package/components/routes/home/AddNewCard.js +1 -1
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/overviews/template/en.md.js +1 -1
- package/components/routes/overviews/template/fr.md.js +1 -1
- package/components/routes/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +125 -3
- package/locales/fr/translation.json +123 -3
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/ApiType.d.ts +1 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Hit.d.ts +1 -0
- package/models/entities/generated/Howler.d.ts +0 -5
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +85 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +42 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +6 -9
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/models/entities/generated/View.d.ts +1 -0
- package/models/socket/CaseUpdate.d.ts +5 -0
- package/models/socket/ViewersUpdate.d.ts +4 -0
- package/package.json +23 -8
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/mocks.d.ts +11 -1
- package/tests/mocks.js +12 -7
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/constants.d.ts +4 -3
- package/utils/constants.js +6 -0
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- package/utils/menuUtils.js +1 -1
- package/utils/socketUtils.d.ts +14 -0
- package/utils/socketUtils.js +17 -1
- package/utils/socketUtils.test.d.ts +1 -0
- package/utils/socketUtils.test.js +59 -0
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +27 -0
- package/utils/viewUtils.js +3 -0
- package/components/app/providers/HitProvider.d.ts +0 -22
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/elements/hit/HitRelated.d.ts +0 -6
- package/components/elements/hit/HitRelated.js +0 -7
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
- package/components/routes/hits/search/BundleScroller.d.ts +0 -2
- package/components/routes/hits/search/BundleScroller.js +0 -6
- package/components/routes/hits/search/HitContextMenu.js +0 -229
- /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
- /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
|
@@ -8,6 +8,16 @@ import { vi } from 'vitest';
|
|
|
8
8
|
// Mock API
|
|
9
9
|
vi.mock('api', { spy: true });
|
|
10
10
|
setupContextSelectorMock();
|
|
11
|
+
// Mock useAppUser hook
|
|
12
|
+
const mockUseAppUser = vi.hoisted(() => vi.fn(() => ({
|
|
13
|
+
user: {
|
|
14
|
+
username: 'test-user',
|
|
15
|
+
roles: ['automation_basic', 'actionrunner_basic']
|
|
16
|
+
}
|
|
17
|
+
})));
|
|
18
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
19
|
+
useAppUser: mockUseAppUser
|
|
20
|
+
}));
|
|
11
21
|
// Mock react-router-dom
|
|
12
22
|
const mockNavigate = vi.fn();
|
|
13
23
|
vi.mock('react-router-dom', async () => {
|
|
@@ -47,6 +57,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
|
|
|
47
57
|
getMatchingTemplate: mockGetMatchingTemplate
|
|
48
58
|
}))
|
|
49
59
|
}));
|
|
60
|
+
const mockShowModal = vi.fn();
|
|
50
61
|
const mockDispatchApi = vi.fn();
|
|
51
62
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
52
63
|
default: vi.fn(() => ({
|
|
@@ -72,6 +83,9 @@ vi.mock('plugins/store', () => ({
|
|
|
72
83
|
plugins: ['plugin1']
|
|
73
84
|
}
|
|
74
85
|
}));
|
|
86
|
+
vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
|
|
87
|
+
default: () => null
|
|
88
|
+
}));
|
|
75
89
|
// Mock MUI components
|
|
76
90
|
vi.mock('@mui/material', async () => {
|
|
77
91
|
const actual = await vi.importActual('@mui/material');
|
|
@@ -91,13 +105,14 @@ vi.mock('@mui/material', async () => {
|
|
|
91
105
|
});
|
|
92
106
|
// Import component after mocks
|
|
93
107
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
94
|
-
import {
|
|
108
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
95
109
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
110
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
96
111
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
97
112
|
import { I18nextProvider } from 'react-i18next';
|
|
98
113
|
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
99
114
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
100
|
-
import
|
|
115
|
+
import RecordContextMenu from './RecordContextMenu';
|
|
101
116
|
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
102
117
|
const mockConfig = {
|
|
103
118
|
lookups: {
|
|
@@ -105,16 +120,16 @@ const mockConfig = {
|
|
|
105
120
|
}
|
|
106
121
|
};
|
|
107
122
|
const mockApiContext = { config: mockConfig };
|
|
108
|
-
const
|
|
109
|
-
|
|
123
|
+
const mockRecordContext = {
|
|
124
|
+
records: {
|
|
110
125
|
'test-hit-1': createMockHit()
|
|
111
126
|
},
|
|
112
|
-
|
|
127
|
+
selectedRecords: []
|
|
113
128
|
};
|
|
114
129
|
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
115
130
|
// Test wrapper
|
|
116
131
|
const Wrapper = ({ children }) => {
|
|
117
|
-
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(
|
|
132
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(ModalContext.Provider, { value: { showModal: mockShowModal }, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }) }));
|
|
118
133
|
};
|
|
119
134
|
describe('HitContextMenu', () => {
|
|
120
135
|
let user;
|
|
@@ -122,11 +137,11 @@ describe('HitContextMenu', () => {
|
|
|
122
137
|
beforeEach(() => {
|
|
123
138
|
user = userEvent.setup();
|
|
124
139
|
vi.clearAllMocks();
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
mockRecordContext.selectedRecords.length = 0;
|
|
141
|
+
mockRecordContext.records['test-hit-1'] = createMockHit();
|
|
127
142
|
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
128
143
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
129
|
-
rerender = render(_jsx(Wrapper, { children: _jsx(
|
|
144
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
130
145
|
});
|
|
131
146
|
describe('Context Menu Initialization', () => {
|
|
132
147
|
it('should open menu on right-click', async () => {
|
|
@@ -190,13 +205,13 @@ describe('HitContextMenu', () => {
|
|
|
190
205
|
});
|
|
191
206
|
it('should disable "Open Hit" when hit is null', async () => {
|
|
192
207
|
act(() => {
|
|
193
|
-
|
|
208
|
+
mockRecordContext.records['test-hit-1'] = null;
|
|
194
209
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
195
210
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
196
211
|
});
|
|
197
212
|
await waitFor(() => {
|
|
198
213
|
const menuItems = screen.getAllByRole('menuitem');
|
|
199
|
-
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit
|
|
214
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
|
|
200
215
|
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
201
216
|
});
|
|
202
217
|
});
|
|
@@ -237,7 +252,7 @@ describe('HitContextMenu', () => {
|
|
|
237
252
|
skip_rationale: false
|
|
238
253
|
}
|
|
239
254
|
}));
|
|
240
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
255
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
241
256
|
act(() => {
|
|
242
257
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
243
258
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -295,7 +310,7 @@ describe('HitContextMenu', () => {
|
|
|
295
310
|
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
296
311
|
];
|
|
297
312
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
298
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
313
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
299
314
|
act(() => {
|
|
300
315
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
301
316
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -339,7 +354,7 @@ describe('HitContextMenu', () => {
|
|
|
339
354
|
});
|
|
340
355
|
it('should disable custom actions menu when no actions are available', async () => {
|
|
341
356
|
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
342
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
357
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
343
358
|
act(() => {
|
|
344
359
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
345
360
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -381,7 +396,7 @@ describe('HitContextMenu', () => {
|
|
|
381
396
|
skip_rationale: true
|
|
382
397
|
}
|
|
383
398
|
}));
|
|
384
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
399
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
385
400
|
act(() => {
|
|
386
401
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
387
402
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -449,7 +464,7 @@ describe('HitContextMenu', () => {
|
|
|
449
464
|
it('should call executeAction with action_id and hit query', async () => {
|
|
450
465
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
451
466
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
452
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
467
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
453
468
|
act(() => {
|
|
454
469
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
455
470
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -502,7 +517,7 @@ describe('HitContextMenu', () => {
|
|
|
502
517
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
503
518
|
keys: ['howler.detection', 'event.id']
|
|
504
519
|
}));
|
|
505
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
520
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
506
521
|
});
|
|
507
522
|
it('should render exclusion submenu with template keys', async () => {
|
|
508
523
|
act(() => {
|
|
@@ -550,7 +565,7 @@ describe('HitContextMenu', () => {
|
|
|
550
565
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
551
566
|
keys: ['howler.outline.indicators']
|
|
552
567
|
}));
|
|
553
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
568
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
554
569
|
act(() => {
|
|
555
570
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
556
571
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -593,7 +608,7 @@ describe('HitContextMenu', () => {
|
|
|
593
608
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
594
609
|
keys: []
|
|
595
610
|
}));
|
|
596
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
611
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
597
612
|
act(() => {
|
|
598
613
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
599
614
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -605,7 +620,7 @@ describe('HitContextMenu', () => {
|
|
|
605
620
|
});
|
|
606
621
|
it('should skip null field values in exclusion menu', async () => {
|
|
607
622
|
act(() => {
|
|
608
|
-
|
|
623
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
609
624
|
});
|
|
610
625
|
act(() => {
|
|
611
626
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -628,7 +643,7 @@ describe('HitContextMenu', () => {
|
|
|
628
643
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
629
644
|
keys: ['howler.detection', 'event.id']
|
|
630
645
|
}));
|
|
631
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
646
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
632
647
|
});
|
|
633
648
|
it('should render inclusion submenu with template keys', async () => {
|
|
634
649
|
act(() => {
|
|
@@ -676,7 +691,7 @@ describe('HitContextMenu', () => {
|
|
|
676
691
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
677
692
|
keys: ['howler.outline.indicators']
|
|
678
693
|
}));
|
|
679
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
694
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
680
695
|
act(() => {
|
|
681
696
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
682
697
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -719,7 +734,7 @@ describe('HitContextMenu', () => {
|
|
|
719
734
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
720
735
|
keys: []
|
|
721
736
|
}));
|
|
722
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
737
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
723
738
|
act(() => {
|
|
724
739
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
725
740
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -731,7 +746,7 @@ describe('HitContextMenu', () => {
|
|
|
731
746
|
});
|
|
732
747
|
it('should skip null field values in inclusion menu', async () => {
|
|
733
748
|
act(() => {
|
|
734
|
-
|
|
749
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
735
750
|
});
|
|
736
751
|
act(() => {
|
|
737
752
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -750,24 +765,24 @@ describe('HitContextMenu', () => {
|
|
|
750
765
|
});
|
|
751
766
|
});
|
|
752
767
|
describe('Multiple Hit Selection', () => {
|
|
753
|
-
it('should use
|
|
768
|
+
it('should use selectedRecords when current hit is included', async () => {
|
|
754
769
|
act(() => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
770
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
771
|
+
mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
|
|
772
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
|
|
758
773
|
mockGetSelectedId.mockReturnValue('hit-1');
|
|
759
774
|
});
|
|
760
775
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
761
776
|
await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
|
|
762
|
-
// The component should use
|
|
777
|
+
// The component should use selectedRecords for actions
|
|
763
778
|
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
764
779
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
765
780
|
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
766
781
|
});
|
|
767
|
-
it('should use only current hit when not in
|
|
782
|
+
it('should use only current hit when not in selectedRecords', async () => {
|
|
768
783
|
act(() => {
|
|
769
|
-
|
|
770
|
-
|
|
784
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
785
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
|
|
771
786
|
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
772
787
|
});
|
|
773
788
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -786,12 +801,12 @@ describe('HitContextMenu', () => {
|
|
|
786
801
|
});
|
|
787
802
|
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
788
803
|
await waitFor(() => {
|
|
789
|
-
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(
|
|
804
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
790
805
|
});
|
|
791
806
|
});
|
|
792
807
|
it('should call getMatchingTemplate when menu opens', async () => {
|
|
793
808
|
await waitFor(() => {
|
|
794
|
-
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(
|
|
809
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
795
810
|
});
|
|
796
811
|
});
|
|
797
812
|
it('should reset state when menu closes', async () => {
|
|
@@ -824,7 +839,7 @@ describe('HitContextMenu', () => {
|
|
|
824
839
|
describe('Edge Cases and Error Handling', () => {
|
|
825
840
|
it('should not crash when hit is null', async () => {
|
|
826
841
|
act(() => {
|
|
827
|
-
|
|
842
|
+
mockRecordContext.records = {};
|
|
828
843
|
});
|
|
829
844
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
830
845
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -835,7 +850,7 @@ describe('HitContextMenu', () => {
|
|
|
835
850
|
});
|
|
836
851
|
it('should not render exclusion menu when template is null', async () => {
|
|
837
852
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
838
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
853
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
839
854
|
act(() => {
|
|
840
855
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
841
856
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -849,7 +864,7 @@ describe('HitContextMenu', () => {
|
|
|
849
864
|
});
|
|
850
865
|
it('should not render inclusion menu when template is null', async () => {
|
|
851
866
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
852
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
867
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
853
868
|
act(() => {
|
|
854
869
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
855
870
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -863,7 +878,7 @@ describe('HitContextMenu', () => {
|
|
|
863
878
|
});
|
|
864
879
|
it('should handle API failure gracefully', async () => {
|
|
865
880
|
mockDispatchApi.mockResolvedValue(null);
|
|
866
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
881
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
867
882
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
868
883
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
869
884
|
await waitFor(() => {
|
|
@@ -874,7 +889,7 @@ describe('HitContextMenu', () => {
|
|
|
874
889
|
});
|
|
875
890
|
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
876
891
|
act(() => {
|
|
877
|
-
|
|
892
|
+
mockRecordContext.records['test-hit-1'].howler.analytic = null;
|
|
878
893
|
});
|
|
879
894
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
880
895
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -893,4 +908,140 @@ describe('HitContextMenu', () => {
|
|
|
893
908
|
expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
|
|
894
909
|
});
|
|
895
910
|
});
|
|
911
|
+
describe('Add to Case Menu Item', () => {
|
|
912
|
+
it('should render "Add to Case" item in the menu', async () => {
|
|
913
|
+
act(() => {
|
|
914
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
915
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
916
|
+
});
|
|
917
|
+
await waitFor(() => {
|
|
918
|
+
expect(screen.getByText('Add to Case')).toBeInTheDocument();
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
it('should enable "Add to Case" when a record is present', async () => {
|
|
922
|
+
act(() => {
|
|
923
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
924
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
925
|
+
});
|
|
926
|
+
await waitFor(() => {
|
|
927
|
+
const menuItems = screen.getAllByRole('menuitem');
|
|
928
|
+
const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
|
|
929
|
+
expect(addToCaseItem).toHaveAttribute('aria-disabled', 'false');
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
it('should disable "Add to Case" when record is null', async () => {
|
|
933
|
+
act(() => {
|
|
934
|
+
mockRecordContext.records['test-hit-1'] = null;
|
|
935
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
936
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
937
|
+
});
|
|
938
|
+
await waitFor(() => {
|
|
939
|
+
const menuItems = screen.getAllByRole('menuitem');
|
|
940
|
+
const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
|
|
941
|
+
expect(addToCaseItem).toHaveAttribute('aria-disabled', 'true');
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
it('should call showModal with an AddToCaseModal element when clicked', async () => {
|
|
945
|
+
act(() => {
|
|
946
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
947
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
948
|
+
});
|
|
949
|
+
await waitFor(() => {
|
|
950
|
+
expect(screen.getByText('Add to Case')).toBeInTheDocument();
|
|
951
|
+
});
|
|
952
|
+
await act(async () => {
|
|
953
|
+
await user.click(screen.getByText('Add to Case'));
|
|
954
|
+
});
|
|
955
|
+
await waitFor(() => {
|
|
956
|
+
expect(mockShowModal).toHaveBeenCalledOnce();
|
|
957
|
+
expect(mockShowModal).toHaveBeenCalledWith(expect.objectContaining({ type: expect.any(Function) }), expect.objectContaining({ maxHeight: expect.any(String) }));
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
describe('Role-Based Action Permissions', () => {
|
|
962
|
+
afterEach(() => {
|
|
963
|
+
// Reset to default user with required roles
|
|
964
|
+
mockUseAppUser.mockReturnValue({
|
|
965
|
+
user: {
|
|
966
|
+
username: 'test-user',
|
|
967
|
+
roles: ['automation_basic', 'actionrunner_basic']
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
it('should disable actions menu when user lacks required roles', async () => {
|
|
972
|
+
mockUseAppUser.mockReturnValue({
|
|
973
|
+
user: {
|
|
974
|
+
username: 'test-user',
|
|
975
|
+
roles: ['user', 'viewer']
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
979
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
980
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
981
|
+
act(() => {
|
|
982
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
983
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
984
|
+
});
|
|
985
|
+
await waitFor(() => {
|
|
986
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
987
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
it('should enable actions menu when user has automation_basic role', async () => {
|
|
991
|
+
mockUseAppUser.mockReturnValue({
|
|
992
|
+
user: {
|
|
993
|
+
username: 'test-user',
|
|
994
|
+
roles: ['automation_basic']
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
998
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
999
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1000
|
+
act(() => {
|
|
1001
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1002
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1003
|
+
});
|
|
1004
|
+
await waitFor(() => {
|
|
1005
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1006
|
+
expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
it('should enable actions menu when user has actionrunner_advanced role', async () => {
|
|
1010
|
+
mockUseAppUser.mockReturnValue({
|
|
1011
|
+
user: {
|
|
1012
|
+
username: 'test-user',
|
|
1013
|
+
roles: ['actionrunner_advanced']
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
1017
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
1018
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1019
|
+
act(() => {
|
|
1020
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1021
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1022
|
+
});
|
|
1023
|
+
await waitFor(() => {
|
|
1024
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1025
|
+
expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
it('should still disable actions menu when user has roles but no actions available', async () => {
|
|
1029
|
+
mockUseAppUser.mockReturnValue({
|
|
1030
|
+
user: {
|
|
1031
|
+
username: 'test-user',
|
|
1032
|
+
roles: ['automation_advanced']
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
mockDispatchApi.mockResolvedValue({ items: [] });
|
|
1036
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1037
|
+
act(() => {
|
|
1038
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1039
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1040
|
+
});
|
|
1041
|
+
await waitFor(() => {
|
|
1042
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1043
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
896
1047
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
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 RecordRelated: FC<{
|
|
5
|
+
record: Hit | Observable;
|
|
6
|
+
}>;
|
|
7
|
+
export default RecordRelated;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Stack, Tab, Tabs, useTheme } from '@mui/material';
|
|
3
|
+
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
4
|
+
import useRelatedRecords from '@cccsaurora/howler-ui/components/hooks/useRelatedRecords';
|
|
5
|
+
import { groupBy } from 'lodash-es';
|
|
6
|
+
import { useMemo, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Link } from 'react-router-dom';
|
|
9
|
+
import { isCase, isHit, isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
10
|
+
import CaseCard from '../case/CaseCard';
|
|
11
|
+
import HitCard from '../hit/HitCard';
|
|
12
|
+
import { HitLayout } from '../hit/HitLayout';
|
|
13
|
+
import RelatedLink from '../hit/related/RelatedLink';
|
|
14
|
+
const RecordRelated = ({ record }) => {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const related = useMemo(() => record?.howler.related ?? [], [record?.howler.related]);
|
|
18
|
+
const records = useRelatedRecords(related, related.length > 0);
|
|
19
|
+
const groups = groupBy(records, '__index');
|
|
20
|
+
const hasLinks = (record?.howler.links?.length ?? 0) > 0;
|
|
21
|
+
const tabs = [
|
|
22
|
+
hasLinks && 'links',
|
|
23
|
+
groups.hit?.length > 0 && 'hit',
|
|
24
|
+
groups.case?.length > 0 && 'case',
|
|
25
|
+
groups.observable?.length > 0 && 'observable'
|
|
26
|
+
].filter(Boolean);
|
|
27
|
+
const [activeTab, setActiveTab] = useState(false);
|
|
28
|
+
const currentTab = activeTab !== false && tabs.includes(activeTab) ? activeTab : (tabs[0] ?? false);
|
|
29
|
+
if (!record) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs(Box, { sx: { borderTop: `thin solid ${theme.palette.divider}`, height: '100%', flex: 1, mr: 2, pb: 2 }, children: [_jsxs(Tabs, { value: currentTab, onChange: (_, v) => setActiveTab(v), variant: "scrollable", scrollButtons: "auto", children: [hasLinks && _jsx(Tab, { value: "links", label: t('hit.related.tab.links') }), groups.hit?.length > 0 && _jsx(Tab, { value: "hit", label: t('hit.related.tab.hit') }), groups.case?.length > 0 && _jsx(Tab, { value: "case", label: t('hit.related.tab.case') }), groups.observable?.length > 0 && _jsx(Tab, { value: "observable", label: t('hit.related.tab.observable') })] }), currentTab === 'links' && (_jsx(Box, { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 1, pt: 1, children: record.howler.links.map(l => (_jsx(RelatedLink, { ...l }, l.title + l.href))) })), currentTab === 'hit' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isHit).map(h => (_jsx(Link, { to: `/hits/${h.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(HitCard, { id: h.howler.id, layout: HitLayout.NORMAL }) }, h.howler.id))) })), currentTab === 'case' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isCase).map(c => (_jsx(Link, { to: `/cases/${c.case_id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(CaseCard, { case: c }) }, c.case_id))) })), currentTab === 'observable' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isObservable).map(o => (_jsx(Link, { to: `/observables/${o.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(ObservableCard, { observable: o }) }, o.howler.id))) }))] }));
|
|
33
|
+
};
|
|
34
|
+
export default RecordRelated;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { HowlerUser } from '@cccsaurora/howler-ui/models/entities/HowlerUser';
|
|
2
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
4
|
import type { FC } from 'react';
|
|
4
|
-
declare const
|
|
5
|
-
|
|
5
|
+
declare const RecordWorklog: FC<{
|
|
6
|
+
record: Hit | Observable;
|
|
6
7
|
users: {
|
|
7
8
|
[id: string]: HowlerUser;
|
|
8
9
|
};
|
|
9
10
|
}>;
|
|
10
|
-
export default
|
|
11
|
+
export default RecordWorklog;
|
|
@@ -10,7 +10,7 @@ import { compareTimestamp, twitterShort } from '@cccsaurora/howler-ui/utils/util
|
|
|
10
10
|
import HowlerAvatar from '../display/HowlerAvatar';
|
|
11
11
|
import HowlerCard from '../display/HowlerCard';
|
|
12
12
|
import Markdown from '../display/Markdown';
|
|
13
|
-
const
|
|
13
|
+
const RecordWorklog = ({ record, users }) => {
|
|
14
14
|
const theme = useTheme();
|
|
15
15
|
const { shiftColor } = useMyUtils();
|
|
16
16
|
const { t } = useTranslation();
|
|
@@ -23,15 +23,15 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
23
23
|
*/
|
|
24
24
|
const worklogGroups = useMemo(() => {
|
|
25
25
|
let setInitialVersion = false;
|
|
26
|
-
return (
|
|
26
|
+
return (record?.howler?.log || [])
|
|
27
27
|
.slice()
|
|
28
28
|
.sort((a, b) => compareTimestamp(b.timestamp, a.timestamp))
|
|
29
29
|
.reduce((acc, l) => {
|
|
30
|
-
if (!initialVersions[
|
|
30
|
+
if (!initialVersions[record.howler.id] && !setInitialVersion) {
|
|
31
31
|
setInitialVersion = true;
|
|
32
32
|
setInitialVersions({
|
|
33
33
|
...initialVersions,
|
|
34
|
-
[
|
|
34
|
+
[record.howler.id]: l.previous_version
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
// Initialize the worklog card groups
|
|
@@ -42,9 +42,9 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
42
42
|
const currArr = acc[acc.length - 1];
|
|
43
43
|
if (
|
|
44
44
|
// Does this log version match the saved version?
|
|
45
|
-
l.previous_version === initialVersions[
|
|
45
|
+
l.previous_version === initialVersions[record.howler.id] &&
|
|
46
46
|
// Does the previous entry not match?
|
|
47
|
-
currArr[currArr.length - 1].previous_version !== initialVersions[
|
|
47
|
+
currArr[currArr.length - 1].previous_version !== initialVersions[record.howler.id]) {
|
|
48
48
|
// If so, we've figured out where the new logs should start, so we start a new card.
|
|
49
49
|
acc.push([l]);
|
|
50
50
|
return acc;
|
|
@@ -59,14 +59,14 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
59
59
|
}, []);
|
|
60
60
|
},
|
|
61
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
-
[
|
|
62
|
+
[record?.howler?.log]);
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
// On unmount, mark the latest entry version as the last seen version.
|
|
65
65
|
return () => {
|
|
66
|
-
if (
|
|
66
|
+
if (record?.howler.id) {
|
|
67
67
|
setInitialVersions({
|
|
68
68
|
...initialVersions,
|
|
69
|
-
[
|
|
69
|
+
[record.howler.id]: worklogGroups[0][0]?.previous_version ?? initialVersions[record.howler.id]
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
};
|
|
@@ -77,7 +77,9 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
77
77
|
if (worklogGroups.length > 0) {
|
|
78
78
|
return worklogGroups.flatMap((ls, index) => {
|
|
79
79
|
const result = [];
|
|
80
|
-
if (index > 0 &&
|
|
80
|
+
if (index > 0 &&
|
|
81
|
+
initialVersions[record.howler.id] &&
|
|
82
|
+
ls[0].previous_version === initialVersions[record.howler.id]) {
|
|
81
83
|
result.push(_jsx(Divider, { children: _jsxs(Stack, { direction: "row", children: [_jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('hit.worklog.new') }), _jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" })] }) }, "new"));
|
|
82
84
|
}
|
|
83
85
|
result.push(_jsxs(HowlerCard, { elevation: 4, children: [_jsx(CardHeader, { avatar: _jsx(HowlerAvatar, { userId: ls[0].user }), title: users[ls[0].user]?.name ?? ls[0].user, subheader: _jsx(Tooltip, { title: new Date(ls[0].timestamp).toLocaleString(), children: _jsx(Typography, { variant: "caption", children: twitterShort(ls[0].timestamp) }) }) }), _jsx(CardContent, { children: _jsx(Stack, { spacing: 1, divider: _jsx(Divider, { orientation: "horizontal" }), children: ls.map(l => (_jsxs(Typography, { variant: "body2", color: "text.secondary", component: "div", position: "relative", children: [l.explanation ? (_jsx(Markdown, { md: l.explanation.trim() })) : (_jsxs(_Fragment, { children: [_jsxs("span", { children: [t('hit.worklog.updated'), "\u00A0"] }), _jsx("code", { children: l.key }), _jsx("span", { children: ":\u00A0" }), {
|
|
@@ -88,10 +90,10 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
88
90
|
return result;
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
|
-
else if (!
|
|
93
|
+
else if (!record?.howler) {
|
|
92
94
|
return (_jsxs(_Fragment, { children: [_jsx(Skeleton, { width: "100%", height: 200, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 220, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 150, variant: "rounded" })] }));
|
|
93
95
|
}
|
|
94
|
-
}, [worklogGroups,
|
|
96
|
+
}, [worklogGroups, record.howler, initialVersions, users, t, shiftColor, theme.palette.text.primary]);
|
|
95
97
|
return (_jsx(Stack, { sx: { p: 2 }, spacing: 1, children: worklogEls }));
|
|
96
98
|
};
|
|
97
|
-
export default
|
|
99
|
+
export default RecordWorklog;
|
|
@@ -4,7 +4,7 @@ import { Chip, Stack, Tooltip, Typography } from '@mui/material';
|
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
|
-
export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
7
|
+
export const ViewTitle = ({ title, type, query, sort, span, indexes }) => {
|
|
8
8
|
const { t } = useTranslation();
|
|
9
9
|
const spanLabel = useMemo(() => {
|
|
10
10
|
if (!span) {
|
|
@@ -17,9 +17,16 @@ export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
|
17
17
|
return t(span);
|
|
18
18
|
}
|
|
19
19
|
}, [span, t]);
|
|
20
|
+
const indexLabel = useMemo(() => {
|
|
21
|
+
if (!indexes || indexes.length === 0) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
else
|
|
25
|
+
return `(${indexes.join(', ')})`;
|
|
26
|
+
}, [indexes]);
|
|
20
27
|
return (_jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", alignItems: "start", spacing: 1, children: [_jsx(Tooltip, { title: t(`route.views.manager.${type}`), children: {
|
|
21
28
|
readonly: _jsx(Lock, { fontSize: "small" }),
|
|
22
29
|
global: _jsx(Language, { fontSize: "small" }),
|
|
23
30
|
personal: _jsx(Person, { fontSize: "small" })
|
|
24
|
-
}[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, {
|
|
31
|
+
}[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span || indexLabel) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { label: spanLabel }), indexLabel && _jsx(Chip, { label: indexLabel })] }))] }));
|
|
25
32
|
};
|
|
@@ -7,7 +7,7 @@ declare const useHitActions: (_hits: Hit | Hit[]) => {
|
|
|
7
7
|
canAssess: boolean;
|
|
8
8
|
loading: boolean;
|
|
9
9
|
manage: (transition: string) => Promise<void>;
|
|
10
|
-
assess: (assessment: string, skipRationale?: boolean) => Promise<void>;
|
|
10
|
+
assess: (assessment: string, skipRationale?: boolean, providedRationale?: any) => Promise<void>;
|
|
11
11
|
vote: (v: string) => Promise<void>;
|
|
12
12
|
selectedVote: string;
|
|
13
13
|
};
|