@cccsaurora/howler-ui 2.18.0-dev.740 → 2.18.0-dev.748
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 +2 -0
- package/api/index.js +4 -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/v2/case/index.d.ts +8 -0
- package/api/v2/case/index.js +20 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.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 +39 -7
- 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 +3 -3
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +346 -94
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
- package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
- package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
- package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
- package/components/app/providers/UserListProvider.js +28 -8
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -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/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 +29 -49
- 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/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 +247 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -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/action/edit/ActionEditor.js +2 -2
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +22 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +101 -0
- package/components/routes/cases/constants.d.ts +5 -0
- package/components/routes/cases/constants.js +5 -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 +61 -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/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 +246 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +57 -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 +227 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +99 -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 +51 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +62 -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 +384 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +47 -60
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
- package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
- package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
- package/components/routes/hits/search/SearchPane.js +26 -49
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +20 -18
- package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
- package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +12 -13
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +89 -3
- package/locales/fr/translation.json +87 -3
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Hit.d.ts +1 -0
- package/models/entities/generated/Howler.d.ts +0 -4
- 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 +43 -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 +2 -10
- 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/package.json +18 -1
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/mocks.d.ts +11 -1
- package/tests/mocks.js +12 -7
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/constants.d.ts +3 -3
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- 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 -227
- /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.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
|
@@ -47,6 +47,7 @@ vi.mock('components/app/hooks/useMatchers', () => ({
|
|
|
47
47
|
getMatchingTemplate: mockGetMatchingTemplate
|
|
48
48
|
}))
|
|
49
49
|
}));
|
|
50
|
+
const mockShowModal = vi.fn();
|
|
50
51
|
const mockDispatchApi = vi.fn();
|
|
51
52
|
vi.mock('components/hooks/useMyApi', () => ({
|
|
52
53
|
default: vi.fn(() => ({
|
|
@@ -72,6 +73,9 @@ vi.mock('plugins/store', () => ({
|
|
|
72
73
|
plugins: ['plugin1']
|
|
73
74
|
}
|
|
74
75
|
}));
|
|
76
|
+
vi.mock('components/routes/cases/modals/AddToCaseModal', () => ({
|
|
77
|
+
default: () => null
|
|
78
|
+
}));
|
|
75
79
|
// Mock MUI components
|
|
76
80
|
vi.mock('@mui/material', async () => {
|
|
77
81
|
const actual = await vi.importActual('@mui/material');
|
|
@@ -91,13 +95,14 @@ vi.mock('@mui/material', async () => {
|
|
|
91
95
|
});
|
|
92
96
|
// Import component after mocks
|
|
93
97
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
94
|
-
import {
|
|
98
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
95
99
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
100
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
96
101
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
97
102
|
import { I18nextProvider } from 'react-i18next';
|
|
98
103
|
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
99
104
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
100
|
-
import
|
|
105
|
+
import RecordContextMenu from './RecordContextMenu';
|
|
101
106
|
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
102
107
|
const mockConfig = {
|
|
103
108
|
lookups: {
|
|
@@ -105,16 +110,16 @@ const mockConfig = {
|
|
|
105
110
|
}
|
|
106
111
|
};
|
|
107
112
|
const mockApiContext = { config: mockConfig };
|
|
108
|
-
const
|
|
109
|
-
|
|
113
|
+
const mockRecordContext = {
|
|
114
|
+
records: {
|
|
110
115
|
'test-hit-1': createMockHit()
|
|
111
116
|
},
|
|
112
|
-
|
|
117
|
+
selectedRecords: []
|
|
113
118
|
};
|
|
114
119
|
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
115
120
|
// Test wrapper
|
|
116
121
|
const Wrapper = ({ children }) => {
|
|
117
|
-
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(
|
|
122
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(ModalContext.Provider, { value: { showModal: mockShowModal }, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }) }));
|
|
118
123
|
};
|
|
119
124
|
describe('HitContextMenu', () => {
|
|
120
125
|
let user;
|
|
@@ -122,11 +127,11 @@ describe('HitContextMenu', () => {
|
|
|
122
127
|
beforeEach(() => {
|
|
123
128
|
user = userEvent.setup();
|
|
124
129
|
vi.clearAllMocks();
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
mockRecordContext.selectedRecords.length = 0;
|
|
131
|
+
mockRecordContext.records['test-hit-1'] = createMockHit();
|
|
127
132
|
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
128
133
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
129
|
-
rerender = render(_jsx(Wrapper, { children: _jsx(
|
|
134
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
130
135
|
});
|
|
131
136
|
describe('Context Menu Initialization', () => {
|
|
132
137
|
it('should open menu on right-click', async () => {
|
|
@@ -190,13 +195,13 @@ describe('HitContextMenu', () => {
|
|
|
190
195
|
});
|
|
191
196
|
it('should disable "Open Hit" when hit is null', async () => {
|
|
192
197
|
act(() => {
|
|
193
|
-
|
|
198
|
+
mockRecordContext.records['test-hit-1'] = null;
|
|
194
199
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
195
200
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
196
201
|
});
|
|
197
202
|
await waitFor(() => {
|
|
198
203
|
const menuItems = screen.getAllByRole('menuitem');
|
|
199
|
-
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit
|
|
204
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
|
|
200
205
|
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
201
206
|
});
|
|
202
207
|
});
|
|
@@ -237,7 +242,7 @@ describe('HitContextMenu', () => {
|
|
|
237
242
|
skip_rationale: false
|
|
238
243
|
}
|
|
239
244
|
}));
|
|
240
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
245
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
241
246
|
act(() => {
|
|
242
247
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
243
248
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -295,7 +300,7 @@ describe('HitContextMenu', () => {
|
|
|
295
300
|
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
296
301
|
];
|
|
297
302
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
298
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
303
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
299
304
|
act(() => {
|
|
300
305
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
301
306
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -339,7 +344,7 @@ describe('HitContextMenu', () => {
|
|
|
339
344
|
});
|
|
340
345
|
it('should disable custom actions menu when no actions are available', async () => {
|
|
341
346
|
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
342
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
347
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
343
348
|
act(() => {
|
|
344
349
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
345
350
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -381,7 +386,7 @@ describe('HitContextMenu', () => {
|
|
|
381
386
|
skip_rationale: true
|
|
382
387
|
}
|
|
383
388
|
}));
|
|
384
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
389
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
385
390
|
act(() => {
|
|
386
391
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
387
392
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -449,7 +454,7 @@ describe('HitContextMenu', () => {
|
|
|
449
454
|
it('should call executeAction with action_id and hit query', async () => {
|
|
450
455
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
451
456
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
452
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
457
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
453
458
|
act(() => {
|
|
454
459
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
455
460
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -502,7 +507,7 @@ describe('HitContextMenu', () => {
|
|
|
502
507
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
503
508
|
keys: ['howler.detection', 'event.id']
|
|
504
509
|
}));
|
|
505
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
510
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
506
511
|
});
|
|
507
512
|
it('should render exclusion submenu with template keys', async () => {
|
|
508
513
|
act(() => {
|
|
@@ -550,7 +555,7 @@ describe('HitContextMenu', () => {
|
|
|
550
555
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
551
556
|
keys: ['howler.outline.indicators']
|
|
552
557
|
}));
|
|
553
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
558
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
554
559
|
act(() => {
|
|
555
560
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
556
561
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -593,7 +598,7 @@ describe('HitContextMenu', () => {
|
|
|
593
598
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
594
599
|
keys: []
|
|
595
600
|
}));
|
|
596
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
601
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
597
602
|
act(() => {
|
|
598
603
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
599
604
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -605,7 +610,7 @@ describe('HitContextMenu', () => {
|
|
|
605
610
|
});
|
|
606
611
|
it('should skip null field values in exclusion menu', async () => {
|
|
607
612
|
act(() => {
|
|
608
|
-
|
|
613
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
609
614
|
});
|
|
610
615
|
act(() => {
|
|
611
616
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -628,7 +633,7 @@ describe('HitContextMenu', () => {
|
|
|
628
633
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
629
634
|
keys: ['howler.detection', 'event.id']
|
|
630
635
|
}));
|
|
631
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
636
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
632
637
|
});
|
|
633
638
|
it('should render inclusion submenu with template keys', async () => {
|
|
634
639
|
act(() => {
|
|
@@ -676,7 +681,7 @@ describe('HitContextMenu', () => {
|
|
|
676
681
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
677
682
|
keys: ['howler.outline.indicators']
|
|
678
683
|
}));
|
|
679
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
684
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
680
685
|
act(() => {
|
|
681
686
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
682
687
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -719,7 +724,7 @@ describe('HitContextMenu', () => {
|
|
|
719
724
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
720
725
|
keys: []
|
|
721
726
|
}));
|
|
722
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
727
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
723
728
|
act(() => {
|
|
724
729
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
725
730
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -731,7 +736,7 @@ describe('HitContextMenu', () => {
|
|
|
731
736
|
});
|
|
732
737
|
it('should skip null field values in inclusion menu', async () => {
|
|
733
738
|
act(() => {
|
|
734
|
-
|
|
739
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
735
740
|
});
|
|
736
741
|
act(() => {
|
|
737
742
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -750,24 +755,24 @@ describe('HitContextMenu', () => {
|
|
|
750
755
|
});
|
|
751
756
|
});
|
|
752
757
|
describe('Multiple Hit Selection', () => {
|
|
753
|
-
it('should use
|
|
758
|
+
it('should use selectedRecords when current hit is included', async () => {
|
|
754
759
|
act(() => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
760
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
761
|
+
mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
|
|
762
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
|
|
758
763
|
mockGetSelectedId.mockReturnValue('hit-1');
|
|
759
764
|
});
|
|
760
765
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
761
766
|
await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
|
|
762
|
-
// The component should use
|
|
767
|
+
// The component should use selectedRecords for actions
|
|
763
768
|
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
764
769
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
765
770
|
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
766
771
|
});
|
|
767
|
-
it('should use only current hit when not in
|
|
772
|
+
it('should use only current hit when not in selectedRecords', async () => {
|
|
768
773
|
act(() => {
|
|
769
|
-
|
|
770
|
-
|
|
774
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
775
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
|
|
771
776
|
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
772
777
|
});
|
|
773
778
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -786,12 +791,12 @@ describe('HitContextMenu', () => {
|
|
|
786
791
|
});
|
|
787
792
|
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
788
793
|
await waitFor(() => {
|
|
789
|
-
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(
|
|
794
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
790
795
|
});
|
|
791
796
|
});
|
|
792
797
|
it('should call getMatchingTemplate when menu opens', async () => {
|
|
793
798
|
await waitFor(() => {
|
|
794
|
-
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(
|
|
799
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
795
800
|
});
|
|
796
801
|
});
|
|
797
802
|
it('should reset state when menu closes', async () => {
|
|
@@ -824,7 +829,7 @@ describe('HitContextMenu', () => {
|
|
|
824
829
|
describe('Edge Cases and Error Handling', () => {
|
|
825
830
|
it('should not crash when hit is null', async () => {
|
|
826
831
|
act(() => {
|
|
827
|
-
|
|
832
|
+
mockRecordContext.records = {};
|
|
828
833
|
});
|
|
829
834
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
830
835
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -835,7 +840,7 @@ describe('HitContextMenu', () => {
|
|
|
835
840
|
});
|
|
836
841
|
it('should not render exclusion menu when template is null', async () => {
|
|
837
842
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
838
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
843
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
839
844
|
act(() => {
|
|
840
845
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
841
846
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -849,7 +854,7 @@ describe('HitContextMenu', () => {
|
|
|
849
854
|
});
|
|
850
855
|
it('should not render inclusion menu when template is null', async () => {
|
|
851
856
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
852
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
857
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
853
858
|
act(() => {
|
|
854
859
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
855
860
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -863,7 +868,7 @@ describe('HitContextMenu', () => {
|
|
|
863
868
|
});
|
|
864
869
|
it('should handle API failure gracefully', async () => {
|
|
865
870
|
mockDispatchApi.mockResolvedValue(null);
|
|
866
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
871
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
867
872
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
868
873
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
869
874
|
await waitFor(() => {
|
|
@@ -874,7 +879,7 @@ describe('HitContextMenu', () => {
|
|
|
874
879
|
});
|
|
875
880
|
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
876
881
|
act(() => {
|
|
877
|
-
|
|
882
|
+
mockRecordContext.records['test-hit-1'].howler.analytic = null;
|
|
878
883
|
});
|
|
879
884
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
880
885
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -893,4 +898,54 @@ describe('HitContextMenu', () => {
|
|
|
893
898
|
expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
|
|
894
899
|
});
|
|
895
900
|
});
|
|
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
|
+
});
|
|
896
951
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
|
+
import { type FC } from 'react';
|
|
4
|
+
declare const RecordRelated: FC<{
|
|
5
|
+
record: Hit | Observable;
|
|
6
|
+
}>;
|
|
7
|
+
export default RecordRelated;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Stack, Tab, Tabs, useTheme } from '@mui/material';
|
|
3
|
+
import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
|
|
4
|
+
import useRelatedRecords from '@cccsaurora/howler-ui/components/hooks/useRelatedRecords';
|
|
5
|
+
import { groupBy } from 'lodash-es';
|
|
6
|
+
import { useMemo, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Link } from 'react-router-dom';
|
|
9
|
+
import { isCase, isHit, isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
|
|
10
|
+
import CaseCard from '../case/CaseCard';
|
|
11
|
+
import HitCard from '../hit/HitCard';
|
|
12
|
+
import { HitLayout } from '../hit/HitLayout';
|
|
13
|
+
import RelatedLink from '../hit/related/RelatedLink';
|
|
14
|
+
const RecordRelated = ({ record }) => {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const related = useMemo(() => record?.howler.related ?? [], [record?.howler.related]);
|
|
18
|
+
const records = useRelatedRecords(related, related.length > 0);
|
|
19
|
+
const groups = groupBy(records, '__index');
|
|
20
|
+
const hasLinks = (record?.howler.links?.length ?? 0) > 0;
|
|
21
|
+
const tabs = [
|
|
22
|
+
hasLinks && 'links',
|
|
23
|
+
groups.hit?.length > 0 && 'hit',
|
|
24
|
+
groups.case?.length > 0 && 'case',
|
|
25
|
+
groups.observable?.length > 0 && 'observable'
|
|
26
|
+
].filter(Boolean);
|
|
27
|
+
const [activeTab, setActiveTab] = useState(false);
|
|
28
|
+
const currentTab = activeTab !== false && tabs.includes(activeTab) ? activeTab : (tabs[0] ?? false);
|
|
29
|
+
if (!record) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs(Box, { sx: { borderTop: `thin solid ${theme.palette.divider}`, height: '100%', flex: 1, mr: 2, pb: 2 }, children: [_jsxs(Tabs, { value: currentTab, onChange: (_, v) => setActiveTab(v), variant: "scrollable", scrollButtons: "auto", children: [hasLinks && _jsx(Tab, { value: "links", label: t('hit.related.tab.links') }), groups.hit?.length > 0 && _jsx(Tab, { value: "hit", label: t('hit.related.tab.hit') }), groups.case?.length > 0 && _jsx(Tab, { value: "case", label: t('hit.related.tab.case') }), groups.observable?.length > 0 && _jsx(Tab, { value: "observable", label: t('hit.related.tab.observable') })] }), currentTab === 'links' && (_jsx(Box, { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 1, pt: 1, children: record.howler.links.map(l => (_jsx(RelatedLink, { ...l }, l.title + l.href))) })), currentTab === 'hit' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isHit).map(h => (_jsx(Link, { to: `/hits/${h.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(HitCard, { id: h.howler.id, layout: HitLayout.NORMAL }) }, h.howler.id))) })), currentTab === 'case' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isCase).map(c => (_jsx(Link, { to: `/cases/${c.case_id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(CaseCard, { case: c }) }, c.case_id))) })), currentTab === 'observable' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isObservable).map(o => (_jsx(Link, { to: `/observables/${o.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(ObservableCard, { observable: o }) }, o.howler.id))) }))] }));
|
|
33
|
+
};
|
|
34
|
+
export default RecordRelated;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { HowlerUser } from '@cccsaurora/howler-ui/models/entities/HowlerUser';
|
|
2
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
4
|
import type { FC } from 'react';
|
|
4
|
-
declare const
|
|
5
|
-
|
|
5
|
+
declare const RecordWorklog: FC<{
|
|
6
|
+
record: Hit | Observable;
|
|
6
7
|
users: {
|
|
7
8
|
[id: string]: HowlerUser;
|
|
8
9
|
};
|
|
9
10
|
}>;
|
|
10
|
-
export default
|
|
11
|
+
export default RecordWorklog;
|
|
@@ -10,7 +10,7 @@ import { compareTimestamp, twitterShort } from '@cccsaurora/howler-ui/utils/util
|
|
|
10
10
|
import HowlerAvatar from '../display/HowlerAvatar';
|
|
11
11
|
import HowlerCard from '../display/HowlerCard';
|
|
12
12
|
import Markdown from '../display/Markdown';
|
|
13
|
-
const
|
|
13
|
+
const RecordWorklog = ({ record, users }) => {
|
|
14
14
|
const theme = useTheme();
|
|
15
15
|
const { shiftColor } = useMyUtils();
|
|
16
16
|
const { t } = useTranslation();
|
|
@@ -23,15 +23,15 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
23
23
|
*/
|
|
24
24
|
const worklogGroups = useMemo(() => {
|
|
25
25
|
let setInitialVersion = false;
|
|
26
|
-
return (
|
|
26
|
+
return (record?.howler?.log || [])
|
|
27
27
|
.slice()
|
|
28
28
|
.sort((a, b) => compareTimestamp(b.timestamp, a.timestamp))
|
|
29
29
|
.reduce((acc, l) => {
|
|
30
|
-
if (!initialVersions[
|
|
30
|
+
if (!initialVersions[record.howler.id] && !setInitialVersion) {
|
|
31
31
|
setInitialVersion = true;
|
|
32
32
|
setInitialVersions({
|
|
33
33
|
...initialVersions,
|
|
34
|
-
[
|
|
34
|
+
[record.howler.id]: l.previous_version
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
// Initialize the worklog card groups
|
|
@@ -42,9 +42,9 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
42
42
|
const currArr = acc[acc.length - 1];
|
|
43
43
|
if (
|
|
44
44
|
// Does this log version match the saved version?
|
|
45
|
-
l.previous_version === initialVersions[
|
|
45
|
+
l.previous_version === initialVersions[record.howler.id] &&
|
|
46
46
|
// Does the previous entry not match?
|
|
47
|
-
currArr[currArr.length - 1].previous_version !== initialVersions[
|
|
47
|
+
currArr[currArr.length - 1].previous_version !== initialVersions[record.howler.id]) {
|
|
48
48
|
// If so, we've figured out where the new logs should start, so we start a new card.
|
|
49
49
|
acc.push([l]);
|
|
50
50
|
return acc;
|
|
@@ -59,14 +59,14 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
59
59
|
}, []);
|
|
60
60
|
},
|
|
61
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
-
[
|
|
62
|
+
[record?.howler?.log]);
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
// On unmount, mark the latest entry version as the last seen version.
|
|
65
65
|
return () => {
|
|
66
|
-
if (
|
|
66
|
+
if (record?.howler.id) {
|
|
67
67
|
setInitialVersions({
|
|
68
68
|
...initialVersions,
|
|
69
|
-
[
|
|
69
|
+
[record.howler.id]: worklogGroups[0][0]?.previous_version ?? initialVersions[record.howler.id]
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
};
|
|
@@ -77,7 +77,9 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
77
77
|
if (worklogGroups.length > 0) {
|
|
78
78
|
return worklogGroups.flatMap((ls, index) => {
|
|
79
79
|
const result = [];
|
|
80
|
-
if (index > 0 &&
|
|
80
|
+
if (index > 0 &&
|
|
81
|
+
initialVersions[record.howler.id] &&
|
|
82
|
+
ls[0].previous_version === initialVersions[record.howler.id]) {
|
|
81
83
|
result.push(_jsx(Divider, { children: _jsxs(Stack, { direction: "row", children: [_jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('hit.worklog.new') }), _jsx(KeyboardArrowUp, { sx: { color: 'text.secondary' }, fontSize: "small" })] }) }, "new"));
|
|
82
84
|
}
|
|
83
85
|
result.push(_jsxs(HowlerCard, { elevation: 4, children: [_jsx(CardHeader, { avatar: _jsx(HowlerAvatar, { userId: ls[0].user }), title: users[ls[0].user]?.name ?? ls[0].user, subheader: _jsx(Tooltip, { title: new Date(ls[0].timestamp).toLocaleString(), children: _jsx(Typography, { variant: "caption", children: twitterShort(ls[0].timestamp) }) }) }), _jsx(CardContent, { children: _jsx(Stack, { spacing: 1, divider: _jsx(Divider, { orientation: "horizontal" }), children: ls.map(l => (_jsxs(Typography, { variant: "body2", color: "text.secondary", component: "div", position: "relative", children: [l.explanation ? (_jsx(Markdown, { md: l.explanation.trim() })) : (_jsxs(_Fragment, { children: [_jsxs("span", { children: [t('hit.worklog.updated'), "\u00A0"] }), _jsx("code", { children: l.key }), _jsx("span", { children: ":\u00A0" }), {
|
|
@@ -88,10 +90,10 @@ const HitWorklog = ({ hit, users }) => {
|
|
|
88
90
|
return result;
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
|
-
else if (!
|
|
93
|
+
else if (!record?.howler) {
|
|
92
94
|
return (_jsxs(_Fragment, { children: [_jsx(Skeleton, { width: "100%", height: 200, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 220, variant: "rounded" }), _jsx(Skeleton, { width: "100%", height: 150, variant: "rounded" })] }));
|
|
93
95
|
}
|
|
94
|
-
}, [worklogGroups,
|
|
96
|
+
}, [worklogGroups, record.howler, initialVersions, users, t, shiftColor, theme.palette.text.primary]);
|
|
95
97
|
return (_jsx(Stack, { sx: { p: 2 }, spacing: 1, children: worklogEls }));
|
|
96
98
|
};
|
|
97
|
-
export default
|
|
99
|
+
export default RecordWorklog;
|
|
@@ -4,7 +4,7 @@ import { Chip, Stack, Tooltip, Typography } from '@mui/material';
|
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
|
-
export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
7
|
+
export const ViewTitle = ({ title, type, query, sort, span, indexes }) => {
|
|
8
8
|
const { t } = useTranslation();
|
|
9
9
|
const spanLabel = useMemo(() => {
|
|
10
10
|
if (!span) {
|
|
@@ -17,9 +17,16 @@ export const ViewTitle = ({ title, type, query, sort, span }) => {
|
|
|
17
17
|
return t(span);
|
|
18
18
|
}
|
|
19
19
|
}, [span, t]);
|
|
20
|
+
const indexLabel = useMemo(() => {
|
|
21
|
+
if (!indexes || indexes.length === 0) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
else
|
|
25
|
+
return `(${indexes.join(', ')})`;
|
|
26
|
+
}, [indexes]);
|
|
20
27
|
return (_jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", alignItems: "start", spacing: 1, children: [_jsx(Tooltip, { title: t(`route.views.manager.${type}`), children: {
|
|
21
28
|
readonly: _jsx(Lock, { fontSize: "small" }),
|
|
22
29
|
global: _jsx(Language, { fontSize: "small" }),
|
|
23
30
|
personal: _jsx(Person, { fontSize: "small" })
|
|
24
|
-
}[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, {
|
|
31
|
+
}[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span || indexLabel) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { label: spanLabel }), indexLabel && _jsx(Chip, { label: indexLabel })] }))] }));
|
|
25
32
|
};
|
|
@@ -7,7 +7,7 @@ declare const useHitActions: (_hits: Hit | Hit[]) => {
|
|
|
7
7
|
canAssess: boolean;
|
|
8
8
|
loading: boolean;
|
|
9
9
|
manage: (transition: string) => Promise<void>;
|
|
10
|
-
assess: (assessment: string, skipRationale?: boolean) => Promise<void>;
|
|
10
|
+
assess: (assessment: string, skipRationale?: boolean, providedRationale?: any) => Promise<void>;
|
|
11
11
|
vote: (v: string) => Promise<void>;
|
|
12
12
|
selectedVote: string;
|
|
13
13
|
};
|
|
@@ -4,8 +4,8 @@ import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
|
4
4
|
import AssignUserDrawer from '@cccsaurora/howler-ui/components/app/drawers/AssignUserDrawer';
|
|
5
5
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
6
|
import { AppDrawerContext } from '@cccsaurora/howler-ui/components/app/providers/AppDrawerProvider';
|
|
7
|
-
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
8
7
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
8
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
9
9
|
import RationaleModal from '@cccsaurora/howler-ui/components/elements/display/modals/RationaleModal';
|
|
10
10
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
@@ -31,7 +31,7 @@ const useHitActions = (_hits) => {
|
|
|
31
31
|
const { showModal } = useContext(ModalContext);
|
|
32
32
|
const { showWarningMessage } = useMySnackbar();
|
|
33
33
|
const { dispatchApi } = useMyApi();
|
|
34
|
-
const updateHit = useContextSelector(
|
|
34
|
+
const updateHit = useContextSelector(RecordContext, ctx => ctx.updateRecord);
|
|
35
35
|
const [loading, setLoading] = useState(false);
|
|
36
36
|
const hits = useMemo(() => (Array.isArray(_hits) ? _hits : [_hits]).filter(_hit => !!_hit), [_hits]);
|
|
37
37
|
const canVote = useMemo(() => hits.every(hit => hit?.howler.assignment !== user.username || hit?.howler.status === 'in-progress'), [hits, user.username]);
|
|
@@ -90,9 +90,9 @@ const useHitActions = (_hits) => {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}, [dispatchApi, hits, selectedVote, updateHit, user.email]);
|
|
93
|
-
const assess = useCallback(async (assessment, skipRationale = false) => {
|
|
93
|
+
const assess = useCallback(async (assessment, skipRationale = false, providedRationale = null) => {
|
|
94
94
|
const rationale = skipRationale
|
|
95
|
-
? t('rationale.default', { assessment })
|
|
95
|
+
? (providedRationale ?? t('rationale.default', { assessment }))
|
|
96
96
|
: await new Promise(res => {
|
|
97
97
|
showModal(_jsx(RationaleModal, { hits: hits, onSubmit: _rationale => {
|
|
98
98
|
res(_rationale);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Api, Article, Book, Code, Dashboard, Description, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
|
|
2
|
+
import { Api, Article, Book, BookRounded, Code, Dashboard, Description, ExitToApp, FormatListBulleted, Help, HelpCenter, Key, ManageSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, SupervisorAccount, Terminal, Topic } from '@mui/icons-material';
|
|
3
3
|
import { Stack } from '@mui/material';
|
|
4
4
|
import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
|
|
5
5
|
import { AppBarContext } from '@cccsaurora/howler-ui/components/app/providers/AppBarProvider';
|
|
@@ -24,6 +24,15 @@ const useMyPreferences = () => {
|
|
|
24
24
|
icon: _jsx(Dashboard, {})
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
type: 'item',
|
|
29
|
+
element: {
|
|
30
|
+
id: 'cases',
|
|
31
|
+
i18nKey: 'route.cases',
|
|
32
|
+
route: '/cases',
|
|
33
|
+
icon: _jsx(BookRounded, {})
|
|
34
|
+
}
|
|
35
|
+
},
|
|
27
36
|
{
|
|
28
37
|
type: 'group',
|
|
29
38
|
element: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Alert, Box, Typography } from '@mui/material';
|
|
3
3
|
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
-
import
|
|
4
|
+
import HitPreview from '@cccsaurora/howler-ui/components/elements/hit/HitPreview';
|
|
5
5
|
import { useMemo } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { Link, useNavigate } from 'react-router-dom';
|
|
@@ -40,7 +40,7 @@ const useMySearch = () => {
|
|
|
40
40
|
},
|
|
41
41
|
headerRenderer: (state) => (state.result?.error || !state.items) && (_jsx(Box, { sx: { p: 1, pb: 0, textAlign: 'center' }, children: state.result?.error ? (_jsx(Alert, { severity: "error", color: "error", children: t('hit.search.invalid') })) : ((!state.items || state.items.length === 0) && (_jsx(Typography, { sx: { mb: -1, color: 'text.secondary' }, children: t('hit.quicksearch') }))) })),
|
|
42
42
|
itemRenderer: (item, options) => {
|
|
43
|
-
return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(
|
|
43
|
+
return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(HitPreview, { hit: item.item, options: options }) }));
|
|
44
44
|
}
|
|
45
45
|
}), [navigate, pageCount, t]);
|
|
46
46
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Article, Book, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
|
|
2
|
+
import { Article, Book, BookRounded, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
|
|
3
3
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -24,6 +24,9 @@ const useMySitemap = () => {
|
|
|
24
24
|
return useMemo(() => ({
|
|
25
25
|
routes: [
|
|
26
26
|
{ path: '/', title: t('route.home'), isRoot: true, icon: _jsx(Dashboard, {}) },
|
|
27
|
+
{ path: '/cases', title: t('route.cases'), isRoot: true, icon: _jsx(BookRounded, {}) },
|
|
28
|
+
{ path: '/cases/:id', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
|
|
29
|
+
{ path: '/cases/:id/*', title: t('route.cases.view'), isLeaf: true, breadcrumbs: ['/cases'] },
|
|
27
30
|
{ path: '/admin/users', title: t('route.admin.user.search'), isRoot: true, icon: _jsx(PersonSearch, {}) },
|
|
28
31
|
{
|
|
29
32
|
path: '/admin/users/:id',
|