@cccsaurora/howler-ui 2.18.0 → 2.19.0-cases.862
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/index.d.ts +4 -0
- package/api/index.js +10 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/facet/hit.d.ts +1 -3
- package/api/search/facet/index.d.ts +3 -1
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/socket/index.d.ts +3 -0
- package/api/socket/index.js +6 -0
- package/api/socket/viewers.d.ts +2 -0
- package/api/socket/viewers.js +8 -0
- package/api/socket/viewers.test.js +44 -0
- package/api/v2/case/index.d.ts +9 -0
- package/api/v2/case/index.js +21 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.js +18 -0
- package/api/v2/case/rules.d.ts +6 -0
- package/api/v2/case/rules.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +52 -12
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +23 -11
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +5 -5
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +346 -94
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +42 -42
- package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
- package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
- package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
- package/components/app/providers/SocketProvider.d.ts +11 -2
- package/components/app/providers/SocketProvider.js +18 -5
- package/components/app/providers/UserListProvider.js +28 -8
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -0
- package/components/elements/ContextMenu.test.d.ts +1 -0
- package/components/elements/ContextMenu.test.js +215 -0
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +18 -8
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +12 -0
- package/components/elements/case/CaseCard.js +42 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/ChipPopper.js +5 -5
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +2 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +1 -0
- package/components/elements/hit/HitBanner.js +34 -51
- package/components/elements/hit/HitCard.d.ts +2 -0
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +1 -0
- package/components/elements/hit/HitOutline.js +3 -3
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitSummary.d.ts +2 -1
- package/components/elements/hit/HitSummary.js +6 -5
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/elements/Assigned.js +6 -3
- package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
- package/components/elements/hit/elements/Assigned.test.js +65 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +6 -0
- package/components/elements/observable/ObservableCard.js +22 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
- package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
- package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
- package/components/elements/record/RecordContextMenu.js +268 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +190 -39
- package/components/elements/record/RecordRelated.d.ts +7 -0
- package/components/elements/record/RecordRelated.js +34 -0
- package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
- package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
- package/components/elements/view/ViewTitle.d.ts +1 -0
- package/components/elements/view/ViewTitle.js +9 -2
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
- package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/403.d.ts +3 -0
- package/components/routes/403.js +10 -0
- package/components/routes/action/edit/ActionEditor.js +3 -3
- package/components/routes/action/useMyActionFunctions.js +4 -1
- package/components/routes/action/view/ActionDetails.js +6 -1
- package/components/routes/action/view/ActionSearch.js +5 -4
- package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
- package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +44 -0
- package/components/routes/cases/CaseViewer.test.d.ts +1 -0
- package/components/routes/cases/CaseViewer.test.js +133 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +148 -0
- package/components/routes/cases/constants.d.ts +6 -0
- package/components/routes/cases/constants.js +6 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +33 -0
- package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
- package/components/routes/cases/detail/CaseAssets.js +104 -0
- package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseAssets.test.js +167 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +66 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +70 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseRules.d.ts +7 -0
- package/components/routes/cases/detail/CaseRules.js +57 -0
- package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseRules.test.js +221 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
- package/components/routes/cases/detail/CaseSidebar.js +107 -0
- package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +66 -0
- package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
- package/components/routes/cases/detail/CaseTimeline.js +106 -0
- package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
- package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
- package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
- package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +95 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
- package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
- package/components/routes/cases/detail/assets/Asset.js +12 -0
- package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
- package/components/routes/cases/detail/assets/Asset.test.js +72 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +29 -0
- package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +69 -0
- package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
- package/components/routes/cases/hooks/useCase.test.js +141 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +59 -0
- package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
- package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
- package/components/routes/cases/modals/CaseRecordRow.js +15 -0
- package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/CreateCaseModal.js +55 -0
- package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
- package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
- package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
- package/components/routes/cases/modals/RenameItemModal.js +48 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +115 -0
- package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
- package/components/routes/cases/modals/ResolveModal.test.js +394 -0
- package/components/routes/cases/modals/hooks.d.ts +7 -0
- package/components/routes/cases/modals/hooks.js +44 -0
- package/components/routes/cases/modals/types.d.ts +5 -0
- package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
- package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
- package/components/routes/cases/search/CaseDateFilter.js +26 -0
- package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
- package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseStatusFilter.js +13 -0
- package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/help/markdown/en/retention.md.js +1 -1
- package/components/routes/help/markdown/fr/retention.md.js +1 -1
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +50 -63
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
- package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
- package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
- package/components/routes/hits/search/SearchPane.js +26 -49
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +20 -18
- package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
- package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +12 -13
- package/components/routes/home/AddNewCard.js +1 -1
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/overviews/template/en.md.js +1 -1
- package/components/routes/overviews/template/fr.md.js +1 -1
- package/components/routes/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +125 -3
- package/locales/fr/translation.json +123 -3
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/ApiType.d.ts +1 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Hit.d.ts +1 -0
- package/models/entities/generated/Howler.d.ts +0 -5
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +85 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +42 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +6 -9
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/models/entities/generated/View.d.ts +1 -0
- package/models/socket/CaseUpdate.d.ts +5 -0
- package/models/socket/ViewersUpdate.d.ts +4 -0
- package/package.json +23 -8
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/mocks.d.ts +11 -1
- package/tests/mocks.js +12 -7
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/constants.d.ts +4 -3
- package/utils/constants.js +6 -0
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- package/utils/menuUtils.js +1 -1
- package/utils/socketUtils.d.ts +14 -0
- package/utils/socketUtils.js +17 -1
- package/utils/socketUtils.test.d.ts +1 -0
- package/utils/socketUtils.test.js +59 -0
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +27 -0
- package/utils/viewUtils.js +3 -0
- package/components/app/providers/HitProvider.d.ts +0 -22
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/elements/hit/HitRelated.d.ts +0 -6
- package/components/elements/hit/HitRelated.js +0 -7
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
- package/components/routes/hits/search/BundleScroller.d.ts +0 -2
- package/components/routes/hits/search/BundleScroller.js +0 -6
- package/components/routes/hits/search/HitContextMenu.js +0 -229
- /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
- /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
7
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
8
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
9
|
+
import {} from 'react';
|
|
10
|
+
import { I18nextProvider } from 'react-i18next';
|
|
11
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
12
|
+
import { createMockCase, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
13
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import ResolveModal from './ResolveModal';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Hoisted mocks
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const mockAssess = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
19
|
+
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
20
|
+
const mockUpdateCase = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
21
|
+
const mockClose = vi.hoisted(() => vi.fn());
|
|
22
|
+
const mockLoadRecords = vi.hoisted(() => vi.fn());
|
|
23
|
+
vi.mock('components/hooks/useMyApi', () => ({
|
|
24
|
+
default: () => ({ dispatchApi: mockDispatchApi })
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('components/hooks/useHitActions', () => ({
|
|
27
|
+
default: () => ({ assess: mockAssess })
|
|
28
|
+
}));
|
|
29
|
+
vi.mock('../hooks/useCase', () => ({
|
|
30
|
+
default: () => ({ update: mockUpdateCase })
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('components/elements/hit/elements/AnalyticLink', () => ({
|
|
33
|
+
default: ({ hit }) => _jsx("span", { "data-testid": `analytic-${hit.howler.id}`, children: hit.howler.analytic })
|
|
34
|
+
}));
|
|
35
|
+
vi.mock('components/elements/hit/elements/EscalationChip', () => ({
|
|
36
|
+
default: () => null
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('components/elements/hit/HitCard', () => ({
|
|
39
|
+
default: ({ id }) => _jsx("div", { children: `HitCard:${id}` })
|
|
40
|
+
}));
|
|
41
|
+
vi.mock('api', () => ({
|
|
42
|
+
default: {
|
|
43
|
+
search: {
|
|
44
|
+
hit: {
|
|
45
|
+
post: (params) => params
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
51
|
+
useAppUser: () => ({ user: { username: 'test-user' } })
|
|
52
|
+
}));
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Fixtures
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const mockConfig = {
|
|
57
|
+
lookups: {
|
|
58
|
+
'howler.assessment': ['legitimate', 'false_positive', 'non_issue']
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const makeUnresolvedHit = (id) => createMockHit({ howler: { id, status: 'open', analytic: `analytic-${id}` } });
|
|
62
|
+
const makeResolvedHit = (id) => createMockHit({ howler: { id, status: 'resolved', analytic: `analytic-${id}` } });
|
|
63
|
+
const HIT_1 = makeUnresolvedHit('hit-1');
|
|
64
|
+
const HIT_2 = makeUnresolvedHit('hit-2');
|
|
65
|
+
const HIT_RESOLVED = makeResolvedHit('hit-resolved');
|
|
66
|
+
const caseWithHits = (hitIds) => createMockCase({
|
|
67
|
+
case_id: 'case-1',
|
|
68
|
+
items: hitIds.map(id => ({ type: 'hit', value: id }))
|
|
69
|
+
});
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Wrapper factory
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
const createWrapper = (records = {}) => {
|
|
74
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ModalContext.Provider, { value: { close: mockClose, open: vi.fn(), setContent: vi.fn() }, children: _jsx(ApiConfigContext.Provider, { value: { config: mockConfig, setConfig: vi.fn() }, children: _jsx(RecordContext.Provider, { value: { records, loadRecords: mockLoadRecords }, children: _jsx(MemoryRouter, { children: children }) }) }) }) }));
|
|
75
|
+
return Wrapper;
|
|
76
|
+
};
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Clicks a MUI Checkbox — operates on the ButtonBase parent of the hidden input
|
|
82
|
+
* so that user-event's special checkbox-input code path is bypassed and the
|
|
83
|
+
* component's own onClick handler fires correctly.
|
|
84
|
+
*/
|
|
85
|
+
const clickCheckbox = (checkbox) => {
|
|
86
|
+
fireEvent.click(checkbox.parentElement);
|
|
87
|
+
};
|
|
88
|
+
/** Fills in assessment and rationale so the confirm button becomes enabled. */
|
|
89
|
+
const fillForm = async (user, assessment = 'legitimate', rationale = 'Test rationale') => {
|
|
90
|
+
const rationaleInput = screen.getByPlaceholderText(i18n.t('modal.rationale.label'));
|
|
91
|
+
await user.clear(rationaleInput);
|
|
92
|
+
await user.type(rationaleInput, rationale);
|
|
93
|
+
// Typing into the combobox triggers MUI Autocomplete's onInputChange which
|
|
94
|
+
// opens the listbox — a plain click does not reliably open it in jsdom.
|
|
95
|
+
// The combobox has no accessible label (only a placeholder), so we query by
|
|
96
|
+
// role alone — there is exactly one combobox in the modal.
|
|
97
|
+
const assessmentInput = screen.getByRole('combobox');
|
|
98
|
+
await user.type(assessmentInput, assessment.slice(0, 3));
|
|
99
|
+
const option = await screen.findByRole('option', { name: assessment });
|
|
100
|
+
await user.click(option);
|
|
101
|
+
};
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Tests
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
describe('ResolveModal', () => {
|
|
106
|
+
let user;
|
|
107
|
+
let mockOnConfirm;
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
user = userEvent.setup();
|
|
110
|
+
mockOnConfirm = vi.fn();
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
// Default: resolve immediately with an empty items list
|
|
113
|
+
mockDispatchApi.mockResolvedValue({ items: [] });
|
|
114
|
+
});
|
|
115
|
+
// -------------------------------------------------------------------------
|
|
116
|
+
// Initial render
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
describe('initial render', () => {
|
|
119
|
+
it('shows the modal title', async () => {
|
|
120
|
+
render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
121
|
+
wrapper: createWrapper()
|
|
122
|
+
});
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByText(i18n.t('modal.cases.resolve'))).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
it('shows the modal description', async () => {
|
|
128
|
+
render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
129
|
+
wrapper: createWrapper()
|
|
130
|
+
});
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByText(i18n.t('modal.cases.resolve.description'))).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it('renders a cancel button', async () => {
|
|
136
|
+
render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
137
|
+
wrapper: createWrapper()
|
|
138
|
+
});
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByRole('button', { name: i18n.t('cancel') })).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('renders a confirm button', async () => {
|
|
144
|
+
render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
145
|
+
wrapper: createWrapper()
|
|
146
|
+
});
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('shows the "Resolved Alerts" accordion', async () => {
|
|
152
|
+
render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
153
|
+
wrapper: createWrapper()
|
|
154
|
+
});
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(screen.getByText(i18n.t('modal.cases.alerts.resolved'))).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
// Loading state
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
describe('loading state', () => {
|
|
164
|
+
it('shows a LinearProgress while the API call is pending', () => {
|
|
165
|
+
mockDispatchApi.mockReturnValue(new Promise(() => { })); // never resolves
|
|
166
|
+
const { container } = render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
167
|
+
wrapper: createWrapper()
|
|
168
|
+
});
|
|
169
|
+
const progress = container.querySelector('.MuiLinearProgress-root');
|
|
170
|
+
expect(progress).toBeInTheDocument();
|
|
171
|
+
// opacity is 1 while loading
|
|
172
|
+
expect(progress).toHaveStyle({ opacity: '1' });
|
|
173
|
+
});
|
|
174
|
+
it('disables the confirm button while loading', () => {
|
|
175
|
+
mockDispatchApi.mockReturnValue(new Promise(() => { }));
|
|
176
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
177
|
+
wrapper: createWrapper()
|
|
178
|
+
});
|
|
179
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
|
|
180
|
+
});
|
|
181
|
+
it('shows a CircularProgress inside the confirm button while loading', () => {
|
|
182
|
+
mockDispatchApi.mockReturnValue(new Promise(() => { }));
|
|
183
|
+
const { container } = render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
184
|
+
wrapper: createWrapper()
|
|
185
|
+
});
|
|
186
|
+
expect(container.querySelector('.MuiCircularProgress-root')).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
it('fades out the LinearProgress after loading completes', async () => {
|
|
189
|
+
const { container } = render(_jsx(ResolveModal, { case: caseWithHits([]), onConfirm: mockOnConfirm }), {
|
|
190
|
+
wrapper: createWrapper()
|
|
191
|
+
});
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
const progress = container.querySelector('.MuiLinearProgress-root');
|
|
194
|
+
expect(progress).toHaveStyle({ opacity: '0' });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Hit rendering
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
describe('hit rendering', () => {
|
|
202
|
+
it('renders unresolved hits with checkboxes after loading', async () => {
|
|
203
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1', 'hit-2']), onConfirm: mockOnConfirm }), {
|
|
204
|
+
wrapper: createWrapper({ 'hit-1': HIT_1, 'hit-2': HIT_2 })
|
|
205
|
+
});
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('does not show a checkbox for resolved hits', async () => {
|
|
211
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1', 'hit-resolved']), onConfirm: mockOnConfirm }), {
|
|
212
|
+
wrapper: createWrapper({ 'hit-1': HIT_1, 'hit-resolved': HIT_RESOLVED })
|
|
213
|
+
});
|
|
214
|
+
await waitFor(() => {
|
|
215
|
+
// only the single unresolved hit gets a checkbox
|
|
216
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(1);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
it('calls loadRecords with the API search results', async () => {
|
|
220
|
+
const items = [HIT_1];
|
|
221
|
+
mockDispatchApi.mockResolvedValueOnce({ items });
|
|
222
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
223
|
+
wrapper: createWrapper()
|
|
224
|
+
});
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(mockLoadRecords).toHaveBeenCalledWith(items);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
// Confirm button disabled states
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
describe('confirm button enablement', () => {
|
|
234
|
+
it('is disabled when no hits are selected', async () => {
|
|
235
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
236
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
237
|
+
});
|
|
238
|
+
// Wait for the hit to load (checkbox appears) but don't select it
|
|
239
|
+
await screen.findAllByRole('checkbox');
|
|
240
|
+
await fillForm(user);
|
|
241
|
+
// No hit selected → selectedHitIds.size === 0 → button still disabled
|
|
242
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
|
|
243
|
+
});
|
|
244
|
+
it('is disabled when assessment is missing', async () => {
|
|
245
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
246
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
247
|
+
});
|
|
248
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
249
|
+
clickCheckbox(checkbox);
|
|
250
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
251
|
+
const rationaleInput = screen.getByPlaceholderText(i18n.t('modal.rationale.label'));
|
|
252
|
+
await user.type(rationaleInput, 'some reason');
|
|
253
|
+
// no assessment chosen → still disabled
|
|
254
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
|
|
255
|
+
});
|
|
256
|
+
it('is disabled when rationale is empty', async () => {
|
|
257
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
258
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
259
|
+
});
|
|
260
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
261
|
+
clickCheckbox(checkbox);
|
|
262
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
263
|
+
// fill only assessment, no rationale
|
|
264
|
+
const assessmentInput = screen.getByRole('combobox');
|
|
265
|
+
await user.type(assessmentInput, 'leg');
|
|
266
|
+
const option = await screen.findByRole('option', { name: 'legitimate' });
|
|
267
|
+
await user.click(option);
|
|
268
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeDisabled();
|
|
269
|
+
});
|
|
270
|
+
it('is enabled when a hit is selected + assessment + rationale are provided', async () => {
|
|
271
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
272
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
273
|
+
});
|
|
274
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
275
|
+
clickCheckbox(checkbox);
|
|
276
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
277
|
+
await fillForm(user);
|
|
278
|
+
expect(screen.getByRole('button', { name: i18n.t('confirm') })).toBeEnabled();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
// Checkbox / selection toggling
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
describe('hit selection', () => {
|
|
285
|
+
it('checks a hit when its checkbox is clicked', async () => {
|
|
286
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
287
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
288
|
+
});
|
|
289
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
290
|
+
expect(checkbox).not.toBeChecked();
|
|
291
|
+
clickCheckbox(checkbox);
|
|
292
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
293
|
+
});
|
|
294
|
+
it('unchecks a hit when its checkbox is clicked a second time', async () => {
|
|
295
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
296
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
297
|
+
});
|
|
298
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
299
|
+
clickCheckbox(checkbox);
|
|
300
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
301
|
+
clickCheckbox(checkbox);
|
|
302
|
+
await waitFor(() => expect(checkbox).not.toBeChecked());
|
|
303
|
+
});
|
|
304
|
+
it('tracks each hit independently when there are multiple', async () => {
|
|
305
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1', 'hit-2']), onConfirm: mockOnConfirm }), {
|
|
306
|
+
wrapper: createWrapper({ 'hit-1': HIT_1, 'hit-2': HIT_2 })
|
|
307
|
+
});
|
|
308
|
+
const checkboxes = await screen.findAllByRole('checkbox');
|
|
309
|
+
expect(checkboxes).toHaveLength(2);
|
|
310
|
+
clickCheckbox(checkboxes[0]);
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(checkboxes[0]).toBeChecked();
|
|
313
|
+
expect(checkboxes[1]).not.toBeChecked();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// -------------------------------------------------------------------------
|
|
318
|
+
// Confirm action
|
|
319
|
+
// -------------------------------------------------------------------------
|
|
320
|
+
describe('confirm action', () => {
|
|
321
|
+
it('calls assess with the chosen assessment, true, and rationale', async () => {
|
|
322
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
323
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
324
|
+
});
|
|
325
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
326
|
+
clickCheckbox(checkbox);
|
|
327
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
328
|
+
await fillForm(user, 'legitimate', 'My rationale');
|
|
329
|
+
await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(mockAssess).toHaveBeenCalledWith('legitimate', true, 'My rationale');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
it('clears the selection after confirm completes', async () => {
|
|
335
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
336
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
337
|
+
});
|
|
338
|
+
const [checkbox] = await screen.findAllByRole('checkbox');
|
|
339
|
+
clickCheckbox(checkbox);
|
|
340
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
341
|
+
await fillForm(user);
|
|
342
|
+
await user.click(screen.getByRole('button', { name: i18n.t('confirm') }));
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(checkbox).not.toBeChecked();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
// -------------------------------------------------------------------------
|
|
349
|
+
// Cancel button
|
|
350
|
+
// -------------------------------------------------------------------------
|
|
351
|
+
describe('cancel button', () => {
|
|
352
|
+
it('calls close() when cancel is clicked', async () => {
|
|
353
|
+
// Use a case with an unresolved hit so the auto-resolve effect does not fire
|
|
354
|
+
// and call close() before we even click cancel.
|
|
355
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
356
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
357
|
+
});
|
|
358
|
+
// Wait for loading to finish so no unexpected state transitions happen mid-click
|
|
359
|
+
await screen.findAllByRole('checkbox');
|
|
360
|
+
await user.click(screen.getByRole('button', { name: i18n.t('cancel') }));
|
|
361
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
// -------------------------------------------------------------------------
|
|
365
|
+
// Auto-resolve when all hits are already resolved
|
|
366
|
+
// -------------------------------------------------------------------------
|
|
367
|
+
describe('auto-resolve', () => {
|
|
368
|
+
it('calls updateCase, onConfirm, and close when no unresolved hits remain after loading', async () => {
|
|
369
|
+
// All hits in the case are already resolved
|
|
370
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-resolved']), onConfirm: mockOnConfirm }), {
|
|
371
|
+
wrapper: createWrapper({ 'hit-resolved': HIT_RESOLVED })
|
|
372
|
+
});
|
|
373
|
+
// dispatchApi resolves → loading becomes false → unresolvedHits.length === 0 → auto-close
|
|
374
|
+
await waitFor(() => {
|
|
375
|
+
expect(mockUpdateCase).toHaveBeenCalledWith({ status: 'resolved' });
|
|
376
|
+
});
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
|
|
379
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
it('does NOT auto-resolve while hits are still unresolved', async () => {
|
|
383
|
+
render(_jsx(ResolveModal, { case: caseWithHits(['hit-1']), onConfirm: mockOnConfirm }), {
|
|
384
|
+
wrapper: createWrapper({ 'hit-1': HIT_1 })
|
|
385
|
+
});
|
|
386
|
+
// Wait for loading to complete: the unresolved hit's checkbox appears once the
|
|
387
|
+
// dispatchApi call settles and the LinearProgress opacity drops to 0.
|
|
388
|
+
await screen.findAllByRole('checkbox');
|
|
389
|
+
// Should not have auto-resolved
|
|
390
|
+
expect(mockUpdateCase).not.toHaveBeenCalled();
|
|
391
|
+
expect(mockClose).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
4
|
+
import type { RecordEntry } from './types';
|
|
5
|
+
export declare const defaultTitle: (record: Hit | Observable) => string;
|
|
6
|
+
export declare const useFolderOptions: (selectedCase: Case | null) => string[];
|
|
7
|
+
export declare const useRecordEntries: (records: (Hit | Observable)[]) => readonly [RecordEntry[], (index: number, field: "title" | "path", value: string) => void];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
export const defaultTitle = (record) => {
|
|
3
|
+
if (record.__index === 'hit') {
|
|
4
|
+
return `${record.howler.analytic} (${record.howler.id})`;
|
|
5
|
+
}
|
|
6
|
+
return `Observable (${record.howler.id})`;
|
|
7
|
+
};
|
|
8
|
+
export const useFolderOptions = (selectedCase) => {
|
|
9
|
+
return useMemo(() => {
|
|
10
|
+
if (!selectedCase?.items) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const paths = new Set();
|
|
14
|
+
for (const item of selectedCase.items) {
|
|
15
|
+
if (!item.path) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const parts = item.path.split('/');
|
|
19
|
+
parts.pop();
|
|
20
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
21
|
+
paths.add(parts.slice(0, i).join('/'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return Array.from(paths);
|
|
25
|
+
}, [selectedCase]);
|
|
26
|
+
};
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Hook
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export const useRecordEntries = (records) => {
|
|
31
|
+
const [entries, setEntries] = useState(() => (records ?? []).map(record => ({
|
|
32
|
+
record,
|
|
33
|
+
path: '',
|
|
34
|
+
title: defaultTitle(record)
|
|
35
|
+
})));
|
|
36
|
+
const updateEntry = useCallback((index, field, value) => {
|
|
37
|
+
setEntries(prev => {
|
|
38
|
+
const next = [...prev];
|
|
39
|
+
next[index] = { ...next[index], [field]: value };
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
42
|
+
}, []);
|
|
43
|
+
return [entries, updateEntry];
|
|
44
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Person } from '@mui/icons-material';
|
|
3
|
+
import { AvatarGroup, Checkbox, Divider, FormControlLabel, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
5
|
+
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
6
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
7
|
+
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
8
|
+
import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
|
|
9
|
+
import { useCallback, useContext, useEffect } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
const CaseAssigneeFilter = ({ assigneeFilter, onChange }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const { user: currentUser } = useAppUser();
|
|
14
|
+
const { users, searchUsers } = useContext(UserListContext);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
searchUsers('uname:*');
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
+
}, []);
|
|
19
|
+
const toggleMyself = useCallback((_, checked) => {
|
|
20
|
+
if (!currentUser) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
onChange(checked
|
|
24
|
+
? [...assigneeFilter.filter(a => a !== currentUser.username), currentUser.username]
|
|
25
|
+
: assigneeFilter.filter(a => a !== currentUser.username));
|
|
26
|
+
}, [currentUser, assigneeFilter, onChange]);
|
|
27
|
+
return (_jsx(ChipPopper, { icon: assigneeFilter.length > 0 ? (_jsx(AvatarGroup, { sx: { '& .MuiAvatar-root': { height: 18, width: 18, fontSize: '0.6rem' } }, children: assigneeFilter.map(u => (_jsx(HowlerAvatar, { userId: u, sx: { height: 18, width: 18 } }, u))) })) : (_jsx(Person, { fontSize: "small" })), label: _jsx(Typography, { variant: "body2", children: assigneeFilter.length === 0
|
|
28
|
+
? t('route.cases.filter.assignee')
|
|
29
|
+
: assigneeFilter.length === 1
|
|
30
|
+
? (users[assigneeFilter[0]]?.name ?? assigneeFilter[0])
|
|
31
|
+
: `${assigneeFilter.length} ${t('route.cases.filter.assignees')}` }), minWidth: "260px", slotProps: { chip: { size: 'small', color: assigneeFilter.length > 0 ? 'primary' : 'default' } }, children: _jsxs(Stack, { direction: "row", divider: _jsx(Divider, { orientation: "vertical", flexItem: true }), spacing: 1, children: [_jsx(UserList, { userIds: assigneeFilter, onChange: onChange, i18nLabel: "route.cases.filter.assignee", multiple: true }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: currentUser ? assigneeFilter.includes(currentUser.username) : false, onChange: toggleMyself }), label: t('route.cases.filter.myself') })] }) }));
|
|
32
|
+
};
|
|
33
|
+
export default CaseAssigneeFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
6
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
|
+
import { I18nextProvider } from 'react-i18next';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import CaseAssigneeFilter from './CaseAssigneeFilter';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Hoisted mocks
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
15
|
+
useAppUser: () => ({ user: { username: 'alice', name: 'Alice Smith' } })
|
|
16
|
+
}));
|
|
17
|
+
// Stub HowlerAvatar to avoid avatar API calls
|
|
18
|
+
vi.mock('components/elements/display/HowlerAvatar', () => ({
|
|
19
|
+
default: ({ userId }) => _jsx("div", { id: `avatar-${userId}`, children: userId })
|
|
20
|
+
}));
|
|
21
|
+
// Stub UserList to a simple multi-select so we can test the onChange wire-up
|
|
22
|
+
// without pulling in the full component and its popover.
|
|
23
|
+
vi.mock('components/elements/UserList', () => ({
|
|
24
|
+
default: ({ userIds, onChange, multiple }) => (_jsxs("div", { id: "user-list", children: [_jsx("button", { id: "user-list-add", onClick: () => onChange([...userIds, 'bob']), children: "Add bob" }), multiple &&
|
|
25
|
+
userIds.map(id => (_jsxs("button", { id: `remove-${id}`, onClick: () => onChange(userIds.filter(u => u !== id)), children: ["Remove ", id] }, id)))] }))
|
|
26
|
+
}));
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const mockSearchUsers = vi.fn();
|
|
31
|
+
const USERS = {
|
|
32
|
+
alice: { username: 'alice', name: 'Alice Smith' },
|
|
33
|
+
bob: { username: 'bob', name: 'Bob Jones' }
|
|
34
|
+
};
|
|
35
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(UserListContext.Provider, { value: { users: USERS, searchUsers: mockSearchUsers, fetchUsers: vi.fn() }, children: children }) }));
|
|
36
|
+
const renderFilter = (assigneeFilter, onChange = vi.fn()) => render(_jsx(CaseAssigneeFilter, { assigneeFilter: assigneeFilter, onChange: onChange }), { wrapper: Wrapper });
|
|
37
|
+
const openPopper = async (user, labelText) => {
|
|
38
|
+
const chip = screen.getByText(labelText).closest('.MuiChip-root');
|
|
39
|
+
await user.click(chip);
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('user-list')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Tests
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
describe('CaseAssigneeFilter', () => {
|
|
48
|
+
let user;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
user = userEvent.setup();
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe('searchUsers on mount', () => {
|
|
54
|
+
it('calls searchUsers with "uname:*" on mount', () => {
|
|
55
|
+
renderFilter([]);
|
|
56
|
+
expect(mockSearchUsers).toHaveBeenCalledWith('uname:*');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('label', () => {
|
|
60
|
+
it('shows the default "Assignee" label when no assignees are selected', () => {
|
|
61
|
+
renderFilter([]);
|
|
62
|
+
expect(screen.getByText(i18n.t('route.cases.filter.assignee'))).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
it('shows the user display name when one assignee is selected', () => {
|
|
65
|
+
renderFilter(['alice']);
|
|
66
|
+
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('shows count + "Assignees" when multiple assignees are selected', () => {
|
|
69
|
+
renderFilter(['alice', 'bob']);
|
|
70
|
+
expect(screen.getByText(`2 ${i18n.t('route.cases.filter.assignees')}`)).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('falls back to username when user is not in the user map', () => {
|
|
73
|
+
renderFilter(['unknown-user']);
|
|
74
|
+
const chipLabel = document.querySelector('.MuiChip-label');
|
|
75
|
+
expect(chipLabel?.textContent).toContain('unknown-user');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('chip color', () => {
|
|
79
|
+
it('uses default color when no assignees are selected', () => {
|
|
80
|
+
renderFilter([]);
|
|
81
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.assignee')).closest('.MuiChip-root');
|
|
82
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
83
|
+
});
|
|
84
|
+
it('uses primary color when assignees are selected', () => {
|
|
85
|
+
renderFilter(['alice']);
|
|
86
|
+
const chip = screen.getByText('Alice Smith').closest('.MuiChip-root');
|
|
87
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('"Myself" checkbox', () => {
|
|
91
|
+
it('is unchecked when the current user is not in the filter', async () => {
|
|
92
|
+
renderFilter([]);
|
|
93
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
94
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
95
|
+
expect(checkbox).not.toBeChecked();
|
|
96
|
+
});
|
|
97
|
+
it('is checked when the current user is already in the filter', async () => {
|
|
98
|
+
renderFilter(['alice']);
|
|
99
|
+
await openPopper(user, 'Alice Smith');
|
|
100
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
101
|
+
expect(checkbox).toBeChecked();
|
|
102
|
+
});
|
|
103
|
+
it('adds the current user to the filter when the checkbox is checked', async () => {
|
|
104
|
+
const onChange = vi.fn();
|
|
105
|
+
renderFilter([], onChange);
|
|
106
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
107
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
108
|
+
expect(onChange).toHaveBeenCalledWith(['alice']);
|
|
109
|
+
});
|
|
110
|
+
it('removes the current user from the filter when the checkbox is unchecked', async () => {
|
|
111
|
+
const onChange = vi.fn();
|
|
112
|
+
renderFilter(['alice', 'bob'], onChange);
|
|
113
|
+
await openPopper(user, `2 ${i18n.t('route.cases.filter.assignees')}`);
|
|
114
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
115
|
+
expect(onChange).toHaveBeenCalledWith(['bob']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('UserList onChange passthrough', () => {
|
|
119
|
+
it('passes onChange directly to UserList', async () => {
|
|
120
|
+
const onChange = vi.fn();
|
|
121
|
+
renderFilter(['alice'], onChange);
|
|
122
|
+
await openPopper(user, 'Alice Smith');
|
|
123
|
+
await user.click(screen.getByTestId('user-list-add'));
|
|
124
|
+
expect(onChange).toHaveBeenCalledWith(['alice', 'bob']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Dayjs } from 'dayjs';
|
|
2
|
+
import type { FC } from 'react';
|
|
3
|
+
import { DATE_RANGES } from '@cccsaurora/howler-ui/utils/constants';
|
|
4
|
+
export type DateRangeOption = (typeof DATE_RANGES)[number];
|
|
5
|
+
declare const CaseDateFilter: FC<{
|
|
6
|
+
dateRange: DateRangeOption;
|
|
7
|
+
onChange: (v: DateRangeOption) => void;
|
|
8
|
+
customStart: Dayjs;
|
|
9
|
+
customEnd: Dayjs;
|
|
10
|
+
onCustomStartChange: (v: Dayjs) => void;
|
|
11
|
+
onCustomEndChange: (v: Dayjs) => void;
|
|
12
|
+
}>;
|
|
13
|
+
export default CaseDateFilter;
|