@cccsaurora/howler-ui 2.18.0-dev.683 → 2.18.0-dev.686
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/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +6 -0
- package/api/v2/case/index.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 +34 -7
- package/components/app/hooks/useMatchers.js +2 -2
- 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/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +307 -14
- 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} +51 -70
- 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 +14 -5
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +8 -0
- package/components/elements/case/CaseCard.js +39 -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 +1 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.js +28 -48
- package/components/elements/hit/HitCard.js +5 -5
- package/components/elements/hit/HitLabels.js +2 -2
- 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 +8 -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 +23 -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 +235 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +39 -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/useParamState.test.js +3 -4
- 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 +12 -0
- package/components/routes/cases/detail/CaseAssets.js +101 -0
- package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseAssets.test.js +163 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +51 -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 +6 -0
- package/components/routes/cases/detail/CaseSidebar.js +61 -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/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 +31 -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 +12 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -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 +13 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +25 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +38 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +59 -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 +65 -3
- package/locales/fr/translation.json +63 -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 +19 -2
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/utils.d.ts +2 -0
- package/tests/utils.js +8 -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
|
@@ -91,13 +91,13 @@ vi.mock('@mui/material', async () => {
|
|
|
91
91
|
});
|
|
92
92
|
// Import component after mocks
|
|
93
93
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
94
|
-
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
95
94
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
95
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
96
96
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
97
97
|
import { I18nextProvider } from 'react-i18next';
|
|
98
98
|
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
99
99
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
100
|
-
import
|
|
100
|
+
import RecordContextMenu from './RecordContextMenu';
|
|
101
101
|
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
102
102
|
const mockConfig = {
|
|
103
103
|
lookups: {
|
|
@@ -105,16 +105,16 @@ const mockConfig = {
|
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
107
|
const mockApiContext = { config: mockConfig };
|
|
108
|
-
const
|
|
109
|
-
|
|
108
|
+
const mockRecordContext = {
|
|
109
|
+
records: {
|
|
110
110
|
'test-hit-1': createMockHit()
|
|
111
111
|
},
|
|
112
|
-
|
|
112
|
+
selectedRecords: []
|
|
113
113
|
};
|
|
114
114
|
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
115
115
|
// Test wrapper
|
|
116
116
|
const Wrapper = ({ children }) => {
|
|
117
|
-
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(
|
|
117
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(RecordContext.Provider, { value: mockRecordContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
|
|
118
118
|
};
|
|
119
119
|
describe('HitContextMenu', () => {
|
|
120
120
|
let user;
|
|
@@ -122,11 +122,11 @@ describe('HitContextMenu', () => {
|
|
|
122
122
|
beforeEach(() => {
|
|
123
123
|
user = userEvent.setup();
|
|
124
124
|
vi.clearAllMocks();
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
mockRecordContext.selectedRecords.length = 0;
|
|
126
|
+
mockRecordContext.records['test-hit-1'] = createMockHit();
|
|
127
127
|
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
128
128
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
129
|
-
rerender = render(_jsx(Wrapper, { children: _jsx(
|
|
129
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
130
130
|
});
|
|
131
131
|
describe('Context Menu Initialization', () => {
|
|
132
132
|
it('should open menu on right-click', async () => {
|
|
@@ -190,13 +190,13 @@ describe('HitContextMenu', () => {
|
|
|
190
190
|
});
|
|
191
191
|
it('should disable "Open Hit" when hit is null', async () => {
|
|
192
192
|
act(() => {
|
|
193
|
-
|
|
193
|
+
mockRecordContext.records['test-hit-1'] = null;
|
|
194
194
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
195
195
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
196
196
|
});
|
|
197
197
|
await waitFor(() => {
|
|
198
198
|
const menuItems = screen.getAllByRole('menuitem');
|
|
199
|
-
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit
|
|
199
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit'));
|
|
200
200
|
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
201
201
|
});
|
|
202
202
|
});
|
|
@@ -237,7 +237,7 @@ describe('HitContextMenu', () => {
|
|
|
237
237
|
skip_rationale: false
|
|
238
238
|
}
|
|
239
239
|
}));
|
|
240
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
240
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
241
241
|
act(() => {
|
|
242
242
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
243
243
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -295,7 +295,7 @@ describe('HitContextMenu', () => {
|
|
|
295
295
|
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
296
296
|
];
|
|
297
297
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
298
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
298
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
299
299
|
act(() => {
|
|
300
300
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
301
301
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -339,7 +339,7 @@ describe('HitContextMenu', () => {
|
|
|
339
339
|
});
|
|
340
340
|
it('should disable custom actions menu when no actions are available', async () => {
|
|
341
341
|
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
342
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
342
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
343
343
|
act(() => {
|
|
344
344
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
345
345
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -381,7 +381,7 @@ describe('HitContextMenu', () => {
|
|
|
381
381
|
skip_rationale: true
|
|
382
382
|
}
|
|
383
383
|
}));
|
|
384
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
384
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
385
385
|
act(() => {
|
|
386
386
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
387
387
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -449,7 +449,7 @@ describe('HitContextMenu', () => {
|
|
|
449
449
|
it('should call executeAction with action_id and hit query', async () => {
|
|
450
450
|
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
451
451
|
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
452
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
452
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
453
453
|
act(() => {
|
|
454
454
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
455
455
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -502,7 +502,7 @@ describe('HitContextMenu', () => {
|
|
|
502
502
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
503
503
|
keys: ['howler.detection', 'event.id']
|
|
504
504
|
}));
|
|
505
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
505
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
506
506
|
});
|
|
507
507
|
it('should render exclusion submenu with template keys', async () => {
|
|
508
508
|
act(() => {
|
|
@@ -550,7 +550,7 @@ describe('HitContextMenu', () => {
|
|
|
550
550
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
551
551
|
keys: ['howler.outline.indicators']
|
|
552
552
|
}));
|
|
553
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
553
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
554
554
|
act(() => {
|
|
555
555
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
556
556
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -593,7 +593,7 @@ describe('HitContextMenu', () => {
|
|
|
593
593
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
594
594
|
keys: []
|
|
595
595
|
}));
|
|
596
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
596
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
597
597
|
act(() => {
|
|
598
598
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
599
599
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -605,7 +605,7 @@ describe('HitContextMenu', () => {
|
|
|
605
605
|
});
|
|
606
606
|
it('should skip null field values in exclusion menu', async () => {
|
|
607
607
|
act(() => {
|
|
608
|
-
|
|
608
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
609
609
|
});
|
|
610
610
|
act(() => {
|
|
611
611
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -628,7 +628,7 @@ describe('HitContextMenu', () => {
|
|
|
628
628
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
629
629
|
keys: ['howler.detection', 'event.id']
|
|
630
630
|
}));
|
|
631
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
631
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
632
632
|
});
|
|
633
633
|
it('should render inclusion submenu with template keys', async () => {
|
|
634
634
|
act(() => {
|
|
@@ -676,7 +676,7 @@ describe('HitContextMenu', () => {
|
|
|
676
676
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
677
677
|
keys: ['howler.outline.indicators']
|
|
678
678
|
}));
|
|
679
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
679
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
680
680
|
act(() => {
|
|
681
681
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
682
682
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -719,7 +719,7 @@ describe('HitContextMenu', () => {
|
|
|
719
719
|
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
720
720
|
keys: []
|
|
721
721
|
}));
|
|
722
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
722
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
723
723
|
act(() => {
|
|
724
724
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
725
725
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -731,7 +731,7 @@ describe('HitContextMenu', () => {
|
|
|
731
731
|
});
|
|
732
732
|
it('should skip null field values in inclusion menu', async () => {
|
|
733
733
|
act(() => {
|
|
734
|
-
|
|
734
|
+
mockRecordContext.records['test-hit-1'].event = {};
|
|
735
735
|
});
|
|
736
736
|
act(() => {
|
|
737
737
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -750,24 +750,24 @@ describe('HitContextMenu', () => {
|
|
|
750
750
|
});
|
|
751
751
|
});
|
|
752
752
|
describe('Multiple Hit Selection', () => {
|
|
753
|
-
it('should use
|
|
753
|
+
it('should use selectedRecords when current hit is included', async () => {
|
|
754
754
|
act(() => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
755
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
756
|
+
mockRecordContext.records['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
|
|
757
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1'], mockRecordContext.records['hit-2']);
|
|
758
758
|
mockGetSelectedId.mockReturnValue('hit-1');
|
|
759
759
|
});
|
|
760
760
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
761
761
|
await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
|
|
762
|
-
// The component should use
|
|
762
|
+
// The component should use selectedRecords for actions
|
|
763
763
|
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
764
764
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
765
765
|
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
766
766
|
});
|
|
767
|
-
it('should use only current hit when not in
|
|
767
|
+
it('should use only current hit when not in selectedRecords', async () => {
|
|
768
768
|
act(() => {
|
|
769
|
-
|
|
770
|
-
|
|
769
|
+
mockRecordContext.records['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
770
|
+
mockRecordContext.selectedRecords.push(mockRecordContext.records['hit-1']);
|
|
771
771
|
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
772
772
|
});
|
|
773
773
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
@@ -786,12 +786,12 @@ describe('HitContextMenu', () => {
|
|
|
786
786
|
});
|
|
787
787
|
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
788
788
|
await waitFor(() => {
|
|
789
|
-
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(
|
|
789
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
790
790
|
});
|
|
791
791
|
});
|
|
792
792
|
it('should call getMatchingTemplate when menu opens', async () => {
|
|
793
793
|
await waitFor(() => {
|
|
794
|
-
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(
|
|
794
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockRecordContext.records['test-hit-1']);
|
|
795
795
|
});
|
|
796
796
|
});
|
|
797
797
|
it('should reset state when menu closes', async () => {
|
|
@@ -824,7 +824,7 @@ describe('HitContextMenu', () => {
|
|
|
824
824
|
describe('Edge Cases and Error Handling', () => {
|
|
825
825
|
it('should not crash when hit is null', async () => {
|
|
826
826
|
act(() => {
|
|
827
|
-
|
|
827
|
+
mockRecordContext.records = {};
|
|
828
828
|
});
|
|
829
829
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
830
830
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -835,7 +835,7 @@ describe('HitContextMenu', () => {
|
|
|
835
835
|
});
|
|
836
836
|
it('should not render exclusion menu when template is null', async () => {
|
|
837
837
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
838
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
838
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
839
839
|
act(() => {
|
|
840
840
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
841
841
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -849,7 +849,7 @@ describe('HitContextMenu', () => {
|
|
|
849
849
|
});
|
|
850
850
|
it('should not render inclusion menu when template is null', async () => {
|
|
851
851
|
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
852
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
852
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
853
853
|
act(() => {
|
|
854
854
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
855
855
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -863,7 +863,7 @@ describe('HitContextMenu', () => {
|
|
|
863
863
|
});
|
|
864
864
|
it('should handle API failure gracefully', async () => {
|
|
865
865
|
mockDispatchApi.mockResolvedValue(null);
|
|
866
|
-
rerender(_jsx(Wrapper, { children: _jsx(
|
|
866
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
867
867
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
868
868
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
869
869
|
await waitFor(() => {
|
|
@@ -874,7 +874,7 @@ describe('HitContextMenu', () => {
|
|
|
874
874
|
});
|
|
875
875
|
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
876
876
|
act(() => {
|
|
877
|
-
|
|
877
|
+
mockRecordContext.records['test-hit-1'].howler.analytic = null;
|
|
878
878
|
});
|
|
879
879
|
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
880
880
|
fireEvent.contextMenu(contextMenuWrapper);
|
|
@@ -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'), 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',
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
const DEFAULT_THEME = {
|
|
2
|
+
components: {
|
|
3
|
+
MuiChip: {
|
|
4
|
+
defaultProps: {
|
|
5
|
+
size: 'small'
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
},
|
|
2
9
|
palette: {
|
|
3
10
|
dark: {
|
|
4
11
|
background: {
|
|
5
|
-
default: '#
|
|
6
|
-
paper: '#
|
|
12
|
+
default: '#181818',
|
|
13
|
+
paper: '#181818'
|
|
7
14
|
},
|
|
8
15
|
primary: {
|
|
9
16
|
main: '#7DA1DB'
|
|
@@ -5,10 +5,9 @@ import { MemoryRouter, useSearchParams } from 'react-router-dom';
|
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
6
|
import useParamState from './useParamState';
|
|
7
7
|
// Creates a MemoryRouter wrapper using createElement to avoid JSX in a .ts file
|
|
8
|
-
const makeWrapper = (search = '') =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
8
|
+
const makeWrapper = (search = '') =>
|
|
9
|
+
// eslint-disable-next-line react/function-component-definition
|
|
10
|
+
({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
|
|
12
11
|
// Composite hook: exposes the param state AND the live URL params for URL-level assertions
|
|
13
12
|
const useParamStateWithUrl = (key, defaultValue) => {
|
|
14
13
|
const [value, setValue] = useParamState(key, defaultValue);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
2
|
import type React from 'react';
|
|
3
|
-
declare const
|
|
3
|
+
declare const useRecordSelection: () => {
|
|
4
4
|
lastSelected: string;
|
|
5
5
|
setLastSelected: React.Dispatch<React.SetStateAction<string>>;
|
|
6
6
|
onClick: (e: React.MouseEvent<HTMLDivElement>, hit: Hit) => void;
|
|
7
7
|
};
|
|
8
|
-
export default
|
|
8
|
+
export default useRecordSelection;
|