@cccsaurora/howler-ui 2.19.0-dev.836 → 2.19.0-dev.842
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 +41 -8
- 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} +98 -43
- 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/action/edit/ActionEditor.js +2 -2
- package/components/routes/action/view/ActionSearch.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/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- 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/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/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +123 -3
- package/locales/fr/translation.json +121 -3
- package/models/WithMetadata.d.ts +2 -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 +21 -1
- 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/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 -239
- /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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, NoteAdd, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
|
|
3
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks/useAppUser';
|
|
5
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
6
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
7
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
8
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
9
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
10
|
+
import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
|
|
11
|
+
import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
|
|
12
|
+
import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
|
|
13
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
14
|
+
import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
|
|
15
|
+
import AddToCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/AddToCaseModal';
|
|
16
|
+
import CreateCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/CreateCaseModal';
|
|
17
|
+
import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
|
|
18
|
+
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
19
|
+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
20
|
+
import { useTranslation } from 'react-i18next';
|
|
21
|
+
import { usePluginStore } from 'react-pluggable';
|
|
22
|
+
import { useContextSelector } from 'use-context-selector';
|
|
23
|
+
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
24
|
+
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
25
|
+
import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
26
|
+
/**
|
|
27
|
+
* Order in which action types should be displayed in the context menu
|
|
28
|
+
*/
|
|
29
|
+
const ORDER = ['assessment', 'vote', 'action'];
|
|
30
|
+
/**
|
|
31
|
+
* Icon mapping for different action types
|
|
32
|
+
*/
|
|
33
|
+
const ICON_MAP = {
|
|
34
|
+
assessment: _jsx(Assignment, {}),
|
|
35
|
+
vote: _jsx(HowToVote, {}),
|
|
36
|
+
action: _jsx(Edit, {})
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Context menu component for hit operations.
|
|
40
|
+
* Provides quick access to common hit actions including assessment, voting,
|
|
41
|
+
* transitions, and exclusion filters based on template fields.
|
|
42
|
+
*/
|
|
43
|
+
const RecordContextMenu = ({ children, getSelectedId, Component }) => {
|
|
44
|
+
const { t } = useTranslation();
|
|
45
|
+
const { dispatchApi } = useMyApi();
|
|
46
|
+
const { executeAction } = useMyActionFunctions();
|
|
47
|
+
const appUser = useAppUser();
|
|
48
|
+
const { config } = useContext(ApiConfigContext);
|
|
49
|
+
const { showModal } = useContext(ModalContext);
|
|
50
|
+
const pluginStore = usePluginStore();
|
|
51
|
+
const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
|
|
52
|
+
const query = useContextSelector(ParameterContext, ctx => ctx?.query);
|
|
53
|
+
const setQuery = useContextSelector(ParameterContext, ctx => ctx?.setQuery);
|
|
54
|
+
const [id, setId] = useState(null);
|
|
55
|
+
const record = useContextSelector(RecordContext, ctx => ctx.records[id]);
|
|
56
|
+
const selectedRecords = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
|
|
57
|
+
const [analytic, setAnalytic] = useState(null);
|
|
58
|
+
const [template, setTemplate] = useState(null);
|
|
59
|
+
const [actions, setActions] = useState([]);
|
|
60
|
+
const records = useMemo(() => selectedRecords.some(_record => _record.howler.id === record?.howler.id)
|
|
61
|
+
? selectedRecords
|
|
62
|
+
: record
|
|
63
|
+
? [record]
|
|
64
|
+
: [], [record, selectedRecords]);
|
|
65
|
+
const hits = useMemo(() => records.filter(isHit), [records]);
|
|
66
|
+
const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
|
|
67
|
+
/**
|
|
68
|
+
* Checks if the current user has permission to run actions.
|
|
69
|
+
* Users must have one of the automation or actionrunner roles, or be an admin.
|
|
70
|
+
*/
|
|
71
|
+
const canRunActions = useCallback(() => {
|
|
72
|
+
const roles = ['admin', 'automation_advanced', 'automation_basic', 'actionrunner_advanced', 'actionrunner_basic'];
|
|
73
|
+
return roles.some((role) => appUser.user?.roles?.includes(role));
|
|
74
|
+
}, [appUser.user?.roles]);
|
|
75
|
+
/**
|
|
76
|
+
* Called by ContextMenu after the menu is positioned and opened.
|
|
77
|
+
* Identifies the clicked record and fetches available actions.
|
|
78
|
+
*/
|
|
79
|
+
const onOpen = useCallback(async (event) => {
|
|
80
|
+
const _id = getSelectedId(event);
|
|
81
|
+
setId(_id);
|
|
82
|
+
// TODO: Bumping the number of rows is a temporary fix - we'll need to improve this.
|
|
83
|
+
const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*', rows: 100, sort: 'name asc' }), {
|
|
84
|
+
throwError: false
|
|
85
|
+
}))?.items;
|
|
86
|
+
if (_actions) {
|
|
87
|
+
setActions(_actions);
|
|
88
|
+
}
|
|
89
|
+
}, [dispatchApi, getSelectedId]);
|
|
90
|
+
const rowStatus = useMemo(() => ({
|
|
91
|
+
assessment: canAssess,
|
|
92
|
+
vote: canVote
|
|
93
|
+
}), [canAssess, canVote]);
|
|
94
|
+
const pluginActions = howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.actions`, records));
|
|
95
|
+
/**
|
|
96
|
+
* Generates grouped action entries for the context menu.
|
|
97
|
+
* Combines transitions, plugin actions, votes, and assessments based on permissions.
|
|
98
|
+
*/
|
|
99
|
+
const entries = useMemo(() => {
|
|
100
|
+
let _actions = [...availableTransitions, ...pluginActions];
|
|
101
|
+
if (canVote) {
|
|
102
|
+
_actions = [
|
|
103
|
+
..._actions,
|
|
104
|
+
...VOTE_OPTIONS.map(option => ({ ...option, actionFunction: () => vote(option.name.toLowerCase()) }))
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
if (config.lookups?.['howler.assessment'] && canAssess) {
|
|
108
|
+
_actions = [
|
|
109
|
+
..._actions,
|
|
110
|
+
...config.lookups['howler.assessment']
|
|
111
|
+
.filter(_assessment => analytic?.triage_settings?.valid_assessments
|
|
112
|
+
? analytic.triage_settings?.valid_assessments.includes(_assessment)
|
|
113
|
+
: true)
|
|
114
|
+
.sort((a, b) => +TOP_ROW.includes(b) - +TOP_ROW.includes(a))
|
|
115
|
+
.map(assessment => ({
|
|
116
|
+
type: 'assessment',
|
|
117
|
+
name: assessment,
|
|
118
|
+
actionFunction: async () => {
|
|
119
|
+
await assess(assessment, analytic?.triage_settings?.skip_rationale);
|
|
120
|
+
}
|
|
121
|
+
}))
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
return Object.entries(groupBy(_actions, 'type')).sort(([a], [b]) => ORDER.indexOf(a) - ORDER.indexOf(b));
|
|
125
|
+
}, [analytic, assess, availableTransitions, canAssess, canVote, config.lookups, vote, pluginActions]);
|
|
126
|
+
// Load analytic and template data when a hit is selected
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!record?.howler.analytic) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
getMatchingAnalytic(record).then(setAnalytic);
|
|
132
|
+
getMatchingTemplate(record).then(setTemplate);
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [record]);
|
|
135
|
+
/**
|
|
136
|
+
* Builds the declarative items structure for the ContextMenu component.
|
|
137
|
+
*/
|
|
138
|
+
const items = useMemo(() => {
|
|
139
|
+
const result = [
|
|
140
|
+
{
|
|
141
|
+
kind: 'item',
|
|
142
|
+
id: 'open-record',
|
|
143
|
+
icon: _jsx(OpenInNew, {}),
|
|
144
|
+
label: t(`${record?.__index ?? 'hit'}.open`),
|
|
145
|
+
disabled: !record,
|
|
146
|
+
to: `/${record?.__index}s/${record?.howler.id}`
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
if (isHit(record)) {
|
|
150
|
+
result.push({
|
|
151
|
+
kind: 'item',
|
|
152
|
+
id: 'open-analytic',
|
|
153
|
+
icon: _jsx(QueryStats, {}),
|
|
154
|
+
label: t('analytic.open'),
|
|
155
|
+
disabled: !analytic,
|
|
156
|
+
to: `/analytics/${analytic?.analytic_id}`
|
|
157
|
+
});
|
|
158
|
+
result.push({ kind: 'divider', id: 'actions-divider' });
|
|
159
|
+
for (const [type, typeItems] of entries) {
|
|
160
|
+
result.push({
|
|
161
|
+
kind: 'submenu',
|
|
162
|
+
id: type,
|
|
163
|
+
icon: ICON_MAP[type] ?? _jsx(Terminal, {}),
|
|
164
|
+
label: t(`hit.details.actions.${type}`),
|
|
165
|
+
disabled: rowStatus[type] === false,
|
|
166
|
+
items: typeItems.map(a => ({
|
|
167
|
+
key: a.name,
|
|
168
|
+
label: a.i18nKey ? t(a.i18nKey) : capitalize(a.name),
|
|
169
|
+
onClick: a.actionFunction
|
|
170
|
+
}))
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
result.push({
|
|
174
|
+
kind: 'submenu',
|
|
175
|
+
id: 'actions',
|
|
176
|
+
icon: _jsx(SettingsSuggest, {}),
|
|
177
|
+
label: t('route.actions.change'),
|
|
178
|
+
disabled: actions.length < 1 || !canRunActions(),
|
|
179
|
+
items: actions.map(action => ({
|
|
180
|
+
key: action.action_id,
|
|
181
|
+
label: action.name,
|
|
182
|
+
onClick: () => executeAction(action.action_id, `howler.id:${record?.howler.id}`)
|
|
183
|
+
}))
|
|
184
|
+
});
|
|
185
|
+
if (!isEmpty(template?.keys ?? []) && setQuery) {
|
|
186
|
+
result.push({ kind: 'divider', id: 'filter-divider' });
|
|
187
|
+
result.push({
|
|
188
|
+
kind: 'submenu',
|
|
189
|
+
id: 'excludes',
|
|
190
|
+
icon: _jsx(RemoveCircleOutline, {}),
|
|
191
|
+
label: t('hit.panel.exclude'),
|
|
192
|
+
items: (template?.keys ?? []).flatMap(key => {
|
|
193
|
+
let newQuery = '';
|
|
194
|
+
if (query !== DEFAULT_QUERY) {
|
|
195
|
+
newQuery = `(${query}) AND `;
|
|
196
|
+
}
|
|
197
|
+
const value = get(record, key);
|
|
198
|
+
if (!value) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
else if (Array.isArray(value)) {
|
|
202
|
+
const sanitizedValues = value
|
|
203
|
+
.map(toString)
|
|
204
|
+
.filter(val => !!val)
|
|
205
|
+
.map(val => `"${sanitizeLuceneQuery(val)}"`);
|
|
206
|
+
if (sanitizedValues.length < 1) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
newQuery += `-${key}:(${sanitizedValues.join(' OR ')})`;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
213
|
+
}
|
|
214
|
+
return [{ key, label: key, onClick: () => setQuery(newQuery) }];
|
|
215
|
+
})
|
|
216
|
+
});
|
|
217
|
+
result.push({
|
|
218
|
+
kind: 'submenu',
|
|
219
|
+
id: 'includes',
|
|
220
|
+
icon: _jsx(AddCircleOutline, {}),
|
|
221
|
+
label: t('hit.panel.include'),
|
|
222
|
+
items: (template?.keys ?? []).flatMap(key => {
|
|
223
|
+
let newQuery = `(${query}) AND `;
|
|
224
|
+
const value = get(record, key);
|
|
225
|
+
if (!value) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
else if (Array.isArray(value)) {
|
|
229
|
+
const sanitizedValues = value
|
|
230
|
+
.map(toString)
|
|
231
|
+
.filter(val => !!val)
|
|
232
|
+
.map(val => `"${sanitizeLuceneQuery(val)}"`);
|
|
233
|
+
if (sanitizedValues.length < 1) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
240
|
+
}
|
|
241
|
+
return [{ key, label: key, onClick: () => setQuery(newQuery) }];
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
result.push({ kind: 'divider', id: 'add-to-case-divider' });
|
|
247
|
+
result.push({
|
|
248
|
+
kind: 'item',
|
|
249
|
+
id: 'add-to-case',
|
|
250
|
+
icon: _jsx(CreateNewFolder, {}),
|
|
251
|
+
label: t('modal.cases.add_to_case'),
|
|
252
|
+
disabled: !record,
|
|
253
|
+
onClick: () => showModal(_jsx(AddToCaseModal, { records: records }), { maxHeight: '90vh' })
|
|
254
|
+
});
|
|
255
|
+
result.push({
|
|
256
|
+
kind: 'item',
|
|
257
|
+
id: 'create-case',
|
|
258
|
+
icon: _jsx(NoteAdd, {}),
|
|
259
|
+
label: t('modal.cases.create_case'),
|
|
260
|
+
disabled: !record,
|
|
261
|
+
onClick: () => showModal(_jsx(CreateCaseModal, { records: records }), { maxHeight: '90vh' })
|
|
262
|
+
});
|
|
263
|
+
return result;
|
|
264
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
265
|
+
}, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction, showModal, records]);
|
|
266
|
+
return (_jsx(ContextMenu, { id: "contextMenu", Component: Component, onOpen: onOpen, onClose: () => setAnalytic(null), items: items, children: children }));
|
|
267
|
+
};
|
|
268
|
+
export default RecordContextMenu;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -57,6 +57,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
|
|
|
57
57
|
getMatchingTemplate: mockGetMatchingTemplate
|
|
58
58
|
}))
|
|
59
59
|
}));
|
|
60
|
+
const mockShowModal = vi.fn();
|
|
60
61
|
const mockDispatchApi = vi.fn();
|
|
61
62
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
62
63
|
default: vi.fn(() => ({
|
|
@@ -82,6 +83,9 @@ vi.mock('plugins/store', () => ({
|
|
|
82
83
|
plugins: ['plugin1']
|
|
83
84
|
}
|
|
84
85
|
}));
|
|
86
|
+
vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
|
|
87
|
+
default: () => null
|
|
88
|
+
}));
|
|
85
89
|
// Mock MUI components
|
|
86
90
|
vi.mock('@mui/material', async () => {
|
|
87
91
|
const actual = await vi.importActual('@mui/material');
|
|
@@ -101,13 +105,14 @@ vi.mock('@mui/material', async () => {
|
|
|
101
105
|
});
|
|
102
106
|
// Import component after mocks
|
|
103
107
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
104
|
-
import {
|
|
108
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
105
109
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
110
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
106
111
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
107
112
|
import { I18nextProvider } from 'react-i18next';
|
|
108
113
|
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
109
114
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
110
|
-
import
|
|
115
|
+
import RecordContextMenu from './RecordContextMenu';
|
|
111
116
|
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
112
117
|
const mockConfig = {
|
|
113
118
|
lookups: {
|
|
@@ -115,16 +120,16 @@ const mockConfig = {
|
|
|
115
120
|
}
|
|
116
121
|
};
|
|
117
122
|
const mockApiContext = { config: mockConfig };
|
|
118
|
-
const
|
|
119
|
-
|
|
123
|
+
const mockRecordContext = {
|
|
124
|
+
records: {
|
|
120
125
|
'test-hit-1': createMockHit()
|
|
121
126
|
},
|
|
122
|
-
|
|
127
|
+
selectedRecords: []
|
|
123
128
|
};
|
|
124
129
|
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
125
130
|
// Test wrapper
|
|
126
131
|
const Wrapper = ({ children }) => {
|
|
127
|
-
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 }) }) }) }) }));
|
|
128
133
|
};
|
|
129
134
|
describe('HitContextMenu', () => {
|
|
130
135
|
let user;
|
|
@@ -132,11 +137,11 @@ describe('HitContextMenu', () => {
|
|
|
132
137
|
beforeEach(() => {
|
|
133
138
|
user = userEvent.setup();
|
|
134
139
|
vi.clearAllMocks();
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
mockRecordContext.selectedRecords.length = 0;
|
|
141
|
+
mockRecordContext.records['test-hit-1'] = createMockHit();
|
|
137
142
|
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
138
143
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
139
|
-
rerender = render(_jsx(Wrapper, { children: _jsx(
|
|
144
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
140
145
|
});
|
|
141
146
|
describe('Context Menu Initialization', () => {
|
|
142
147
|
it('should open menu on right-click', async () => {
|
|
@@ -200,13 +205,13 @@ describe('HitContextMenu', () => {
|
|
|
200
205
|
});
|
|
201
206
|
it('should disable "Open Hit" when hit is null', async () => {
|
|
202
207
|
act(() => {
|
|
203
|
-
|
|
208
|
+
mockRecordContext.records['test-hit-1'] = null;
|
|
204
209
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
205
210
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
206
211
|
});
|
|
207
212
|
await waitFor(() => {
|
|
208
213
|
const menuItems = screen.getAllByRole('menuitem');
|
|
209
|
-
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit
|
|
214
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
|
|
210
215
|
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
211
216
|
});
|
|
212
217
|
});
|
|
@@ -247,7 +252,7 @@ describe('HitContextMenu', () => {
|
|
|
247
252
|
skip_rationale: false
|
|
248
253
|
}
|
|
249
254
|
}));
|
|
250
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
255
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
251
256
|
act(() => {
|
|
252
257
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
253
258
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -305,7 +310,7 @@ describe('HitContextMenu', () => {
|
|
|
305
310
|
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
306
311
|
];
|
|
307
312
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
308
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
313
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
309
314
|
act(() => {
|
|
310
315
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
311
316
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -349,7 +354,7 @@ describe('HitContextMenu', () => {
|
|
|
349
354
|
});
|
|
350
355
|
it('should disable custom actions menu when no actions are available', async () => {
|
|
351
356
|
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
352
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
357
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
353
358
|
act(() => {
|
|
354
359
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
355
360
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -391,7 +396,7 @@ describe('HitContextMenu', () => {
|
|
|
391
396
|
skip_rationale: true
|
|
392
397
|
}
|
|
393
398
|
}));
|
|
394
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
399
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
395
400
|
act(() => {
|
|
396
401
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
397
402
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -459,7 +464,7 @@ describe('HitContextMenu', () => {
|
|
|
459
464
|
it('should call executeAction with action_id and hit query', async () => {
|
|
460
465
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
461
466
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
462
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
467
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
463
468
|
act(() => {
|
|
464
469
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
465
470
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -512,7 +517,7 @@ describe('HitContextMenu', () => {
|
|
|
512
517
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
513
518
|
keys: ['howler.detection', 'event.id']
|
|
514
519
|
}));
|
|
515
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
520
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
516
521
|
});
|
|
517
522
|
it('should render exclusion submenu with template keys', async () => {
|
|
518
523
|
act(() => {
|
|
@@ -560,7 +565,7 @@ describe('HitContextMenu', () => {
|
|
|
560
565
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
561
566
|
keys: ['howler.outline.indicators']
|
|
562
567
|
}));
|
|
563
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
568
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
564
569
|
act(() => {
|
|
565
570
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
566
571
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -603,7 +608,7 @@ describe('HitContextMenu', () => {
|
|
|
603
608
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
604
609
|
keys: []
|
|
605
610
|
}));
|
|
606
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
611
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
607
612
|
act(() => {
|
|
608
613
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
609
614
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -615,7 +620,7 @@ describe('HitContextMenu', () => {
|
|
|
615
620
|
});
|
|
616
621
|
it('should skip null field values in exclusion menu', async () => {
|
|
617
622
|
act(() => {
|
|
618
|
-
|
|
623
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
619
624
|
});
|
|
620
625
|
act(() => {
|
|
621
626
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -638,7 +643,7 @@ describe('HitContextMenu', () => {
|
|
|
638
643
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
639
644
|
keys: ['howler.detection', 'event.id']
|
|
640
645
|
}));
|
|
641
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
646
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
642
647
|
});
|
|
643
648
|
it('should render inclusion submenu with template keys', async () => {
|
|
644
649
|
act(() => {
|
|
@@ -686,7 +691,7 @@ describe('HitContextMenu', () => {
|
|
|
686
691
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
687
692
|
keys: ['howler.outline.indicators']
|
|
688
693
|
}));
|
|
689
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
694
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
690
695
|
act(() => {
|
|
691
696
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
692
697
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -729,7 +734,7 @@ describe('HitContextMenu', () => {
|
|
|
729
734
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
730
735
|
keys: []
|
|
731
736
|
}));
|
|
732
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
737
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
733
738
|
act(() => {
|
|
734
739
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
735
740
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -741,7 +746,7 @@ describe('HitContextMenu', () => {
|
|
|
741
746
|
});
|
|
742
747
|
it('should skip null field values in inclusion menu', async () => {
|
|
743
748
|
act(() => {
|
|
744
|
-
|
|
749
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
745
750
|
});
|
|
746
751
|
act(() => {
|
|
747
752
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -760,24 +765,24 @@ describe('HitContextMenu', () => {
|
|
|
760
765
|
});
|
|
761
766
|
});
|
|
762
767
|
describe('Multiple Hit Selection', () => {
|
|
763
|
-
it('should use
|
|
768
|
+
it('should use selectedRecords when current hit is included', async () => {
|
|
764
769
|
act(() => {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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']);
|
|
768
773
|
mockGetSelectedId.mockReturnValue('hit-1');
|
|
769
774
|
});
|
|
770
775
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
771
776
|
await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
|
|
772
|
-
// The component should use
|
|
777
|
+
// The component should use selectedRecords for actions
|
|
773
778
|
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
774
779
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
775
780
|
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
776
781
|
});
|
|
777
|
-
it('should use only current hit when not in
|
|
782
|
+
it('should use only current hit when not in selectedRecords', async () => {
|
|
778
783
|
act(() => {
|
|
779
|
-
|
|
780
|
-
|
|
784
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
785
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
|
|
781
786
|
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
782
787
|
});
|
|
783
788
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -796,12 +801,12 @@ describe('HitContextMenu', () => {
|
|
|
796
801
|
});
|
|
797
802
|
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
798
803
|
await waitFor(() => {
|
|
799
|
-
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(
|
|
804
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
800
805
|
});
|
|
801
806
|
});
|
|
802
807
|
it('should call getMatchingTemplate when menu opens', async () => {
|
|
803
808
|
await waitFor(() => {
|
|
804
|
-
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(
|
|
809
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
805
810
|
});
|
|
806
811
|
});
|
|
807
812
|
it('should reset state when menu closes', async () => {
|
|
@@ -834,7 +839,7 @@ describe('HitContextMenu', () => {
|
|
|
834
839
|
describe('Edge Cases and Error Handling', () => {
|
|
835
840
|
it('should not crash when hit is null', async () => {
|
|
836
841
|
act(() => {
|
|
837
|
-
|
|
842
|
+
mockRecordContext.records = {};
|
|
838
843
|
});
|
|
839
844
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
840
845
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -845,7 +850,7 @@ describe('HitContextMenu', () => {
|
|
|
845
850
|
});
|
|
846
851
|
it('should not render exclusion menu when template is null', async () => {
|
|
847
852
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
848
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
853
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
849
854
|
act(() => {
|
|
850
855
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
851
856
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -859,7 +864,7 @@ describe('HitContextMenu', () => {
|
|
|
859
864
|
});
|
|
860
865
|
it('should not render inclusion menu when template is null', async () => {
|
|
861
866
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
862
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
867
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
863
868
|
act(() => {
|
|
864
869
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
865
870
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -873,7 +878,7 @@ describe('HitContextMenu', () => {
|
|
|
873
878
|
});
|
|
874
879
|
it('should handle API failure gracefully', async () => {
|
|
875
880
|
mockDispatchApi.mockResolvedValue(null);
|
|
876
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
881
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
877
882
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
878
883
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
879
884
|
await waitFor(() => {
|
|
@@ -884,7 +889,7 @@ describe('HitContextMenu', () => {
|
|
|
884
889
|
});
|
|
885
890
|
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
886
891
|
act(() => {
|
|
887
|
-
|
|
892
|
+
mockRecordContext.records['test-hit-1'].howler.analytic = null;
|
|
888
893
|
});
|
|
889
894
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
890
895
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -903,6 +908,56 @@ describe('HitContextMenu', () => {
|
|
|
903
908
|
expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
|
|
904
909
|
});
|
|
905
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
|
+
});
|
|
906
961
|
describe('Role-Based Action Permissions', () => {
|
|
907
962
|
afterEach(() => {
|
|
908
963
|
// Reset to default user with required roles
|
|
@@ -922,7 +977,7 @@ describe('HitContextMenu', () => {
|
|
|
922
977
|
});
|
|
923
978
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
924
979
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
925
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
980
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
926
981
|
act(() => {
|
|
927
982
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
928
983
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -941,7 +996,7 @@ describe('HitContextMenu', () => {
|
|
|
941
996
|
});
|
|
942
997
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
943
998
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
944
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
999
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
945
1000
|
act(() => {
|
|
946
1001
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
947
1002
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -960,7 +1015,7 @@ describe('HitContextMenu', () => {
|
|
|
960
1015
|
});
|
|
961
1016
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
962
1017
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
963
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
1018
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
964
1019
|
act(() => {
|
|
965
1020
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
966
1021
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -978,7 +1033,7 @@ describe('HitContextMenu', () => {
|
|
|
978
1033
|
}
|
|
979
1034
|
});
|
|
980
1035
|
mockDispatchApi.mockResolvedValue({ items: [] });
|
|
981
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
1036
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
982
1037
|
act(() => {
|
|
983
1038
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
984
1039
|
fireEvent.contextMenu(contextMenuWrapper);
|