@cccsaurora/howler-ui 2.18.0-dev.736 → 2.18.0-dev.737
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/index.d.ts +0 -2
- package/api/index.js +2 -4
- package/api/search/facet/hit.d.ts +3 -1
- package/api/search/facet/index.d.ts +1 -3
- package/api/search/index.d.ts +1 -2
- package/api/search/index.js +1 -2
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +7 -39
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +11 -23
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +3 -3
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/HitProvider.d.ts +22 -0
- package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
- package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
- package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
- package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +70 -51
- package/components/app/providers/ModalProvider.d.ts +0 -1
- package/components/app/providers/ParameterProvider.d.ts +2 -9
- package/components/app/providers/ParameterProvider.js +240 -165
- package/components/app/providers/ParameterProvider.test.js +94 -346
- package/components/app/providers/UserListProvider.js +8 -28
- package/components/elements/PluginTypography.d.ts +1 -2
- package/components/elements/PluginTypography.js +2 -3
- package/components/elements/UserList.d.ts +2 -5
- package/components/elements/UserList.js +8 -18
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +0 -2
- package/components/elements/display/icons/BundleButton.d.ts +6 -0
- package/components/elements/display/icons/BundleButton.js +32 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +0 -1
- package/components/elements/hit/HitBanner.js +49 -29
- package/components/elements/hit/HitCard.d.ts +0 -2
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
- package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
- package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +0 -1
- package/components/elements/hit/HitOutline.js +3 -3
- package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
- package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
- package/components/elements/hit/HitRelated.d.ts +6 -0
- package/components/elements/hit/HitRelated.js +7 -0
- package/components/elements/hit/HitSummary.d.ts +1 -2
- package/components/elements/hit/HitSummary.js +5 -6
- package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
- package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/view/ViewTitle.d.ts +0 -1
- package/components/elements/view/ViewTitle.js +2 -9
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
- package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
- package/components/hooks/useMyPreferences.js +1 -10
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +1 -4
- package/components/hooks/useMyTheme.js +2 -9
- package/components/hooks/useParamState.test.js +4 -3
- 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/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/BundleDocumentation.d.ts +3 -0
- package/components/routes/help/BundleDocumentation.js +12 -0
- package/components/routes/help/HitBannerDocumentation.js +0 -1
- package/components/routes/help/HitDocumentation.js +3 -1
- package/components/routes/help/markdown/en/bundles.md.js +1 -0
- package/components/routes/help/markdown/fr/bundles.md.js +1 -0
- package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
- package/components/routes/hits/search/BundleParentMenu.js +32 -0
- package/components/routes/hits/search/BundleScroller.d.ts +2 -0
- package/components/routes/hits/search/BundleScroller.js +6 -0
- package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
- package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
- package/components/routes/hits/search/HitContextMenu.js +227 -0
- package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
- package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
- package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
- package/components/routes/hits/search/InformationPane.d.ts +0 -1
- package/components/routes/hits/search/InformationPane.js +60 -47
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +1 -2
- package/components/routes/hits/search/QuerySettings.test.js +9 -14
- package/components/routes/hits/search/SearchPane.js +49 -26
- 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 +4 -5
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +18 -20
- package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
- package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
- package/components/routes/hits/view/HitViewer.js +13 -12
- package/components/routes/home/ViewCard.js +41 -47
- package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/views/ViewComposer.js +19 -46
- package/locales/en/translation.json +3 -89
- package/locales/fr/translation.json +3 -87
- package/models/WithMetadata.d.ts +1 -2
- package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
- package/models/entities/generated/Hit.d.ts +0 -1
- package/models/entities/generated/Howler.d.ts +4 -0
- package/models/entities/generated/Rule.d.ts +10 -2
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/View.d.ts +0 -1
- package/package.json +2 -19
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +1 -2
- package/tests/mocks.d.ts +1 -11
- package/tests/mocks.js +7 -12
- package/tests/server-handlers.js +1 -6
- package/tests/utils.d.ts +0 -4
- package/tests/utils.js +0 -20
- package/utils/constants.d.ts +3 -3
- package/utils/hitFunctions.d.ts +1 -2
- package/utils/hitFunctions.js +4 -4
- package/utils/viewUtils.js +0 -3
- package/api/search/case.d.ts +0 -4
- package/api/search/case.js +0 -8
- package/api/v2/case/index.d.ts +0 -8
- package/api/v2/case/index.js +0 -20
- package/api/v2/case/items.d.ts +0 -6
- package/api/v2/case/items.js +0 -18
- package/api/v2/index.d.ts +0 -4
- package/api/v2/index.js +0 -6
- package/api/v2/search/facet.d.ts +0 -3
- package/api/v2/search/facet.js +0 -12
- package/api/v2/search/index.d.ts +0 -5
- package/api/v2/search/index.js +0 -24
- package/components/app/providers/RecordProvider.d.ts +0 -23
- package/components/elements/ContextMenu.d.ts +0 -56
- package/components/elements/ContextMenu.js +0 -109
- package/components/elements/ContextMenu.test.js +0 -215
- package/components/elements/ObjectDetails.d.ts +0 -6
- package/components/elements/case/CaseCard.d.ts +0 -12
- package/components/elements/case/CaseCard.js +0 -42
- package/components/elements/case/CasePreview.d.ts +0 -6
- package/components/elements/case/CasePreview.js +0 -17
- package/components/elements/case/StatusIcon.d.ts +0 -5
- package/components/elements/case/StatusIcon.js +0 -13
- package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
- package/components/elements/hit/elements/AnalyticLink.js +0 -22
- package/components/elements/hit/related/RelatedRecords.js +0 -63
- package/components/elements/observable/ObservableCard.d.ts +0 -6
- package/components/elements/observable/ObservableCard.js +0 -22
- package/components/elements/observable/ObservablePreview.d.ts +0 -6
- package/components/elements/observable/ObservablePreview.js +0 -12
- package/components/elements/record/RecordContextMenu.js +0 -247
- package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
- package/components/elements/record/RecordRelated.d.ts +0 -7
- package/components/elements/record/RecordRelated.js +0 -34
- package/components/hooks/useRelatedRecords.d.ts +0 -13
- package/components/hooks/useRelatedRecords.js +0 -32
- package/components/routes/cases/CaseViewer.d.ts +0 -2
- package/components/routes/cases/CaseViewer.js +0 -22
- package/components/routes/cases/Cases.d.ts +0 -2
- package/components/routes/cases/Cases.js +0 -101
- package/components/routes/cases/constants.d.ts +0 -5
- package/components/routes/cases/constants.js +0 -5
- package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
- package/components/routes/cases/detail/AlertPanel.js +0 -33
- package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
- package/components/routes/cases/detail/CaseAssets.js +0 -104
- package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
- package/components/routes/cases/detail/CaseAssets.test.js +0 -167
- package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
- package/components/routes/cases/detail/CaseDashboard.js +0 -66
- package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
- package/components/routes/cases/detail/CaseDetails.js +0 -61
- package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
- package/components/routes/cases/detail/CaseOverview.js +0 -43
- package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
- package/components/routes/cases/detail/CaseSidebar.js +0 -107
- package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
- package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
- package/components/routes/cases/detail/CaseTask.d.ts +0 -11
- package/components/routes/cases/detail/CaseTask.js +0 -57
- package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
- package/components/routes/cases/detail/CaseTimeline.js +0 -106
- package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
- package/components/routes/cases/detail/CaseTimeline.test.js +0 -227
- package/components/routes/cases/detail/ItemPage.d.ts +0 -6
- package/components/routes/cases/detail/ItemPage.js +0 -99
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
- package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
- package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
- package/components/routes/cases/detail/TaskPanel.js +0 -52
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
- package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
- package/components/routes/cases/detail/assets/Asset.js +0 -12
- package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
- package/components/routes/cases/detail/assets/Asset.test.js +0 -72
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
- package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
- package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
- package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
- package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
- package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
- package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
- package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
- package/components/routes/cases/detail/sidebar/utils.js +0 -29
- package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
- package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
- package/components/routes/cases/hooks/useCase.d.ts +0 -13
- package/components/routes/cases/hooks/useCase.js +0 -51
- package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
- package/components/routes/cases/modals/AddToCaseModal.js +0 -62
- package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
- package/components/routes/cases/modals/RenameItemModal.js +0 -48
- package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
- package/components/routes/cases/modals/ResolveModal.js +0 -115
- package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
- package/components/routes/cases/modals/ResolveModal.test.js +0 -384
- package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
- package/components/routes/hits/search/shared/IndexPicker.js +0 -20
- package/components/routes/observables/ObservableViewer.d.ts +0 -7
- package/components/routes/observables/ObservableViewer.js +0 -27
- package/models/entities/generated/AttachmentsFile.d.ts +0 -12
- package/models/entities/generated/Case.d.ts +0 -28
- package/models/entities/generated/DestinationOriginal.d.ts +0 -19
- package/models/entities/generated/EmailAttachment.d.ts +0 -8
- package/models/entities/generated/EmailParent.d.ts +0 -19
- package/models/entities/generated/Enrichments.d.ts +0 -7
- package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
- package/models/entities/generated/HttpResponse.d.ts +0 -11
- package/models/entities/generated/Item.d.ts +0 -9
- package/models/entities/generated/Observable.d.ts +0 -85
- package/models/entities/generated/ObservableCloud.d.ts +0 -20
- package/models/entities/generated/ObservableDestination.d.ts +0 -23
- package/models/entities/generated/ObservableEmail.d.ts +0 -30
- package/models/entities/generated/ObservableFile.d.ts +0 -36
- package/models/entities/generated/ObservableHowler.d.ts +0 -43
- package/models/entities/generated/ObservableHttp.d.ts +0 -11
- package/models/entities/generated/ObservableObserver.d.ts +0 -21
- package/models/entities/generated/ObservableOrganization.d.ts +0 -7
- package/models/entities/generated/ObservableProcess.d.ts +0 -34
- package/models/entities/generated/ObservableSource.d.ts +0 -23
- package/models/entities/generated/ObservableThreat.d.ts +0 -21
- package/models/entities/generated/ObservableTls.d.ts +0 -12
- package/models/entities/generated/ObserverIngress.d.ts +0 -9
- package/models/entities/generated/Task.d.ts +0 -10
- package/utils/typeUtils.d.ts +0 -7
- package/utils/typeUtils.js +0 -27
- /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
- /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
- /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
- /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
- /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronLeft, Close, ManageSearch } from '@mui/icons-material';
|
|
3
3
|
import { Box, Card, Checkbox, Collapse, Drawer, Fab, IconButton, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
4
|
+
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
5
|
+
import HitSearchProvider, { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
4
6
|
import ParameterProvider, { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
5
|
-
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
6
|
-
import RecordSearchProvider, { RecordSearchContext } from '@cccsaurora/howler-ui/components/app/providers/RecordSearchProvider';
|
|
7
7
|
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
8
8
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
9
9
|
import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
|
|
@@ -32,14 +32,14 @@ const HitBrowser = () => {
|
|
|
32
32
|
const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
|
|
33
33
|
const setOffset = useContextSelector(ParameterContext, ctx => ctx.setOffset);
|
|
34
34
|
const selectedViews = useContextSelector(ParameterContext, ctx => ctx.views);
|
|
35
|
-
const selectedHits = useContextSelector(
|
|
36
|
-
const addHitToSelection = useContextSelector(
|
|
37
|
-
const removeHitFromSelection = useContextSelector(
|
|
38
|
-
const clearSelectedHits = useContextSelector(
|
|
35
|
+
const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
|
|
36
|
+
const addHitToSelection = useContextSelector(HitContext, ctx => ctx.addHitToSelection);
|
|
37
|
+
const removeHitFromSelection = useContextSelector(HitContext, ctx => ctx.removeHitFromSelection);
|
|
38
|
+
const clearSelectedHits = useContextSelector(HitContext, ctx => ctx.clearSelectedHits);
|
|
39
39
|
const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
|
|
40
40
|
const forceDrawer = useMyLocalStorageItem(StorageKey.FORCE_DRAWER, false)[0];
|
|
41
|
-
const displayType = useContextSelector(
|
|
42
|
-
const response = useContextSelector(
|
|
41
|
+
const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
|
|
42
|
+
const response = useContextSelector(HitSearchContext, ctx => ctx.response);
|
|
43
43
|
const location = useLocation();
|
|
44
44
|
const routeParams = useParams();
|
|
45
45
|
const [show, setShow] = useState(!!selected);
|
|
@@ -128,6 +128,6 @@ const HitBrowser = () => {
|
|
|
128
128
|
}, children: _jsx(ChevronLeft, { sx: { transition: 'rotate 250ms', rotate: show ? '180deg' : '0deg' } }) }))] }));
|
|
129
129
|
};
|
|
130
130
|
const HitBrowserProvider = () => {
|
|
131
|
-
return (_jsx(ParameterProvider, { children: _jsx(
|
|
131
|
+
return (_jsx(ParameterProvider, { children: _jsx(HitSearchProvider, { children: _jsx(HitBrowser, {}) }) }));
|
|
132
132
|
};
|
|
133
133
|
export default HitBrowserProvider;
|
package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts}
RENAMED
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
/**
|
|
4
4
|
* Props for the HitContextMenu component
|
|
5
5
|
*/
|
|
6
|
-
interface
|
|
6
|
+
interface HitContextMenuProps {
|
|
7
7
|
/**
|
|
8
8
|
* Function to extract the hit ID from a mouse event
|
|
9
9
|
*/
|
|
@@ -18,5 +18,5 @@ interface RecordContextMenuProps {
|
|
|
18
18
|
* Provides quick access to common hit actions including assessment, voting,
|
|
19
19
|
* transitions, and exclusion filters based on template fields.
|
|
20
20
|
*/
|
|
21
|
-
declare const
|
|
22
|
-
export default
|
|
21
|
+
declare const HitContextMenu: FC<PropsWithChildren<HitContextMenuProps>>;
|
|
22
|
+
export default HitContextMenu;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { AddCircleOutline, Assignment, Edit, HowToVote, KeyboardArrowRight, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
|
|
3
|
+
import { Box, Divider, Fade, ListItemIcon, ListItemText, Menu, MenuItem, MenuList, Paper } from '@mui/material';
|
|
4
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
6
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
7
|
+
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
8
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
9
|
+
import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
|
|
10
|
+
import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
|
|
11
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
12
|
+
import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
|
|
13
|
+
import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
|
|
14
|
+
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
15
|
+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
16
|
+
import { useTranslation } from 'react-i18next';
|
|
17
|
+
import { usePluginStore } from 'react-pluggable';
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
import { useContextSelector } from 'use-context-selector';
|
|
20
|
+
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
21
|
+
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
22
|
+
/**
|
|
23
|
+
* Order in which action types should be displayed in the context menu
|
|
24
|
+
*/
|
|
25
|
+
const ORDER = ['assessment', 'vote', 'action'];
|
|
26
|
+
/**
|
|
27
|
+
* The margin at the bottom of the screen by which the context menu should be inverted.
|
|
28
|
+
* That is, if right clicking within this many pixels of the bottom, render the context menu to the top right
|
|
29
|
+
* of the pointer instead of the bottom right.
|
|
30
|
+
*/
|
|
31
|
+
const CONTEXTMENU_MARGIN = 350;
|
|
32
|
+
/**
|
|
33
|
+
* Icon mapping for different action types
|
|
34
|
+
*/
|
|
35
|
+
const ICON_MAP = {
|
|
36
|
+
assessment: _jsx(Assignment, {}),
|
|
37
|
+
vote: _jsx(HowToVote, {}),
|
|
38
|
+
action: _jsx(Edit, {})
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Context menu component for hit operations.
|
|
42
|
+
* Provides quick access to common hit actions including assessment, voting,
|
|
43
|
+
* transitions, and exclusion filters based on template fields.
|
|
44
|
+
*/
|
|
45
|
+
const HitContextMenu = ({ children, getSelectedId, Component = Box }) => {
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
const { dispatchApi } = useMyApi();
|
|
48
|
+
const { executeAction } = useMyActionFunctions();
|
|
49
|
+
const { config } = useContext(ApiConfigContext);
|
|
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 hit = useContextSelector(HitContext, ctx => ctx.hits[id]);
|
|
56
|
+
const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
|
|
57
|
+
const [analytic, setAnalytic] = useState(null);
|
|
58
|
+
const [template, setTemplate] = useState(null);
|
|
59
|
+
const [anchorEl, setAnchorEl] = useState();
|
|
60
|
+
const [transformProps, setTransformProps] = useState({});
|
|
61
|
+
const [actions, setActions] = useState([]);
|
|
62
|
+
const [show, setShow] = useState({});
|
|
63
|
+
const hits = useMemo(() => (selectedHits.some(_hit => _hit.howler.id === hit?.howler.id) ? selectedHits : [hit]), [hit, selectedHits]);
|
|
64
|
+
const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
|
|
65
|
+
/**
|
|
66
|
+
* Handles right-click context menu events.
|
|
67
|
+
* Opens the context menu at the click location and loads available actions.
|
|
68
|
+
*/
|
|
69
|
+
const onContextMenu = useCallback(async (event) => {
|
|
70
|
+
if (anchorEl) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
setAnchorEl(null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
const _id = getSelectedId(event);
|
|
77
|
+
setId(_id);
|
|
78
|
+
if (window.innerHeight - event.clientY < 300) {
|
|
79
|
+
setTransformProps({
|
|
80
|
+
position: 'fixed',
|
|
81
|
+
bottom: `${window.innerHeight - event.clientY}px !important`,
|
|
82
|
+
top: 'unset !important',
|
|
83
|
+
left: `${event.clientX}px !important`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
setTransformProps({
|
|
88
|
+
position: 'fixed',
|
|
89
|
+
top: `${event.clientY}px !important`,
|
|
90
|
+
left: `${event.clientX}px !important`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
setAnchorEl(event.target);
|
|
94
|
+
const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*' }), { throwError: false }))
|
|
95
|
+
?.items;
|
|
96
|
+
if (_actions) {
|
|
97
|
+
setActions(_actions);
|
|
98
|
+
}
|
|
99
|
+
}, [anchorEl, dispatchApi, getSelectedId]);
|
|
100
|
+
const rowStatus = useMemo(() => ({
|
|
101
|
+
assessment: canAssess,
|
|
102
|
+
vote: canVote
|
|
103
|
+
}), [canAssess, canVote]);
|
|
104
|
+
const pluginActions = howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.actions`, hits));
|
|
105
|
+
/**
|
|
106
|
+
* Generates grouped action entries for the context menu.
|
|
107
|
+
* Combines transitions, plugin actions, votes, and assessments based on permissions.
|
|
108
|
+
*/
|
|
109
|
+
const entries = useMemo(() => {
|
|
110
|
+
let _actions = [...availableTransitions, ...pluginActions];
|
|
111
|
+
if (canVote) {
|
|
112
|
+
_actions = [
|
|
113
|
+
..._actions,
|
|
114
|
+
...VOTE_OPTIONS.map(option => ({ ...option, actionFunction: () => vote(option.name.toLowerCase()) }))
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
if (config.lookups?.['howler.assessment'] && canAssess) {
|
|
118
|
+
_actions = [
|
|
119
|
+
..._actions,
|
|
120
|
+
...config.lookups['howler.assessment']
|
|
121
|
+
.filter(_assessment => analytic?.triage_settings?.valid_assessments
|
|
122
|
+
? analytic.triage_settings?.valid_assessments.includes(_assessment)
|
|
123
|
+
: true)
|
|
124
|
+
.sort((a, b) => +TOP_ROW.includes(b) - +TOP_ROW.includes(a))
|
|
125
|
+
.map(assessment => ({
|
|
126
|
+
type: 'assessment',
|
|
127
|
+
name: assessment,
|
|
128
|
+
actionFunction: async () => {
|
|
129
|
+
await assess(assessment, analytic?.triage_settings?.skip_rationale);
|
|
130
|
+
}
|
|
131
|
+
}))
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
return Object.entries(groupBy(_actions, 'type')).sort(([a], [b]) => ORDER.indexOf(a) - ORDER.indexOf(b));
|
|
135
|
+
}, [analytic, assess, availableTransitions, canAssess, canVote, config.lookups, vote, pluginActions]);
|
|
136
|
+
/**
|
|
137
|
+
* Calculates appropriate styles for submenu positioning.
|
|
138
|
+
* Adjusts position based on available screen space to prevent overflow.
|
|
139
|
+
*/
|
|
140
|
+
const calculateSubMenuStyles = useCallback((parent) => {
|
|
141
|
+
const baseStyles = { position: 'absolute', maxHeight: '300px', overflow: 'auto' };
|
|
142
|
+
const defaultStyles = { ...baseStyles, top: 0, left: '100%' };
|
|
143
|
+
if (!parent) {
|
|
144
|
+
return defaultStyles;
|
|
145
|
+
}
|
|
146
|
+
const parentBounds = parent.getBoundingClientRect();
|
|
147
|
+
if (window.innerHeight - parentBounds.y < CONTEXTMENU_MARGIN) {
|
|
148
|
+
return { ...baseStyles, bottom: 0, left: '100%' };
|
|
149
|
+
}
|
|
150
|
+
return defaultStyles;
|
|
151
|
+
}, []);
|
|
152
|
+
// Load analytic and template data when a hit is selected
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!hit?.howler.analytic) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
getMatchingAnalytic(hit).then(setAnalytic);
|
|
158
|
+
getMatchingTemplate(hit).then(setTemplate);
|
|
159
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
160
|
+
}, [hit]);
|
|
161
|
+
// Reset menu state when context menu is closed
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!anchorEl) {
|
|
164
|
+
setShow({});
|
|
165
|
+
setAnalytic(null);
|
|
166
|
+
}
|
|
167
|
+
}, [anchorEl]);
|
|
168
|
+
return (_jsxs(Component, { id: "contextMenu", onContextMenu: onContextMenu, children: [children, _jsxs(Menu, { id: "hit-menu", open: !!anchorEl, anchorEl: anchorEl, onClose: () => setAnchorEl(null), slotProps: {
|
|
169
|
+
paper: {
|
|
170
|
+
sx: {
|
|
171
|
+
...transformProps,
|
|
172
|
+
overflow: 'visible !important'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, MenuListProps: { dense: true, sx: { minWidth: '250px' } }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, onClick: () => setAnchorEl(null), children: [_jsxs(MenuItem, { component: Link, to: `/hits/${hit?.howler.id}`, disabled: !hit, children: [_jsx(ListItemIcon, { children: _jsx(OpenInNew, {}) }), _jsx(ListItemText, { children: t('hit.panel.open') })] }), _jsxs(MenuItem, { component: Link, to: `/analytics/${analytic?.analytic_id}`, disabled: !analytic, children: [_jsx(ListItemIcon, { children: _jsx(QueryStats, {}) }), _jsx(ListItemText, { children: t('hit.panel.analytic.open') })] }), _jsx(Divider, {}), entries.map(([type, items]) => (_jsxs(MenuItem, { id: `${type}-menu-item`, sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, [type]: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, [type]: null })), disabled: rowStatus[type] === false, children: [_jsx(ListItemIcon, { children: ICON_MAP[type] ?? _jsx(Terminal, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t(`hit.details.actions.${type}`) }), rowStatus[type] !== false && (_jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } })), _jsx(Fade, { in: !!show[type], unmountOnExit: true, children: _jsx(Paper, { id: `${type}-submenu`, sx: calculateSubMenuStyles(show[type]), elevation: 8, children: _jsx(MenuList, { sx: { p: 0, borderTopLeftRadius: 0 }, dense: true, role: "group", children: items.map(a => (_jsx(MenuItem, { value: a.name, onClick: a.actionFunction, children: a.i18nKey ? t(a.i18nKey) : capitalize(a.name) }, a.name))) }) }) })] }, type))), _jsxs(MenuItem, { id: "actions-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, actions: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, actions: null })), disabled: actions.length < 1, children: [_jsx(ListItemIcon, { children: _jsx(SettingsSuggest, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('route.actions.change') }), actions.length > 0 && _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.actions, unmountOnExit: true, children: _jsx(Paper, { id: "actions-submenu", sx: calculateSubMenuStyles(show.actions), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: actions.map(action => (_jsx(MenuItem, { onClick: () => executeAction(action.action_id, `howler.id:${hit?.howler.id}`), children: _jsx(ListItemText, { children: action.name }) }, action.action_id))) }) }) })] }), !isEmpty(template?.keys ?? []) && setQuery && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(MenuItem, { id: "excludes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, excludes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, excludes: null })), children: [_jsx(ListItemIcon, { children: _jsx(RemoveCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.exclude') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.excludes, unmountOnExit: true, children: _jsx(Paper, { id: "excludes-submenu", sx: calculateSubMenuStyles(show.excludes), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
|
|
176
|
+
// Build exclusion query based on current query and field value
|
|
177
|
+
let newQuery = '';
|
|
178
|
+
if (query !== DEFAULT_QUERY) {
|
|
179
|
+
newQuery = `(${query}) AND `;
|
|
180
|
+
}
|
|
181
|
+
const value = get(hit, key);
|
|
182
|
+
if (!value) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
else if (Array.isArray(value)) {
|
|
186
|
+
// Handle array values by excluding all items
|
|
187
|
+
const sanitizedValues = value
|
|
188
|
+
.map(toString)
|
|
189
|
+
.filter(val => !!val)
|
|
190
|
+
.map(val => `"${sanitizeLuceneQuery(val)}"`);
|
|
191
|
+
if (sanitizedValues.length < 1) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
newQuery += `-${key}:(${sanitizedValues.join(' OR ')})`;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Handle single value
|
|
198
|
+
newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
199
|
+
}
|
|
200
|
+
return (_jsx(MenuItem, { onClick: () => setQuery(newQuery), children: _jsx(ListItemText, { children: key }) }, key));
|
|
201
|
+
}) }) }) })] }), _jsxs(MenuItem, { id: "includes-menu-item", sx: { position: 'relative' }, onMouseEnter: ev => setShow(_show => ({ ..._show, includes: ev.target })), onMouseLeave: () => setShow(_show => ({ ..._show, includes: null })), children: [_jsx(ListItemIcon, { children: _jsx(AddCircleOutline, {}) }), _jsx(ListItemText, { sx: { flex: 1 }, children: t('hit.panel.include') }), _jsx(KeyboardArrowRight, { fontSize: "small", sx: { color: 'text.secondary', mr: -1 } }), _jsx(Fade, { in: !!show.includes, unmountOnExit: true, children: _jsx(Paper, { id: "includes-submenu", sx: calculateSubMenuStyles(show.includes), elevation: 8, children: _jsx(MenuList, { sx: { p: 0 }, dense: true, role: "group", children: template?.keys.map(key => {
|
|
202
|
+
// Build inclusion query based on current query and field
|
|
203
|
+
// If default, we include default query
|
|
204
|
+
let newQuery = `(${query}) AND `;
|
|
205
|
+
const value = get(hit, key);
|
|
206
|
+
if (!value) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
else if (Array.isArray(value)) {
|
|
210
|
+
// Handle array values by including all items
|
|
211
|
+
const sanitizedValues = value
|
|
212
|
+
.map(toString)
|
|
213
|
+
.filter(val => !!val)
|
|
214
|
+
.map(val => `"${sanitizeLuceneQuery(val)}"`);
|
|
215
|
+
if (sanitizedValues.length < 1) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Handle single value
|
|
222
|
+
newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
|
|
223
|
+
}
|
|
224
|
+
return (_jsx(MenuItem, { onClick: () => setQuery(newQuery), children: _jsx(ListItemText, { children: key }) }, key));
|
|
225
|
+
}) }) }) })] })] }))] })] }));
|
|
226
|
+
};
|
|
227
|
+
export default HitContextMenu;
|
|
@@ -47,7 +47,6 @@ vi.mock('components/app/hooks/useMatchers', () => ({
|
|
|
47
47
|
getMatchingTemplate: mockGetMatchingTemplate
|
|
48
48
|
}))
|
|
49
49
|
}));
|
|
50
|
-
const mockShowModal = vi.fn();
|
|
51
50
|
const mockDispatchApi = vi.fn();
|
|
52
51
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
53
52
|
default: vi.fn(() => ({
|
|
@@ -73,9 +72,6 @@ vi.mock('plugins/store', () => ({
|
|
|
73
72
|
plugins: ['plugin1']
|
|
74
73
|
}
|
|
75
74
|
}));
|
|
76
|
-
vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
|
|
77
|
-
default: () => null
|
|
78
|
-
}));
|
|
79
75
|
// Mock MUI components
|
|
80
76
|
vi.mock('@mui/material', async () => {
|
|
81
77
|
const actual = await vi.importActual('@mui/material');
|
|
@@ -95,14 +91,13 @@ vi.mock('@mui/material', async () => {
|
|
|
95
91
|
});
|
|
96
92
|
// Import component after mocks
|
|
97
93
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
98
|
-
import {
|
|
94
|
+
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
99
95
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
100
|
-
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
101
96
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
102
97
|
import { I18nextProvider } from 'react-i18next';
|
|
103
98
|
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
104
99
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
105
|
-
import
|
|
100
|
+
import HitContextMenu from './HitContextMenu';
|
|
106
101
|
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
107
102
|
const mockConfig = {
|
|
108
103
|
lookups: {
|
|
@@ -110,16 +105,16 @@ const mockConfig = {
|
|
|
110
105
|
}
|
|
111
106
|
};
|
|
112
107
|
const mockApiContext = { config: mockConfig };
|
|
113
|
-
const
|
|
114
|
-
|
|
108
|
+
const mockHitContext = {
|
|
109
|
+
hits: {
|
|
115
110
|
'test-hit-1': createMockHit()
|
|
116
111
|
},
|
|
117
|
-
|
|
112
|
+
selectedHits: []
|
|
118
113
|
};
|
|
119
114
|
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
120
115
|
// Test wrapper
|
|
121
116
|
const Wrapper = ({ children }) => {
|
|
122
|
-
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(
|
|
117
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(HitContext.Provider, { value: mockHitContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
|
|
123
118
|
};
|
|
124
119
|
describe('HitContextMenu', () => {
|
|
125
120
|
let user;
|
|
@@ -127,11 +122,11 @@ describe('HitContextMenu', () => {
|
|
|
127
122
|
beforeEach(() => {
|
|
128
123
|
user = userEvent.setup();
|
|
129
124
|
vi.clearAllMocks();
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
mockHitContext.selectedHits.length = 0;
|
|
126
|
+
mockHitContext.hits['test-hit-1'] = createMockHit();
|
|
132
127
|
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
133
128
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
134
|
-
rerender = render(_jsx(Wrapper, { children: _jsx(
|
|
129
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
135
130
|
});
|
|
136
131
|
describe('Context Menu Initialization', () => {
|
|
137
132
|
it('should open menu on right-click', async () => {
|
|
@@ -195,13 +190,13 @@ describe('HitContextMenu', () => {
|
|
|
195
190
|
});
|
|
196
191
|
it('should disable "Open Hit" when hit is null', async () => {
|
|
197
192
|
act(() => {
|
|
198
|
-
|
|
193
|
+
mockHitContext.hits['test-hit-1'] = null;
|
|
199
194
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
200
195
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
201
196
|
});
|
|
202
197
|
await waitFor(() => {
|
|
203
198
|
const menuItems = screen.getAllByRole('menuitem');
|
|
204
|
-
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
|
|
199
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
|
|
205
200
|
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
206
201
|
});
|
|
207
202
|
});
|
|
@@ -242,7 +237,7 @@ describe('HitContextMenu', () => {
|
|
|
242
237
|
skip_rationale: false
|
|
243
238
|
}
|
|
244
239
|
}));
|
|
245
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
240
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
246
241
|
act(() => {
|
|
247
242
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
248
243
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -300,7 +295,7 @@ describe('HitContextMenu', () => {
|
|
|
300
295
|
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
301
296
|
];
|
|
302
297
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
303
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
298
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
304
299
|
act(() => {
|
|
305
300
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
306
301
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -344,7 +339,7 @@ describe('HitContextMenu', () => {
|
|
|
344
339
|
});
|
|
345
340
|
it('should disable custom actions menu when no actions are available', async () => {
|
|
346
341
|
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
347
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
342
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
348
343
|
act(() => {
|
|
349
344
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
350
345
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -386,7 +381,7 @@ describe('HitContextMenu', () => {
|
|
|
386
381
|
skip_rationale: true
|
|
387
382
|
}
|
|
388
383
|
}));
|
|
389
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
384
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
390
385
|
act(() => {
|
|
391
386
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
392
387
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -454,7 +449,7 @@ describe('HitContextMenu', () => {
|
|
|
454
449
|
it('should call executeAction with action_id and hit query', async () => {
|
|
455
450
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
456
451
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
457
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
452
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
458
453
|
act(() => {
|
|
459
454
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
460
455
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -507,7 +502,7 @@ describe('HitContextMenu', () => {
|
|
|
507
502
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
508
503
|
keys: ['howler.detection', 'event.id']
|
|
509
504
|
}));
|
|
510
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
505
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
511
506
|
});
|
|
512
507
|
it('should render exclusion submenu with template keys', async () => {
|
|
513
508
|
act(() => {
|
|
@@ -555,7 +550,7 @@ describe('HitContextMenu', () => {
|
|
|
555
550
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
556
551
|
keys: ['howler.outline.indicators']
|
|
557
552
|
}));
|
|
558
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
553
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
559
554
|
act(() => {
|
|
560
555
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
561
556
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -598,7 +593,7 @@ describe('HitContextMenu', () => {
|
|
|
598
593
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
599
594
|
keys: []
|
|
600
595
|
}));
|
|
601
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
596
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
602
597
|
act(() => {
|
|
603
598
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
604
599
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -610,7 +605,7 @@ describe('HitContextMenu', () => {
|
|
|
610
605
|
});
|
|
611
606
|
it('should skip null field values in exclusion menu', async () => {
|
|
612
607
|
act(() => {
|
|
613
|
-
|
|
608
|
+
mockHitContext.hits['test-hit-1'].event = {};
|
|
614
609
|
});
|
|
615
610
|
act(() => {
|
|
616
611
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -633,7 +628,7 @@ describe('HitContextMenu', () => {
|
|
|
633
628
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
634
629
|
keys: ['howler.detection', 'event.id']
|
|
635
630
|
}));
|
|
636
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
631
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
637
632
|
});
|
|
638
633
|
it('should render inclusion submenu with template keys', async () => {
|
|
639
634
|
act(() => {
|
|
@@ -681,7 +676,7 @@ describe('HitContextMenu', () => {
|
|
|
681
676
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
682
677
|
keys: ['howler.outline.indicators']
|
|
683
678
|
}));
|
|
684
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
679
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
685
680
|
act(() => {
|
|
686
681
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
687
682
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -724,7 +719,7 @@ describe('HitContextMenu', () => {
|
|
|
724
719
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
725
720
|
keys: []
|
|
726
721
|
}));
|
|
727
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
722
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
728
723
|
act(() => {
|
|
729
724
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
730
725
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -736,7 +731,7 @@ describe('HitContextMenu', () => {
|
|
|
736
731
|
});
|
|
737
732
|
it('should skip null field values in inclusion menu', async () => {
|
|
738
733
|
act(() => {
|
|
739
|
-
|
|
734
|
+
mockHitContext.hits['test-hit-1'].event = {};
|
|
740
735
|
});
|
|
741
736
|
act(() => {
|
|
742
737
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -755,24 +750,24 @@ describe('HitContextMenu', () => {
|
|
|
755
750
|
});
|
|
756
751
|
});
|
|
757
752
|
describe('Multiple Hit Selection', () => {
|
|
758
|
-
it('should use
|
|
753
|
+
it('should use selectedHits when current hit is included', async () => {
|
|
759
754
|
act(() => {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
755
|
+
mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
756
|
+
mockHitContext.hits['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
|
|
757
|
+
mockHitContext.selectedHits.push(mockHitContext.hits['hit-1'], mockHitContext.hits['hit-2']);
|
|
763
758
|
mockGetSelectedId.mockReturnValue('hit-1');
|
|
764
759
|
});
|
|
765
760
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
766
761
|
await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
|
|
767
|
-
// The component should use
|
|
762
|
+
// The component should use selectedHits for actions
|
|
768
763
|
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
769
764
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
770
765
|
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
771
766
|
});
|
|
772
|
-
it('should use only current hit when not in
|
|
767
|
+
it('should use only current hit when not in selectedHits', async () => {
|
|
773
768
|
act(() => {
|
|
774
|
-
|
|
775
|
-
|
|
769
|
+
mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
770
|
+
mockHitContext.selectedHits.push(mockHitContext.hits['hit-1']);
|
|
776
771
|
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
777
772
|
});
|
|
778
773
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -791,12 +786,12 @@ describe('HitContextMenu', () => {
|
|
|
791
786
|
});
|
|
792
787
|
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
793
788
|
await waitFor(() => {
|
|
794
|
-
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(
|
|
789
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
|
|
795
790
|
});
|
|
796
791
|
});
|
|
797
792
|
it('should call getMatchingTemplate when menu opens', async () => {
|
|
798
793
|
await waitFor(() => {
|
|
799
|
-
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(
|
|
794
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
|
|
800
795
|
});
|
|
801
796
|
});
|
|
802
797
|
it('should reset state when menu closes', async () => {
|
|
@@ -829,7 +824,7 @@ describe('HitContextMenu', () => {
|
|
|
829
824
|
describe('Edge Cases and Error Handling', () => {
|
|
830
825
|
it('should not crash when hit is null', async () => {
|
|
831
826
|
act(() => {
|
|
832
|
-
|
|
827
|
+
mockHitContext.hits = {};
|
|
833
828
|
});
|
|
834
829
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
835
830
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -840,7 +835,7 @@ describe('HitContextMenu', () => {
|
|
|
840
835
|
});
|
|
841
836
|
it('should not render exclusion menu when template is null', async () => {
|
|
842
837
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
843
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
838
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
844
839
|
act(() => {
|
|
845
840
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
846
841
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -854,7 +849,7 @@ describe('HitContextMenu', () => {
|
|
|
854
849
|
});
|
|
855
850
|
it('should not render inclusion menu when template is null', async () => {
|
|
856
851
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
857
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
852
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
858
853
|
act(() => {
|
|
859
854
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
860
855
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -868,7 +863,7 @@ describe('HitContextMenu', () => {
|
|
|
868
863
|
});
|
|
869
864
|
it('should handle API failure gracefully', async () => {
|
|
870
865
|
mockDispatchApi.mockResolvedValue(null);
|
|
871
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
866
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
872
867
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
873
868
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
874
869
|
await waitFor(() => {
|
|
@@ -879,7 +874,7 @@ describe('HitContextMenu', () => {
|
|
|
879
874
|
});
|
|
880
875
|
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
881
876
|
act(() => {
|
|
882
|
-
|
|
877
|
+
mockHitContext.hits['test-hit-1'].howler.analytic = null;
|
|
883
878
|
});
|
|
884
879
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
885
880
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -898,54 +893,4 @@ describe('HitContextMenu', () => {
|
|
|
898
893
|
expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
|
|
899
894
|
});
|
|
900
895
|
});
|
|
901
|
-
describe('Add to Case Menu Item', () => {
|
|
902
|
-
it('should render "Add to Case" item in the menu', async () => {
|
|
903
|
-
act(() => {
|
|
904
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
905
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
906
|
-
});
|
|
907
|
-
await waitFor(() => {
|
|
908
|
-
expect(screen.getByText('Add to Case')).toBeInTheDocument();
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
it('should enable "Add to Case" when a record is present', async () => {
|
|
912
|
-
act(() => {
|
|
913
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
914
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
915
|
-
});
|
|
916
|
-
await waitFor(() => {
|
|
917
|
-
const menuItems = screen.getAllByRole('menuitem');
|
|
918
|
-
const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
|
|
919
|
-
expect(addToCaseItem).toHaveAttribute('aria-disabled', 'false');
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
it('should disable "Add to Case" when record is null', async () => {
|
|
923
|
-
act(() => {
|
|
924
|
-
mockRecordContext.records['test-hit-1'] = null;
|
|
925
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
926
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
927
|
-
});
|
|
928
|
-
await waitFor(() => {
|
|
929
|
-
const menuItems = screen.getAllByRole('menuitem');
|
|
930
|
-
const addToCaseItem = menuItems.find(item => item.textContent?.includes('Add to Case'));
|
|
931
|
-
expect(addToCaseItem).toHaveAttribute('aria-disabled', 'true');
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
it('should call showModal with an AddToCaseModal element when clicked', async () => {
|
|
935
|
-
act(() => {
|
|
936
|
-
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
937
|
-
fireEvent.contextMenu(contextMenuWrapper);
|
|
938
|
-
});
|
|
939
|
-
await waitFor(() => {
|
|
940
|
-
expect(screen.getByText('Add to Case')).toBeInTheDocument();
|
|
941
|
-
});
|
|
942
|
-
await act(async () => {
|
|
943
|
-
await user.click(screen.getByText('Add to Case'));
|
|
944
|
-
});
|
|
945
|
-
await waitFor(() => {
|
|
946
|
-
expect(mockShowModal).toHaveBeenCalledOnce();
|
|
947
|
-
expect(mockShowModal).toHaveBeenCalledWith(expect.objectContaining({ type: expect.any(Function) }));
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
896
|
});
|