@cccsaurora/howler-ui 2.18.0-dev.732 → 2.18.0-dev.736
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} +51 -70
- 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/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 +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 +114 -97
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
import { setupContextSelectorMock } from '@cccsaurora/howler-ui/tests/mocks';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
setupContextSelectorMock();
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mocks
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
vi.mock('react-router-dom', async () => {
|
|
12
|
+
const actual = await vi.importActual('react-router-dom');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
Link: ({ to, children, ...props }) => (_jsx("a", { href: to, ...props, children: children })),
|
|
16
|
+
useLocation: vi.fn(() => ({ pathname: '/', search: '' }))
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
// Controllable draggable mock — lets individual tests flip isDragging and transform
|
|
20
|
+
const mockDraggable = vi.hoisted(() => ({
|
|
21
|
+
isDragging: false,
|
|
22
|
+
transform: null
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('@dnd-kit/core', () => ({
|
|
25
|
+
useDraggable: vi.fn(() => ({
|
|
26
|
+
attributes: {},
|
|
27
|
+
listeners: {},
|
|
28
|
+
setNodeRef: vi.fn(),
|
|
29
|
+
transform: mockDraggable.transform,
|
|
30
|
+
isDragging: mockDraggable.isDragging,
|
|
31
|
+
active: null
|
|
32
|
+
})),
|
|
33
|
+
useDroppable: vi.fn(() => ({
|
|
34
|
+
setNodeRef: vi.fn(),
|
|
35
|
+
isOver: false
|
|
36
|
+
}))
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('@dnd-kit/utilities', () => ({
|
|
39
|
+
CSS: { Transform: { toString: (t) => (t ? `translate3d(${t.x}px,${t.y}px,0)` : '') } }
|
|
40
|
+
}));
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Imports after mocks
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
import { useDroppable } from '@dnd-kit/core';
|
|
45
|
+
import FolderEntry from './FolderEntry';
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const hitItem = (path, value = path) => ({ type: 'hit', value, path });
|
|
50
|
+
const renderEntry = (props = {}) => render(_jsx(MemoryRouter, { children: _jsx(FolderEntry, { caseId: "case-1", path: "folder/item", indent: 1, label: "my label", itemType: "hit", ...props }) }));
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Setup
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockDraggable.isDragging = false;
|
|
56
|
+
mockDraggable.transform = null;
|
|
57
|
+
vi.mocked(useDroppable).mockReturnValue({ setNodeRef: vi.fn(), isOver: false });
|
|
58
|
+
});
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Tests
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
describe('FolderEntry', () => {
|
|
63
|
+
describe('rendering', () => {
|
|
64
|
+
it('renders the label text', () => {
|
|
65
|
+
renderEntry({ label: 'hello world' });
|
|
66
|
+
expect(screen.getByText('hello world')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('renders as a link when `to` is provided', () => {
|
|
69
|
+
renderEntry({ to: '/cases/case-1/folder/item' });
|
|
70
|
+
const el = screen.getByText('my label').closest('a');
|
|
71
|
+
expect(el).toHaveAttribute('href', '/cases/case-1/folder/item');
|
|
72
|
+
});
|
|
73
|
+
it('opens link in new tab for reference items', () => {
|
|
74
|
+
renderEntry({ itemType: 'reference', to: 'https://example.com', label: 'ext' });
|
|
75
|
+
const el = screen.getByText('ext').closest('a');
|
|
76
|
+
expect(el).toHaveAttribute('target', '_blank');
|
|
77
|
+
expect(el).toHaveAttribute('rel', 'noopener noreferrer');
|
|
78
|
+
});
|
|
79
|
+
it('does not render as a link when `to` is omitted', () => {
|
|
80
|
+
renderEntry({ to: undefined });
|
|
81
|
+
expect(screen.getByText('my label').closest('a')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('shows the chevron for folder items', () => {
|
|
84
|
+
// Chevron is visible (non-zero opacity) for folder and case types
|
|
85
|
+
const { container } = renderEntry({ itemType: 'folder' });
|
|
86
|
+
// ChevronRight is the first svg inside the row
|
|
87
|
+
const chevron = container.querySelector('svg');
|
|
88
|
+
expect(chevron).toBeInTheDocument();
|
|
89
|
+
expect(chevron).not.toHaveStyle('opacity: 0');
|
|
90
|
+
});
|
|
91
|
+
it('hides the chevron for non-folder, non-case items', () => {
|
|
92
|
+
const { container } = renderEntry({ itemType: 'hit' });
|
|
93
|
+
const chevron = container.querySelector('svg');
|
|
94
|
+
// Chevron rendered but with opacity 0 (applied via MUI sx / emotion class)
|
|
95
|
+
expect(chevron).toBeInTheDocument();
|
|
96
|
+
expect(chevron).toHaveStyle({ opacity: '0' });
|
|
97
|
+
});
|
|
98
|
+
it('rotates chevron when chevronOpen is true', () => {
|
|
99
|
+
const { container } = renderEntry({ itemType: 'folder', chevronOpen: true });
|
|
100
|
+
const chevron = container.querySelector('svg');
|
|
101
|
+
expect(chevron).toHaveStyle({ transform: 'rotate(90deg)' });
|
|
102
|
+
});
|
|
103
|
+
it('does not rotate chevron when chevronOpen is false', () => {
|
|
104
|
+
const { container } = renderEntry({ itemType: 'folder', chevronOpen: false });
|
|
105
|
+
const chevron = container.querySelector('svg');
|
|
106
|
+
expect(chevron).toHaveStyle({ transform: 'rotate(0deg)' });
|
|
107
|
+
});
|
|
108
|
+
it('falls back to the Folder icon for unknown itemTypes', () => {
|
|
109
|
+
// Should not throw; renders something
|
|
110
|
+
expect(() => renderEntry({ itemType: 'unknown-type' })).not.toThrow();
|
|
111
|
+
expect(screen.getByText('my label')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('drag state', () => {
|
|
115
|
+
it('is fully visible when not dragging', () => {
|
|
116
|
+
const { container } = renderEntry();
|
|
117
|
+
const row = container.firstElementChild;
|
|
118
|
+
expect(row.style.opacity).toBe('');
|
|
119
|
+
});
|
|
120
|
+
it('becomes invisible (opacity 0) while dragging', () => {
|
|
121
|
+
mockDraggable.isDragging = true;
|
|
122
|
+
const { container } = renderEntry();
|
|
123
|
+
const row = container.firstElementChild;
|
|
124
|
+
expect(row.style.opacity).toBe('0');
|
|
125
|
+
});
|
|
126
|
+
it('does not render as a link while dragging even if `to` is set', () => {
|
|
127
|
+
mockDraggable.isDragging = true;
|
|
128
|
+
renderEntry({ to: '/cases/case-1/folder/item' });
|
|
129
|
+
// isLink = to != null && !isDragging → false, so no <a> wrapping
|
|
130
|
+
expect(screen.getByText('my label').closest('a')).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
it('applies the CSS transform from useDraggable', () => {
|
|
133
|
+
mockDraggable.transform = { x: 42, y: 10, scaleX: 1, scaleY: 1 };
|
|
134
|
+
const { container } = renderEntry();
|
|
135
|
+
const row = container.firstElementChild;
|
|
136
|
+
expect(row.style.transform).toMatch(/translate3d\(42px,10px/);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('droppable behaviour', () => {
|
|
140
|
+
it('disables the droppable for non-folder items', () => {
|
|
141
|
+
renderEntry({ itemType: 'hit', caseId: 'case-1' });
|
|
142
|
+
expect(vi.mocked(useDroppable)).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
|
|
143
|
+
});
|
|
144
|
+
it('enables the droppable for folder items when caseId is set and not dragging', () => {
|
|
145
|
+
renderEntry({ itemType: 'folder', caseId: 'case-1' });
|
|
146
|
+
expect(vi.mocked(useDroppable)).toHaveBeenCalledWith(expect.objectContaining({ disabled: false }));
|
|
147
|
+
});
|
|
148
|
+
it('disables the droppable for folder items when caseId is null', () => {
|
|
149
|
+
renderEntry({ itemType: 'folder', caseId: null });
|
|
150
|
+
expect(vi.mocked(useDroppable)).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
|
|
151
|
+
});
|
|
152
|
+
it('shows the drop highlight border when isOver and caseId match', () => {
|
|
153
|
+
vi.mocked(useDroppable).mockReturnValue({ setNodeRef: vi.fn(), isOver: true });
|
|
154
|
+
const { container } = renderEntry({ caseId: 'case-1', itemType: 'folder' });
|
|
155
|
+
// The absolute overlay Box has a dynamic border-color — it should be present in DOM
|
|
156
|
+
const overlay = container.querySelector('[style*="border"]') ?? container.querySelector('div > div');
|
|
157
|
+
expect(overlay).toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('DnD ID namespacing', () => {
|
|
161
|
+
it('passes the namespaced id to useDraggable', async () => {
|
|
162
|
+
const { useDraggable } = await import('@dnd-kit/core');
|
|
163
|
+
renderEntry({ caseId: 'case-1', itemType: 'hit', path: 'folder/item' });
|
|
164
|
+
expect(vi.mocked(useDraggable)).toHaveBeenCalledWith(expect.objectContaining({ id: 'case-1:hit:folder/item' }));
|
|
165
|
+
});
|
|
166
|
+
it('passes the namespaced id to useDroppable', async () => {
|
|
167
|
+
renderEntry({ caseId: 'case-1', itemType: 'folder', path: 'docs' });
|
|
168
|
+
expect(vi.mocked(useDroppable)).toHaveBeenCalledWith(expect.objectContaining({ id: 'case-1:folder:docs' }));
|
|
169
|
+
});
|
|
170
|
+
it('uses empty string prefix when caseId is null', async () => {
|
|
171
|
+
const { useDraggable } = await import('@dnd-kit/core');
|
|
172
|
+
renderEntry({ caseId: null, itemType: 'hit', path: 'item' });
|
|
173
|
+
expect(vi.mocked(useDraggable)).toHaveBeenCalledWith(expect.objectContaining({ id: ':hit:item' }));
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe('drag disabled when caseId is falsy', () => {
|
|
177
|
+
it('disables dragging when caseId is null', async () => {
|
|
178
|
+
const { useDraggable } = await import('@dnd-kit/core');
|
|
179
|
+
renderEntry({ caseId: null });
|
|
180
|
+
expect(vi.mocked(useDraggable)).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
|
|
181
|
+
});
|
|
182
|
+
it('enables dragging when caseId is set', async () => {
|
|
183
|
+
const { useDraggable } = await import('@dnd-kit/core');
|
|
184
|
+
renderEntry({ caseId: 'case-1' });
|
|
185
|
+
expect(vi.mocked(useDraggable)).toHaveBeenCalledWith(expect.objectContaining({ disabled: false }));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('click handler', () => {
|
|
189
|
+
it('calls onClick when the entry is clicked', async () => {
|
|
190
|
+
const onClick = vi.fn();
|
|
191
|
+
renderEntry({ onClick });
|
|
192
|
+
await userEvent.click(screen.getByText('my label'));
|
|
193
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('drag data payload', () => {
|
|
197
|
+
it('includes type, label, entry, and caseId in drag data', async () => {
|
|
198
|
+
const { useDraggable } = await import('@dnd-kit/core');
|
|
199
|
+
const entry = hitItem('folder/item');
|
|
200
|
+
renderEntry({ caseId: 'case-1', itemType: 'hit', label: 'my label', entry });
|
|
201
|
+
expect(vi.mocked(useDraggable)).toHaveBeenCalledWith(expect.objectContaining({
|
|
202
|
+
data: { type: 'hit', label: 'my label', entry, caseId: 'case-1' }
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDndContext, useDroppable } from '@dnd-kit/core';
|
|
3
|
+
import { Inbox } from '@mui/icons-material';
|
|
4
|
+
import { alpha, Box, Typography } from '@mui/material';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
const RootDropZone = ({ caseId }) => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { active } = useDndContext();
|
|
9
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
10
|
+
id: `${caseId}:folder:__root__`,
|
|
11
|
+
data: { path: '', caseId }
|
|
12
|
+
});
|
|
13
|
+
if (!active) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return (_jsxs(Box, { ref: setNodeRef, sx: theme => ({
|
|
17
|
+
minHeight: '250px',
|
|
18
|
+
display: 'flex',
|
|
19
|
+
flexDirection: 'column',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
gap: 1,
|
|
23
|
+
border: '2px dashed',
|
|
24
|
+
borderColor: isOver ? theme.palette.primary.main : theme.palette.divider,
|
|
25
|
+
borderRadius: 1,
|
|
26
|
+
mx: 1,
|
|
27
|
+
mt: 1,
|
|
28
|
+
transition: theme.transitions.create(['border-color', 'background-color']),
|
|
29
|
+
bgcolor: isOver ? alpha(theme.palette.primary.main, 0.08) : 'transparent',
|
|
30
|
+
color: isOver ? theme.palette.primary.main : theme.palette.text.secondary
|
|
31
|
+
}), children: [_jsx(Inbox, { fontSize: "large", sx: { opacity: 0.6 } }), _jsx(Typography, { variant: "caption", children: t('page.cases.folder.drop.root') })] }));
|
|
32
|
+
};
|
|
33
|
+
export default RootDropZone;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { get, set, sortBy, take } from 'lodash-es';
|
|
2
|
+
export const buildTree = (items = []) => {
|
|
3
|
+
// Root tree node stores direct children in `leaves`; subfolders live under `folders`.
|
|
4
|
+
const tree = { leaves: [], path: '' };
|
|
5
|
+
sortBy(items, 'path').forEach(item => {
|
|
6
|
+
// Ignore items that cannot be placed in the folder structure.
|
|
7
|
+
if (!item?.path) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
// Split path into folder segments + item name, then remove the item name.
|
|
11
|
+
const parts = item.path.split('/');
|
|
12
|
+
parts.pop();
|
|
13
|
+
// Ensure each folder node exists and has its path set.
|
|
14
|
+
parts.forEach((_, index) => {
|
|
15
|
+
const folderPath = `folders.${take(parts, index + 1).join('.folders.')}`;
|
|
16
|
+
set(tree, `${folderPath}.path`, take(parts, index + 1).join('/'));
|
|
17
|
+
});
|
|
18
|
+
if (parts.length > 0) {
|
|
19
|
+
// Navigate to the target folder via the `folders` nesting and append the leaf.
|
|
20
|
+
const folderPath = `folders.${parts.join('.folders.')}`;
|
|
21
|
+
const size = get(tree, folderPath)?.leaves?.length ?? 0;
|
|
22
|
+
set(tree, `${folderPath}.leaves.${size}`, item);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Items without parent folders are top-level leaves.
|
|
26
|
+
tree.leaves.push(item);
|
|
27
|
+
});
|
|
28
|
+
return tree;
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildTree } from './utils';
|
|
3
|
+
const hit = (path, value = path) => ({ type: 'hit', value, path });
|
|
4
|
+
describe('buildTree', () => {
|
|
5
|
+
it('returns an empty tree for no items', () => {
|
|
6
|
+
expect(buildTree()).toEqual({ path: '', leaves: [] });
|
|
7
|
+
});
|
|
8
|
+
it('returns an empty tree for an empty array', () => {
|
|
9
|
+
expect(buildTree([])).toEqual({ path: '', leaves: [] });
|
|
10
|
+
});
|
|
11
|
+
it('ignores items with no path', () => {
|
|
12
|
+
const noPath = { type: 'hit', value: 'x' };
|
|
13
|
+
expect(buildTree([noPath])).toEqual({ path: '', leaves: [] });
|
|
14
|
+
});
|
|
15
|
+
it('places a path-less item alongside a valid one without affecting the folder', () => {
|
|
16
|
+
const valid = hit('folder/item');
|
|
17
|
+
const noPath = { type: 'hit', value: 'x' };
|
|
18
|
+
const result = buildTree([valid, noPath]);
|
|
19
|
+
expect(result.leaves).toEqual([]);
|
|
20
|
+
expect(result.folders?.folder.leaves).toEqual([valid]);
|
|
21
|
+
});
|
|
22
|
+
it('places an item with a filename but no folder as a top-level leaf', () => {
|
|
23
|
+
const flat = hit('flat-item');
|
|
24
|
+
const result = buildTree([flat]);
|
|
25
|
+
expect(result.leaves).toEqual([flat]);
|
|
26
|
+
expect(result.folders).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
it('places an item in a single-level folder', () => {
|
|
29
|
+
const nested = hit('folderA/item');
|
|
30
|
+
const result = buildTree([nested]);
|
|
31
|
+
expect(result.leaves).toEqual([]);
|
|
32
|
+
expect(result.folders?.folderA).toMatchObject({ path: 'folderA', leaves: [nested] });
|
|
33
|
+
});
|
|
34
|
+
it('sets path correctly for a single-level folder', () => {
|
|
35
|
+
const result = buildTree([hit('folderA/item')]);
|
|
36
|
+
expect(result.folders?.folderA.path).toBe('folderA');
|
|
37
|
+
});
|
|
38
|
+
it('places an item in a two-level nested folder', () => {
|
|
39
|
+
const deep = hit('a/b/item');
|
|
40
|
+
const result = buildTree([deep]);
|
|
41
|
+
expect(result.folders?.a.folders?.b.leaves).toEqual([deep]);
|
|
42
|
+
});
|
|
43
|
+
it('sets path correctly for deeply nested folders', () => {
|
|
44
|
+
const result = buildTree([hit('a/b/item')]);
|
|
45
|
+
expect(result.folders?.a.path).toBe('a');
|
|
46
|
+
expect(result.folders?.a.folders?.b.path).toBe('a/b');
|
|
47
|
+
});
|
|
48
|
+
it('groups multiple items under the same folder', () => {
|
|
49
|
+
const item1 = hit('folder/item1', 'v1');
|
|
50
|
+
const item2 = hit('folder/item2', 'v2');
|
|
51
|
+
const result = buildTree([item1, item2]);
|
|
52
|
+
const leaves = result.folders?.folder.leaves ?? [];
|
|
53
|
+
expect(leaves).toHaveLength(2);
|
|
54
|
+
expect(leaves).toEqual(expect.arrayContaining([item1, item2]));
|
|
55
|
+
});
|
|
56
|
+
it('handles multiple top-level folders independently', () => {
|
|
57
|
+
const hitA = hit('folderA/item', 'a');
|
|
58
|
+
const hitB = hit('folderB/item', 'b');
|
|
59
|
+
const result = buildTree([hitA, hitB]);
|
|
60
|
+
expect(result.folders?.folderA.leaves).toEqual([hitA]);
|
|
61
|
+
expect(result.folders?.folderB.leaves).toEqual([hitB]);
|
|
62
|
+
});
|
|
63
|
+
it('handles a mix of top-level leaves and folder items', () => {
|
|
64
|
+
const flat = hit('flat-item', 'flat');
|
|
65
|
+
const nested = hit('folder/item', 'nested');
|
|
66
|
+
const result = buildTree([flat, nested]);
|
|
67
|
+
expect(result.leaves).toEqual([flat]);
|
|
68
|
+
expect(result.folders?.folder.leaves).toEqual([nested]);
|
|
69
|
+
});
|
|
70
|
+
it('places items across sibling folders at different depths', () => {
|
|
71
|
+
const shallow = hit('a/item', 'shallow');
|
|
72
|
+
const deep = hit('a/b/item', 'deep');
|
|
73
|
+
const result = buildTree([shallow, deep]);
|
|
74
|
+
expect(result.folders?.a.leaves).toEqual([shallow]);
|
|
75
|
+
expect(result.folders?.a.folders?.b.leaves).toEqual([deep]);
|
|
76
|
+
});
|
|
77
|
+
it('preserves all item fields on leaves', () => {
|
|
78
|
+
const rich = { type: 'reference', value: 'https://example.com', path: 'links/example' };
|
|
79
|
+
const result = buildTree([rich]);
|
|
80
|
+
expect(result.folders?.links.leaves?.[0]).toEqual(rich);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
|
+
interface CaseArguments {
|
|
3
|
+
case?: Case;
|
|
4
|
+
caseId?: string;
|
|
5
|
+
}
|
|
6
|
+
interface CaseResult {
|
|
7
|
+
case: Case;
|
|
8
|
+
update: (update: Partial<Case>, publish?: boolean) => Promise<void>;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
missing: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const useCase: (args: CaseArguments) => CaseResult;
|
|
13
|
+
export default useCase;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
2
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
const useCase = ({ caseId, case: providedCase }) => {
|
|
5
|
+
const { dispatchApi } = useMyApi();
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const [missing, setMissing] = useState(false);
|
|
8
|
+
const [_case, setCase] = useState(providedCase);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (providedCase) {
|
|
11
|
+
setCase(providedCase);
|
|
12
|
+
}
|
|
13
|
+
}, [providedCase]);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (caseId) {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
dispatchApi(api.v2.case.get(caseId), { throwError: false })
|
|
18
|
+
.then(setCase)
|
|
19
|
+
.finally(() => setLoading(false));
|
|
20
|
+
}
|
|
21
|
+
}, [caseId, dispatchApi]);
|
|
22
|
+
const update = useCallback(async (_updatedCase, publish = true) => {
|
|
23
|
+
if (!_case?.case_id) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (publish) {
|
|
28
|
+
setCase(await dispatchApi(api.v2.case.put(_case.case_id, _updatedCase)));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
setCase(prevCase => {
|
|
32
|
+
if (!prevCase) {
|
|
33
|
+
return prevCase;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...prevCase,
|
|
37
|
+
..._updatedCase
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
setMissing(true);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}, [_case?.case_id, dispatchApi]);
|
|
49
|
+
return { case: _case, update, loading, missing };
|
|
50
|
+
};
|
|
51
|
+
export default useCase;
|
|
@@ -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 AddToCaseModal: FC<{
|
|
5
|
+
records: (Hit | Observable)[];
|
|
6
|
+
}>;
|
|
7
|
+
export default AddToCaseModal;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Autocomplete, Button, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
|
+
import CaseCard from '@cccsaurora/howler-ui/components/elements/case/CaseCard';
|
|
7
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
8
|
+
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
const AddToCaseModal = ({ records }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const { dispatchApi } = useMyApi();
|
|
13
|
+
const { close } = useContext(ModalContext);
|
|
14
|
+
const [cases, setCases] = useState([]);
|
|
15
|
+
const [selectedCase, setSelectedCase] = useState(null);
|
|
16
|
+
const [path, setPath] = useState('');
|
|
17
|
+
const [title, setTitle] = useState('');
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
dispatchApi(api.search.case.post({ query: 'case_id:*', rows: 100 }), { throwError: false }).then(result => {
|
|
20
|
+
if (result) {
|
|
21
|
+
setCases(result.items);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}, [dispatchApi]);
|
|
25
|
+
const folderOptions = useMemo(() => {
|
|
26
|
+
if (!selectedCase?.items) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const paths = new Set();
|
|
30
|
+
for (const item of selectedCase.items) {
|
|
31
|
+
if (!item.path) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const parts = item.path.split('/');
|
|
35
|
+
parts.pop();
|
|
36
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
37
|
+
paths.add(parts.slice(0, i).join('/'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Array.from(paths).sort();
|
|
41
|
+
}, [selectedCase]);
|
|
42
|
+
const fullPath = path ? `${path}/${title}` : title;
|
|
43
|
+
const isValid = !!selectedCase && !!title;
|
|
44
|
+
const onSubmit = async () => {
|
|
45
|
+
if (!selectedCase || records?.length < 1) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await dispatchApi(api.v2.case.items.post(selectedCase.case_id, {
|
|
49
|
+
path: fullPath,
|
|
50
|
+
value: records[0].howler.id,
|
|
51
|
+
type: records[0].__index
|
|
52
|
+
}));
|
|
53
|
+
close();
|
|
54
|
+
};
|
|
55
|
+
// TODO: No support currently for multiple records
|
|
56
|
+
return (_jsxs(Stack, { spacing: 2, p: 2, sx: { minWidth: 'min(800px, 60vw)', height: '100%' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.add_to_case') }), _jsx(Autocomplete, { options: cases, getOptionLabel: option => option.title ?? option.case_id ?? '', isOptionEqualToValue: (option, value) => option.case_id === value.case_id, value: selectedCase, disablePortal: true, onChange: (_ev, newVal) => {
|
|
57
|
+
setSelectedCase(newVal);
|
|
58
|
+
setPath('');
|
|
59
|
+
}, renderOption: (props, option) => (_createElement("li", { ...props, key: option.case_id, style: { ...props.style, display: 'flex', justifyContent: 'stretch', alignItems: 'stretch' } },
|
|
60
|
+
_jsx(CaseCard, { case: option, slotProps: { card: { sx: { width: '100%' } } } }))), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_case'), fullWidth: true })) }), selectedCase && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { freeSolo: true, disablePortal: true, options: folderOptions, value: path, onInputChange: (_ev, newVal) => setPath(newVal), renderInput: params => (_jsx(TextField, { ...params, size: "small", placeholder: t('modal.cases.add_to_case.select_path'), fullWidth: true })) }), _jsx(TextField, { size: "small", placeholder: t('modal.cases.add_to_case.title'), value: title, onChange: ev => setTitle(ev.target.value), fullWidth: true }), title && (_jsx(Typography, { variant: "caption", color: "textSecondary", children: t('modal.cases.add_to_case.full_path', { path: fullPath }) }))] })), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", color: "error", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", color: "success", disabled: !isValid, onClick: onSubmit, children: t('confirm') })] })] }));
|
|
61
|
+
};
|
|
62
|
+
export default AddToCaseModal;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
|
|
2
|
+
import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
|
|
3
|
+
import { type FC } from 'react';
|
|
4
|
+
declare const RenameItemModal: FC<{
|
|
5
|
+
_case: Case;
|
|
6
|
+
leaf: Item;
|
|
7
|
+
onRenamed?: (updatedCase: Case) => void;
|
|
8
|
+
}>;
|
|
9
|
+
export default RenameItemModal;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Stack, TextField, Typography } from '@mui/material';
|
|
3
|
+
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
5
|
+
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
6
|
+
import { useContext, useMemo, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
const RenameItemModal = ({ _case, leaf, onRenamed }) => {
|
|
9
|
+
const { t } = useTranslation();
|
|
10
|
+
const { dispatchApi } = useMyApi();
|
|
11
|
+
const { close } = useContext(ModalContext);
|
|
12
|
+
const currentPath = leaf.path ?? '';
|
|
13
|
+
const lastSlash = currentPath.lastIndexOf('/');
|
|
14
|
+
const folderPrefix = lastSlash >= 0 ? currentPath.slice(0, lastSlash) : '';
|
|
15
|
+
const currentName = lastSlash >= 0 ? currentPath.slice(lastSlash + 1) : currentPath;
|
|
16
|
+
const [name, setName] = useState(currentName);
|
|
17
|
+
const newPath = folderPrefix ? `${folderPrefix}/${name}` : name;
|
|
18
|
+
const existingPaths = useMemo(() => new Set((_case.items ?? []).filter(item => item.value !== leaf.value).map(item => item.path)), [_case.items, leaf.value]);
|
|
19
|
+
const nameError = useMemo(() => {
|
|
20
|
+
if (!name.trim()) {
|
|
21
|
+
return t('modal.cases.rename_item.error.empty');
|
|
22
|
+
}
|
|
23
|
+
if (name.includes('/')) {
|
|
24
|
+
return t('modal.cases.rename_item.error.slash');
|
|
25
|
+
}
|
|
26
|
+
if (existingPaths.has(newPath)) {
|
|
27
|
+
return t('modal.cases.rename_item.error.taken');
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}, [name, newPath, existingPaths, t]);
|
|
31
|
+
const isValid = !nameError;
|
|
32
|
+
const onSubmit = async () => {
|
|
33
|
+
if (!isValid || !_case.case_id || !leaf.value) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const updatedCase = await dispatchApi(api.v2.case.items.patch(_case.case_id, leaf.value, newPath));
|
|
37
|
+
if (updatedCase) {
|
|
38
|
+
onRenamed?.(updatedCase);
|
|
39
|
+
close();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return (_jsxs(Stack, { spacing: 2, p: 2, sx: { minWidth: 'min(600px, 60vw)' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.cases.rename_item') }), folderPrefix && (_jsx(Typography, { variant: "body2", color: "textSecondary", children: t('modal.cases.rename_item.folder_path', { path: folderPrefix }) })), _jsx(TextField, { size: "small", label: t('modal.cases.rename_item.new_name'), value: name, onChange: ev => setName(ev.target.value), error: !!nameError, helperText: nameError ?? ' ', fullWidth: true, autoFocus: true, onKeyDown: ev => {
|
|
43
|
+
if (ev.key === 'Enter' && isValid) {
|
|
44
|
+
onSubmit();
|
|
45
|
+
}
|
|
46
|
+
} }), _jsxs(Stack, { direction: "row", justifyContent: "flex-end", spacing: 1, children: [_jsx(Button, { onClick: close, color: "error", variant: "outlined", children: t('button.cancel') }), _jsx(Button, { onClick: onSubmit, color: "success", variant: "outlined", disabled: !isValid, children: t('button.confirm') })] })] }));
|
|
47
|
+
};
|
|
48
|
+
export default RenameItemModal;
|