@cccsaurora/howler-ui 2.18.0-dev.688 → 2.18.0-dev.695

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.
@@ -2,4 +2,4 @@ import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case'
2
2
  import type { Item } from '@cccsaurora/howler-ui/models/entities/generated/Item';
3
3
  export declare const uri: (id: string) => string;
4
4
  export declare const post: (id: string, newData: Item) => Promise<Case>;
5
- export declare const del: (id: string, value: string) => Promise<void>;
5
+ export declare const del: (id: string, values: string | string[]) => Promise<Case>;
@@ -7,6 +7,9 @@ export const uri = (id) => {
7
7
  export const post = (id, newData) => {
8
8
  return hpost(uri(id), newData);
9
9
  };
10
- export const del = (id, value) => {
11
- return hdelete(uri(id), { value });
10
+ export const del = (id, values) => {
11
+ if (!Array.isArray(values)) {
12
+ values = [values];
13
+ }
14
+ return hdelete(uri(id), { values });
12
15
  };
@@ -9,11 +9,11 @@ import CaseSidebar from './detail/CaseSidebar';
9
9
  import useCase from './hooks/useCase';
10
10
  const CaseViewer = () => {
11
11
  const params = useParams();
12
- const { case: _case, missing } = useCase({ caseId: params.id });
12
+ const { case: _case, missing, update } = useCase({ caseId: params.id });
13
13
  if (missing) {
14
14
  return _jsx(NotFoundPage, {});
15
15
  }
16
- return (_jsxs(Stack, { direction: "row", height: "100%", children: [_jsx(CaseSidebar, { case: _case }), _jsx(Box, { sx: {
16
+ return (_jsxs(Stack, { direction: "row", height: "100%", children: [_jsx(CaseSidebar, { case: _case, update: updatedCase => update(updatedCase, false) }), _jsx(Box, { sx: {
17
17
  maxHeight: 'calc(100vh - 64px)',
18
18
  flex: 1,
19
19
  overflow: 'auto'
@@ -31,7 +31,7 @@ const CaseDashboard = ({ case: providedCase, caseId }) => {
31
31
  const { dispatchApi } = useMyApi();
32
32
  const theme = useTheme();
33
33
  const routeCase = useOutletContext();
34
- const { case: _case, updateCase } = useCase({ case: providedCase ?? routeCase, caseId });
34
+ const { case: _case, update: updateCase } = useCase({ case: providedCase ?? routeCase, caseId });
35
35
  const [records, setRecords] = useState(null);
36
36
  const ids = useMemo(() => (_case?.items ?? [])
37
37
  .filter(item => ['hit', 'observable'].includes(item.type))
@@ -12,7 +12,7 @@ import ResolveModal from '../modals/ResolveModal';
12
12
  import SourceAggregate from './aggregates/SourceAggregate';
13
13
  const CaseDetails = ({ case: providedCase }) => {
14
14
  const { t } = useTranslation();
15
- const { case: _case, updateCase } = useCase({ case: providedCase });
15
+ const { case: _case, update: updateCase } = useCase({ case: providedCase });
16
16
  const { showModal } = useContext(ModalContext);
17
17
  const { config } = useContext(ApiConfigContext);
18
18
  const [loading, setLoading] = useState(false);
@@ -1,6 +1,8 @@
1
1
  import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
2
  import { type FC } from 'react';
3
- declare const CaseSidebar: FC<{
3
+ interface CaseSidebarProps {
4
4
  case: Case;
5
- }>;
5
+ update: (newCase: Case) => void;
6
+ }
7
+ declare const CaseSidebar: FC<CaseSidebarProps>;
6
8
  export default CaseSidebar;
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
  import { Link, useLocation } from 'react-router-dom';
8
8
  import { ESCALATION_COLOR_MAP } from '../constants';
9
9
  import CaseFolder from './sidebar/CaseFolder';
10
- const CaseSidebar = ({ case: _case }) => {
10
+ const CaseSidebar = ({ case: _case, update }) => {
11
11
  const { t } = useTranslation();
12
12
  const location = useLocation();
13
13
  const theme = useTheme();
@@ -56,6 +56,6 @@ const CaseSidebar = ({ case: _case }) => {
56
56
  ], component: Link, to: `/cases/${_case?.case_id}/assets`, children: [_jsx(Dataset, {}), _jsx(Typography, { sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: t('page.cases.assets') })] }), _jsx(Divider, {}), _case && (_jsx(Box, { flex: 1, overflow: "auto", width: "100%", sx: {
57
57
  position: 'relative',
58
58
  borderRight: `thin solid ${theme.palette.divider}`
59
- }, children: _jsx(Box, { position: "absolute", sx: { left: 0, right: 0 }, children: _jsx(CaseFolder, { case: _case }) }) }))] }));
59
+ }, children: _jsx(Box, { position: "absolute", sx: { left: 0, right: 0 }, children: _jsx(CaseFolder, { case: _case, onItemRemoved: update }) }) }))] }));
60
60
  };
61
61
  export default CaseSidebar;
@@ -8,6 +8,7 @@ interface CaseFolderProps {
8
8
  step?: number;
9
9
  rootCaseId?: string;
10
10
  pathPrefix?: string;
11
+ onItemRemoved?: (newCase: Case) => void;
11
12
  }
12
13
  declare const CaseFolder: FC<CaseFolderProps>;
13
14
  export default CaseFolder;
@@ -7,6 +7,7 @@ import { omit } from 'lodash-es';
7
7
  import { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { Link, useLocation } from 'react-router-dom';
9
9
  import { ESCALATION_COLORS } from '@cccsaurora/howler-ui/utils/constants';
10
+ import CaseFolderContextMenu from './CaseFolderContextMenu';
10
11
  import { buildTree } from './utils';
11
12
  // Static map: item type → MUI icon component (avoids re-creating closures on each render)
12
13
  const ICON_FOR_TYPE = {
@@ -17,7 +18,7 @@ const ICON_FOR_TYPE = {
17
18
  lead: Lightbulb,
18
19
  reference: LinkIcon
19
20
  };
20
- const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '' }) => {
21
+ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '', onItemRemoved }) => {
21
22
  const theme = useTheme();
22
23
  const location = useLocation();
23
24
  const { dispatchApi } = useMyApi();
@@ -75,17 +76,17 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
75
76
  setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], loading: false } }));
76
77
  });
77
78
  }, [caseStates, dispatchApi]);
78
- return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsxs(Stack, { direction: "row", pl: step * 1.5, py: 0.25, sx: {
79
- cursor: 'pointer',
80
- transition: theme.transitions.create('background', { duration: 50 }),
81
- background: 'transparent',
82
- '&:hover': {
83
- background: theme.palette.grey[800]
84
- }
85
- }, onClick: () => setOpen(_open => !_open), children: [_jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
86
- { transition: theme.transitions.create('transform', { duration: 100 }), transform: 'rotate(0deg)' },
87
- open && { transform: 'rotate(90deg)' }
88
- ] }), _jsx(FolderIcon, { fontSize: "small", color: "disabled" }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: name })] })), open && (_jsxs(_Fragment, { children: [Object.entries(omit(tree, 'leaves')).map(([path, subfolder]) => (_jsx(CaseFolder, { name: path, case: _case, folder: subfolder, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: pathPrefix }, `${_case?.case_id}-${path}`))), tree.leaves?.map(leaf => {
79
+ return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsx(CaseFolderContextMenu, { _case: _case, tree: tree, onRemoved: onItemRemoved, children: _jsxs(Stack, { direction: "row", pl: step * 1.5, py: 0.25, sx: {
80
+ cursor: 'pointer',
81
+ transition: theme.transitions.create('background', { duration: 50 }),
82
+ background: 'transparent',
83
+ '&:hover': {
84
+ background: theme.palette.grey[800]
85
+ }
86
+ }, onClick: () => setOpen(_open => !_open), children: [_jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
87
+ { transition: theme.transitions.create('transform', { duration: 100 }), transform: 'rotate(0deg)' },
88
+ open && { transform: 'rotate(90deg)' }
89
+ ] }), _jsx(FolderIcon, { fontSize: "small", color: "disabled" }), _jsx(Typography, { variant: "caption", color: "textSecondary", sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: name })] }) })), open && (_jsxs(_Fragment, { children: [Object.entries(omit(tree, 'leaves')).map(([path, subfolder]) => (_jsx(CaseFolder, { name: path, case: _case, folder: subfolder, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: pathPrefix, onItemRemoved: onItemRemoved }, `${_case?.case_id}-${path}`))), tree.leaves?.map(leaf => {
89
90
  const itemType = leaf.type?.toLowerCase();
90
91
  const isCase = itemType === 'case';
91
92
  const fullRelativePath = [pathPrefix, leaf.path].filter(Boolean).join('/');
@@ -103,30 +104,30 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
103
104
  const iconColor = escalationColor ?? 'inherit';
104
105
  const leafColor = escalationColor ? `${escalationColor}.light` : 'text.secondary';
105
106
  const Icon = ICON_FOR_TYPE[itemType ?? ''] ?? Article;
106
- return (_jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", pl: step * 1.5 + 1, py: 0.25, sx: [
107
- {
108
- cursor: 'pointer',
109
- overflow: 'visible',
110
- color: `${theme.palette.text.secondary} !important`,
111
- textDecoration: 'none',
112
- transition: theme.transitions.create('background', { duration: 100 }),
113
- background: 'transparent',
114
- '&:hover': {
115
- background: theme.palette.grey[800]
107
+ return (_jsx(CaseFolderContextMenu, { _case: _case, leaf: leaf, onRemoved: onItemRemoved, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", pl: step * 1.5 + 1, py: 0.25, sx: [
108
+ {
109
+ cursor: 'pointer',
110
+ overflow: 'visible',
111
+ color: `${theme.palette.text.secondary} !important`,
112
+ textDecoration: 'none',
113
+ transition: theme.transitions.create('background', { duration: 100 }),
114
+ background: 'transparent',
115
+ '&:hover': {
116
+ background: theme.palette.grey[800]
117
+ },
118
+ borderRight: '3px solid transparent'
116
119
  },
117
- borderRight: '3px solid transparent'
118
- },
119
- decodeURIComponent(location.pathname) === itemPath && {
120
- background: alpha(theme.palette.grey[600], 0.15),
121
- borderRightColor: theme.palette.primary.main
122
- }
123
- ], onClick: () => isCase && toggleCase(leaf, itemKey), component: Link, to: itemPath, target: itemType === 'reference' ? '_blank' : undefined, rel: itemType === 'reference' ? 'noopener noreferrer' : undefined, children: [_jsx(ChevronRight, { fontSize: "small", sx: [
124
- !isCase && { opacity: 0 },
125
- isCase && {
126
- transition: theme.transitions.create('transform', { duration: 100 }),
127
- transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
128
- }
129
- ] }), _jsx(Icon, { fontSize: "small", color: iconColor }), _jsx(Typography, { variant: "caption", color: leafColor, sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: leaf.path?.split('/').pop() || leaf.value })] }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: fullRelativePath }))] }, `${_case?.case_id}-${leaf.value}-${leaf.path}`));
120
+ decodeURIComponent(location.pathname) === itemPath && {
121
+ background: alpha(theme.palette.grey[600], 0.15),
122
+ borderRightColor: theme.palette.primary.main
123
+ }
124
+ ], onClick: () => isCase && toggleCase(leaf, itemKey), component: Link, to: itemPath, target: itemType === 'reference' ? '_blank' : undefined, rel: itemType === 'reference' ? 'noopener noreferrer' : undefined, children: [_jsx(ChevronRight, { fontSize: "small", sx: [
125
+ !isCase && { opacity: 0 },
126
+ isCase && {
127
+ transition: theme.transitions.create('transform', { duration: 100 }),
128
+ transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
129
+ }
130
+ ] }), _jsx(Icon, { fontSize: "small", color: iconColor }), _jsx(Typography, { variant: "caption", color: leafColor, sx: { userSelect: 'none', pl: 0.5, textWrap: 'nowrap' }, children: leaf.path?.split('/').pop() || leaf.value })] }), isCase && isCaseOpen && isCaseLoading && (_jsx(Stack, { pl: step * 1.5 + 4, py: 0.25, children: _jsx(Skeleton, { width: 140, height: 16 }) })), isCase && isCaseOpen && nestedCase && (_jsx(CaseFolder, { case: nestedCase, step: step + 1, rootCaseId: currentRootCaseId, pathPrefix: fullRelativePath, onItemRemoved: onItemRemoved }))] }) }, `${_case?.case_id}-${leaf.value}-${leaf.path}`));
130
131
  })] }))] }));
131
132
  };
132
133
  export default CaseFolder;
@@ -0,0 +1,34 @@
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, type PropsWithChildren } from 'react';
4
+ import type { Tree } from './types';
5
+ /**
6
+ * Recursively collects all leaf items from a folder tree.
7
+ */
8
+ export declare const collectAllLeaves: (tree: Tree) => Item[];
9
+ /**
10
+ * Returns the URL to open for a given leaf item, or null if no URL applies.
11
+ * - reference: the item's value (an external URL)
12
+ * - hit: /hits/<id>
13
+ * - observable: /observables/<id>
14
+ * - case: /cases/<id>
15
+ * - table / lead: null (no dedicated detail page)
16
+ */
17
+ export declare const getOpenUrl: (leaf: Item) => string | null;
18
+ export interface CaseFolderContextMenuProps extends PropsWithChildren {
19
+ /** The case that owns the item(s). */
20
+ _case: Case;
21
+ /** Present when the context menu is for a single leaf item. */
22
+ leaf?: Item;
23
+ /** Present when the context menu is for a folder (all leaves within it will be removed). */
24
+ tree?: Tree;
25
+ /** Called after item(s) have been successfully removed. */
26
+ onRemoved?: (updatedCase: Case) => void;
27
+ }
28
+ /**
29
+ * Wraps its children with a right-click context menu providing:
30
+ * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
31
+ * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
32
+ */
33
+ declare const CaseFolderContextMenu: FC<CaseFolderContextMenuProps>;
34
+ export default CaseFolderContextMenu;
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Delete, OpenInNew } from '@mui/icons-material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
5
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
+ import { useMemo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ /**
9
+ * Recursively collects all leaf items from a folder tree.
10
+ */
11
+ export const collectAllLeaves = (tree) => {
12
+ const result = [...(tree.leaves ?? [])];
13
+ for (const key of Object.keys(tree)) {
14
+ if (key !== 'leaves') {
15
+ result.push(...collectAllLeaves(tree[key]));
16
+ }
17
+ }
18
+ return result;
19
+ };
20
+ /**
21
+ * Returns the URL to open for a given leaf item, or null if no URL applies.
22
+ * - reference: the item's value (an external URL)
23
+ * - hit: /hits/<id>
24
+ * - observable: /observables/<id>
25
+ * - case: /cases/<id>
26
+ * - table / lead: null (no dedicated detail page)
27
+ */
28
+ export const getOpenUrl = (leaf) => {
29
+ const type = leaf.type?.toLowerCase();
30
+ if (type === 'reference') {
31
+ return leaf.value ?? null;
32
+ }
33
+ if (type === 'hit') {
34
+ return leaf.value ? `/hits/${leaf.value}` : null;
35
+ }
36
+ if (type === 'observable') {
37
+ return leaf.value ? `/observables/${leaf.value}` : null;
38
+ }
39
+ if (type === 'case') {
40
+ return leaf.value ? `/cases/${leaf.value}` : null;
41
+ }
42
+ return null;
43
+ };
44
+ /**
45
+ * Wraps its children with a right-click context menu providing:
46
+ * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
47
+ * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
48
+ */
49
+ const CaseFolderContextMenu = ({ _case, leaf, tree, onRemoved, children }) => {
50
+ const { dispatchApi } = useMyApi();
51
+ const { t } = useTranslation();
52
+ const items = useMemo(() => {
53
+ const entries = [];
54
+ if (leaf) {
55
+ const openUrl = getOpenUrl(leaf);
56
+ if (openUrl) {
57
+ entries.push({
58
+ kind: 'item',
59
+ id: 'open-item',
60
+ label: t('page.cases.sidebar.item.open'),
61
+ icon: _jsx(OpenInNew, { fontSize: "small" }),
62
+ onClick: () => window.open(openUrl, '_blank', 'noopener noreferrer')
63
+ });
64
+ }
65
+ }
66
+ if (entries.length > 0) {
67
+ entries.push({ kind: 'divider', id: 'divider-remove' });
68
+ }
69
+ const isFolder = !leaf && !!tree;
70
+ entries.push({
71
+ kind: 'item',
72
+ id: 'remove-item',
73
+ label: isFolder ? t('page.cases.sidebar.folder.remove') : t('page.cases.sidebar.item.remove'),
74
+ icon: _jsx(Delete, { fontSize: "small" }),
75
+ onClick: () => {
76
+ if (!_case.case_id) {
77
+ return;
78
+ }
79
+ const itemsToDelete = leaf ? [leaf] : tree ? collectAllLeaves(tree) : [];
80
+ const values = itemsToDelete.filter(i => !!i.value).map(i => i.value);
81
+ if (!values.length) {
82
+ return;
83
+ }
84
+ dispatchApi(api.v2.case.items.del(_case.case_id, values), { throwError: false }).then(updatedCase => {
85
+ if (updatedCase) {
86
+ onRemoved?.(updatedCase);
87
+ }
88
+ });
89
+ }
90
+ });
91
+ return entries;
92
+ }, [_case, leaf, tree, dispatchApi, onRemoved, t]);
93
+ return _jsx(ContextMenu, { items: items, children: children });
94
+ };
95
+ export default CaseFolderContextMenu;
@@ -0,0 +1,296 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import { act } from 'react';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks
7
+ // ---------------------------------------------------------------------------
8
+ vi.mock('components/elements/ContextMenu', () => ({
9
+ default: ({ items, children }) => (_jsxs("div", { children: [children, items.map((item) => {
10
+ if (item.kind === 'item') {
11
+ return (_jsx("button", { id: item.id, onClick: item.onClick, children: item.label }, item.id));
12
+ }
13
+ if (item.kind === 'divider') {
14
+ return _jsx("hr", {}, item.id);
15
+ }
16
+ return null;
17
+ })] }))
18
+ }));
19
+ const mockDispatchApi = vi.hoisted(() => vi.fn());
20
+ vi.mock('components/hooks/useMyApi', () => ({
21
+ default: () => ({ dispatchApi: mockDispatchApi })
22
+ }));
23
+ const mockDel = vi.hoisted(() => vi.fn());
24
+ vi.mock('api', () => ({
25
+ default: {
26
+ v2: {
27
+ case: {
28
+ items: {
29
+ del: (...args) => mockDel(...args)
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }));
35
+ // ---------------------------------------------------------------------------
36
+ // Imports (after mocks so that module registry picks up the stubs)
37
+ // ---------------------------------------------------------------------------
38
+ import CaseFolderContextMenu, { collectAllLeaves, getOpenUrl } from './CaseFolderContextMenu';
39
+ // ---------------------------------------------------------------------------
40
+ // Fixtures
41
+ // ---------------------------------------------------------------------------
42
+ const mockCase = { case_id: 'case-1', title: 'Test Case', items: [] };
43
+ const hitLeaf = { type: 'hit', value: 'hit-123', path: 'folder/hit-item' };
44
+ const referenceLeaf = { type: 'reference', value: 'https://example.com', path: 'folder/ref-item' };
45
+ const observableLeaf = { type: 'observable', value: 'obs-456', path: 'folder/obs-item' };
46
+ const caseLeaf = { type: 'case', value: 'nested-case-id', path: 'folder/case-item' };
47
+ const tableLeaf = { type: 'table', value: 'table-789', path: 'folder/table-item' };
48
+ const leadLeaf = { type: 'lead', value: 'lead-999', path: 'folder/lead-item' };
49
+ const renderMenu = (props) => render(_jsx(CaseFolderContextMenu, { _case: mockCase, ...props, children: _jsx("div", { id: "child", children: "child" }) }));
50
+ // ---------------------------------------------------------------------------
51
+ // Setup
52
+ // ---------------------------------------------------------------------------
53
+ beforeEach(() => {
54
+ mockDel.mockClear();
55
+ mockDispatchApi.mockClear();
56
+ mockDispatchApi.mockImplementation((p) => p);
57
+ mockDel.mockResolvedValue(mockCase);
58
+ vi.spyOn(window, 'open').mockReturnValue(null);
59
+ });
60
+ // ---------------------------------------------------------------------------
61
+ // Unit tests for exported utilities
62
+ // ---------------------------------------------------------------------------
63
+ describe('collectAllLeaves', () => {
64
+ it('returns leaves at the root level', () => {
65
+ const tree = { leaves: [hitLeaf, referenceLeaf] };
66
+ expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
67
+ });
68
+ it('returns leaves from nested subfolders', () => {
69
+ const tree = {
70
+ leaves: [hitLeaf],
71
+ subfolder: { leaves: [referenceLeaf] }
72
+ };
73
+ expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
74
+ });
75
+ it('returns leaves from deeply nested subfolders', () => {
76
+ const tree = {
77
+ leaves: [],
78
+ level1: {
79
+ leaves: [hitLeaf],
80
+ level2: { leaves: [referenceLeaf] }
81
+ }
82
+ };
83
+ const result = collectAllLeaves(tree);
84
+ expect(result).toContain(hitLeaf);
85
+ expect(result).toContain(referenceLeaf);
86
+ });
87
+ it('returns an empty array for an empty tree', () => {
88
+ expect(collectAllLeaves({ leaves: [] })).toEqual([]);
89
+ });
90
+ });
91
+ describe('getOpenUrl', () => {
92
+ it('returns the value directly for a reference item', () => {
93
+ expect(getOpenUrl(referenceLeaf)).toBe('https://example.com');
94
+ });
95
+ it('returns /hits/<id> for a hit item', () => {
96
+ expect(getOpenUrl(hitLeaf)).toBe('/hits/hit-123');
97
+ });
98
+ it('returns /observables/<id> for an observable item', () => {
99
+ expect(getOpenUrl(observableLeaf)).toBe('/observables/obs-456');
100
+ });
101
+ it('returns /cases/<id> for a case item', () => {
102
+ expect(getOpenUrl(caseLeaf)).toBe('/cases/nested-case-id');
103
+ });
104
+ it('returns null for a table item', () => {
105
+ expect(getOpenUrl(tableLeaf)).toBeNull();
106
+ });
107
+ it('returns null for a lead item', () => {
108
+ expect(getOpenUrl(leadLeaf)).toBeNull();
109
+ });
110
+ it('returns null when value is undefined', () => {
111
+ expect(getOpenUrl({ type: 'hit' })).toBeNull();
112
+ });
113
+ it('returns null when type is undefined', () => {
114
+ expect(getOpenUrl({ value: 'something' })).toBeNull();
115
+ });
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // Component tests
119
+ // ---------------------------------------------------------------------------
120
+ describe('CaseFolderContextMenu', () => {
121
+ describe('renders children', () => {
122
+ it('renders children content', () => {
123
+ renderMenu({ leaf: hitLeaf });
124
+ expect(screen.getByTestId('child')).toBeInTheDocument();
125
+ });
126
+ });
127
+ describe('menu items for leaf types', () => {
128
+ it('shows "Open item" and "Remove item" for a hit leaf', () => {
129
+ renderMenu({ leaf: hitLeaf });
130
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
131
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
132
+ });
133
+ it('shows "Open item" and "Remove item" for a reference leaf', () => {
134
+ renderMenu({ leaf: referenceLeaf });
135
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
136
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
137
+ });
138
+ it('shows "Open item" and "Remove item" for an observable leaf', () => {
139
+ renderMenu({ leaf: observableLeaf });
140
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
141
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
142
+ });
143
+ it('shows "Open item" and "Remove item" for a case leaf', () => {
144
+ renderMenu({ leaf: caseLeaf });
145
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
146
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
147
+ });
148
+ it('shows only "Remove item" for a table leaf (no open URL)', () => {
149
+ renderMenu({ leaf: tableLeaf });
150
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
151
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
152
+ });
153
+ it('shows only "Remove item" for a lead leaf (no open URL)', () => {
154
+ renderMenu({ leaf: leadLeaf });
155
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
156
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
157
+ });
158
+ it('labels the remove button "Remove item" for a leaf', () => {
159
+ renderMenu({ leaf: hitLeaf });
160
+ expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.item.remove');
161
+ });
162
+ it('shows a divider only when "Open item" is also present', () => {
163
+ const { container: withOpen } = renderMenu({ leaf: hitLeaf });
164
+ expect(withOpen.querySelector('hr')).not.toBeNull();
165
+ const { container: withoutOpen } = renderMenu({ leaf: tableLeaf });
166
+ expect(withoutOpen.querySelector('hr')).toBeNull();
167
+ });
168
+ });
169
+ describe('menu items for folders', () => {
170
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
171
+ it('shows only "Remove folder" for a folder (no open URL)', () => {
172
+ renderMenu({ tree: folderTree });
173
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
174
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
175
+ });
176
+ it('labels the remove button "Remove folder" for a tree', () => {
177
+ renderMenu({ tree: folderTree });
178
+ expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.folder.remove');
179
+ });
180
+ });
181
+ describe('"Open item" action', () => {
182
+ it('calls window.open with the hit URL', () => {
183
+ renderMenu({ leaf: hitLeaf });
184
+ act(() => {
185
+ fireEvent.click(screen.getByTestId('open-item'));
186
+ });
187
+ expect(window.open).toHaveBeenCalledWith('/hits/hit-123', '_blank', 'noopener noreferrer');
188
+ });
189
+ it('calls window.open with the reference URL directly', () => {
190
+ renderMenu({ leaf: referenceLeaf });
191
+ act(() => {
192
+ fireEvent.click(screen.getByTestId('open-item'));
193
+ });
194
+ expect(window.open).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener noreferrer');
195
+ });
196
+ it('calls window.open with the observable URL', () => {
197
+ renderMenu({ leaf: observableLeaf });
198
+ act(() => {
199
+ fireEvent.click(screen.getByTestId('open-item'));
200
+ });
201
+ expect(window.open).toHaveBeenCalledWith('/observables/obs-456', '_blank', 'noopener noreferrer');
202
+ });
203
+ it('calls window.open with the case URL', () => {
204
+ renderMenu({ leaf: caseLeaf });
205
+ act(() => {
206
+ fireEvent.click(screen.getByTestId('open-item'));
207
+ });
208
+ expect(window.open).toHaveBeenCalledWith('/cases/nested-case-id', '_blank', 'noopener noreferrer');
209
+ });
210
+ });
211
+ describe('"Remove item" action for a leaf', () => {
212
+ it('calls dispatchApi with the delete call for the leaf', async () => {
213
+ renderMenu({ leaf: hitLeaf });
214
+ act(() => {
215
+ fireEvent.click(screen.getByTestId('remove-item'));
216
+ });
217
+ await waitFor(() => {
218
+ expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123']);
219
+ });
220
+ });
221
+ it('calls onRemoved with the updated case after the delete resolves', async () => {
222
+ const onRemoved = vi.fn();
223
+ renderMenu({ leaf: hitLeaf, onRemoved });
224
+ act(() => {
225
+ fireEvent.click(screen.getByTestId('remove-item'));
226
+ });
227
+ await waitFor(() => {
228
+ expect(onRemoved).toHaveBeenCalledWith(mockCase);
229
+ });
230
+ });
231
+ it('does not call the API when case_id is missing', () => {
232
+ renderMenu({ _case: { title: 'No ID' }, leaf: hitLeaf });
233
+ act(() => {
234
+ fireEvent.click(screen.getByTestId('remove-item'));
235
+ });
236
+ expect(mockDel).not.toHaveBeenCalled();
237
+ });
238
+ it('skips items with no value', async () => {
239
+ const noValueLeaf = { type: 'hit', path: 'folder/no-value' };
240
+ renderMenu({ leaf: noValueLeaf });
241
+ act(() => {
242
+ fireEvent.click(screen.getByTestId('remove-item'));
243
+ });
244
+ await waitFor(() => {
245
+ expect(mockDel).not.toHaveBeenCalled();
246
+ });
247
+ });
248
+ });
249
+ describe('"Remove folder" action', () => {
250
+ it('calls dispatchApi with all leaf values in a single batch call', async () => {
251
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
252
+ renderMenu({ tree: folderTree });
253
+ act(() => {
254
+ fireEvent.click(screen.getByTestId('remove-item'));
255
+ });
256
+ await waitFor(() => {
257
+ expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123', 'https://example.com']);
258
+ expect(mockDel).toHaveBeenCalledTimes(1);
259
+ });
260
+ });
261
+ it('calls dispatchApi with leaves from nested subfolders in a single batch call', async () => {
262
+ const nestedTree = {
263
+ leaves: [hitLeaf],
264
+ subfolder: { leaves: [referenceLeaf] }
265
+ };
266
+ renderMenu({ tree: nestedTree });
267
+ act(() => {
268
+ fireEvent.click(screen.getByTestId('remove-item'));
269
+ });
270
+ await waitFor(() => {
271
+ expect(mockDel).toHaveBeenCalledWith('case-1', expect.arrayContaining(['hit-123', 'https://example.com']));
272
+ expect(mockDel).toHaveBeenCalledTimes(1);
273
+ });
274
+ });
275
+ it('calls onRemoved with the updated case after deletion', async () => {
276
+ const onRemoved = vi.fn();
277
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
278
+ renderMenu({ tree: folderTree, onRemoved });
279
+ act(() => {
280
+ fireEvent.click(screen.getByTestId('remove-item'));
281
+ });
282
+ await waitFor(() => {
283
+ expect(onRemoved).toHaveBeenCalledWith(mockCase);
284
+ });
285
+ });
286
+ it('does not call the API or onRemoved for an empty folder', () => {
287
+ const onRemoved = vi.fn();
288
+ renderMenu({ tree: { leaves: [] }, onRemoved });
289
+ act(() => {
290
+ fireEvent.click(screen.getByTestId('remove-item'));
291
+ });
292
+ expect(mockDel).not.toHaveBeenCalled();
293
+ expect(onRemoved).not.toHaveBeenCalled();
294
+ });
295
+ });
296
+ });
@@ -5,7 +5,7 @@ interface CaseArguments {
5
5
  }
6
6
  interface CaseResult {
7
7
  case: Case;
8
- updateCase: (update: Partial<Case>) => Promise<void>;
8
+ update: (update: Partial<Case>, publish?: boolean) => Promise<void>;
9
9
  loading: boolean;
10
10
  missing: boolean;
11
11
  }
@@ -19,12 +19,25 @@ const useCase = ({ caseId, case: providedCase }) => {
19
19
  .finally(() => setLoading(false));
20
20
  }
21
21
  }, [caseId, dispatchApi]);
22
- const updateCase = useCallback(async (_updatedCase) => {
22
+ const update = useCallback(async (_updatedCase, publish = true) => {
23
23
  if (!_case?.case_id) {
24
24
  return;
25
25
  }
26
26
  try {
27
- setCase(await dispatchApi(api.v2.case.put(_case.case_id, _updatedCase)));
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
+ }
28
41
  }
29
42
  catch (e) {
30
43
  setMissing(true);
@@ -33,6 +46,6 @@ const useCase = ({ caseId, case: providedCase }) => {
33
46
  return;
34
47
  }
35
48
  }, [_case?.case_id, dispatchApi]);
36
- return { case: _case, updateCase, loading, missing };
49
+ return { case: _case, update, loading, missing };
37
50
  };
38
51
  export default useCase;
@@ -19,7 +19,7 @@ const ResolveModal = ({ case: _case, onConfirm }) => {
19
19
  const { dispatchApi } = useMyApi();
20
20
  const { close } = useContext(ModalContext);
21
21
  const { config } = useContext(ApiConfigContext);
22
- const { updateCase } = useCase({ case: _case });
22
+ const { update: updateCase } = useCase({ case: _case });
23
23
  const [loading, setLoading] = useState(true);
24
24
  const [rationale, setRationale] = useState('');
25
25
  const [assessment, setAssessment] = useState(null);
@@ -310,13 +310,13 @@
310
310
  "modal.action.empty": "Action Name cannot be empty.",
311
311
  "modal.action.label": "Action Name",
312
312
  "modal.action.title": "Save Action",
313
- "modal.cases.resolve": "Resolve Case",
314
- "modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
315
313
  "modal.cases.add_to_case": "Add to Case",
314
+ "modal.cases.add_to_case.full_path": "Full path: {{path}}",
316
315
  "modal.cases.add_to_case.select_case": "Search Cases",
317
316
  "modal.cases.add_to_case.select_path": "Select Folder Path",
318
317
  "modal.cases.add_to_case.title": "Item Title",
319
- "modal.cases.add_to_case.full_path": "Full path: {{path}}",
318
+ "modal.cases.resolve": "Resolve Case",
319
+ "modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
320
320
  "modal.confirm.delete.description": "Are you sure you want to delete this item?",
321
321
  "modal.confirm.delete.title": "Confirm Deletion",
322
322
  "modal.rationale.description": "Provide a rationale that succinctly explains to other analysts why you are confident in this assessment.",
@@ -384,6 +384,9 @@
384
384
  "page.cases.detail.properties": "Properties",
385
385
  "page.cases.detail.status": "Status",
386
386
  "page.cases.escalation": "Escalation",
387
+ "page.cases.sidebar.folder.remove": "Remove folder",
388
+ "page.cases.sidebar.item.open": "Open item",
389
+ "page.cases.sidebar.item.remove": "Remove item",
387
390
  "page.cases.sources": "Sources",
388
391
  "page.cases.updated": "Updated",
389
392
  "page.dashboard.settings.edit": "Edit Dashboard",
@@ -310,13 +310,13 @@
310
310
  "modal.action.empty": "Le nom de l'action ne peut pas être vide.",
311
311
  "modal.action.label": "Nom de l'action",
312
312
  "modal.action.title": "Enregistrer l'action",
313
- "modal.cases.resolve": "Résoudre le cas",
314
- "modal.cases.resolve.description": "Lors de la résolution d'un cas, vous devez soit évaluer toutes les alertes ouvertes, soit ajouter une évaluation aux alertes.",
315
313
  "modal.cases.add_to_case": "Ajouter au cas",
314
+ "modal.cases.add_to_case.full_path": "Chemin complet : {{path}}",
316
315
  "modal.cases.add_to_case.select_case": "Rechercher des cas",
317
316
  "modal.cases.add_to_case.select_path": "Sélectionner le chemin du dossier",
318
317
  "modal.cases.add_to_case.title": "Titre de l'élément",
319
- "modal.cases.add_to_case.full_path": "Chemin complet : {{path}}",
318
+ "modal.cases.resolve": "Résoudre le cas",
319
+ "modal.cases.resolve.description": "Lors de la résolution d'un cas, vous devez soit évaluer toutes les alertes ouvertes, soit ajouter une évaluation aux alertes.",
320
320
  "modal.confirm.delete.description": "Êtes-vous sûr de vouloir supprimer cet élément ?",
321
321
  "modal.confirm.delete.title": "Confirmer la suppression",
322
322
  "modal.rationale.description": "Fournissez une justification qui explique succinctement aux autres analystes les raisons pour lesquelles vous êtes confiant dans cette évaluation.",
@@ -384,6 +384,9 @@
384
384
  "page.cases.detail.properties": "Propriétés",
385
385
  "page.cases.detail.status": "Statut",
386
386
  "page.cases.escalation": "Escalade",
387
+ "page.cases.sidebar.folder.remove": "Supprimer le dossier",
388
+ "page.cases.sidebar.item.open": "Ouvrir l'élément",
389
+ "page.cases.sidebar.item.remove": "Supprimer l'élément",
387
390
  "page.cases.sources": "Sources",
388
391
  "page.cases.updated": "Mis à jour",
389
392
  "page.dashboard.settings.edit": "Modifier le tableau de bord",
package/package.json CHANGED
@@ -101,183 +101,183 @@
101
101
  "internal-slot": "1.0.7"
102
102
  },
103
103
  "type": "module",
104
- "version": "2.18.0-dev.688",
104
+ "version": "2.18.0-dev.695",
105
105
  "exports": {
106
106
  "./i18n": "./i18n.js",
107
107
  "./index.css": "./index.css",
108
108
  "./components/*": "./components/*.js",
109
- "./rest/*": "./rest/*.js",
110
- "./rest": "./rest/index.js",
111
- "./plugins/*": "./plugins/*.js",
112
- "./models/*": "./models/*.js",
113
- "./utils/*": "./utils/*.js",
114
- "./utils/*.json": "./utils/*.json",
115
109
  "./branding/*": "./branding/*.js",
116
110
  "./tests/*": "./tests/*.js",
117
- "./locales/*.json": "./locales/*.json",
118
111
  "./commons/*": "./commons/*.js",
112
+ "./utils/*": "./utils/*.js",
113
+ "./utils/*.json": "./utils/*.json",
114
+ "./locales/*.json": "./locales/*.json",
119
115
  "./api/*": "./api/*.js",
120
116
  "./api": "./api/index.js",
121
- "./components/routes/*": "./components/routes/*.js",
117
+ "./plugins/*": "./plugins/*.js",
118
+ "./models/*": "./models/*.js",
119
+ "./rest/*": "./rest/*.js",
120
+ "./rest": "./rest/index.js",
121
+ "./components/logins/*": "./components/logins/*.js",
122
122
  "./components/app/*": "./components/app/*.js",
123
- "./components/hooks/*": "./components/hooks/*.js",
124
123
  "./components/elements/*": "./components/elements/*.js",
125
- "./components/logins/*": "./components/logins/*.js",
126
- "./components/routes/admin/*": "./components/routes/admin/*.js",
127
- "./components/routes/hits/*": "./components/routes/hits/*.js",
124
+ "./components/hooks/*": "./components/hooks/*.js",
125
+ "./components/routes/*": "./components/routes/*.js",
126
+ "./components/logins/auth/*": "./components/logins/auth/*.js",
127
+ "./components/logins/hooks/*": "./components/logins/hooks/*.js",
128
+ "./components/app/drawers/*": "./components/app/drawers/*.js",
129
+ "./components/app/providers/*": "./components/app/providers/*.js",
130
+ "./components/app/hooks/*": "./components/app/hooks/*.js",
131
+ "./components/elements/display/*": "./components/elements/display/*.js",
132
+ "./components/elements/observable/*": "./components/elements/observable/*.js",
133
+ "./components/elements/hit/*": "./components/elements/hit/*.js",
134
+ "./components/elements/record/*": "./components/elements/record/*.js",
135
+ "./components/elements/view/*": "./components/elements/view/*.js",
136
+ "./components/elements/case/*": "./components/elements/case/*.js",
137
+ "./components/elements/addons/*": "./components/elements/addons/*.js",
138
+ "./components/elements/display/handlebars/*": "./components/elements/display/handlebars/*.js",
139
+ "./components/elements/display/modals/*": "./components/elements/display/modals/*.js",
140
+ "./components/elements/display/features/*": "./components/elements/display/features/*.js",
141
+ "./components/elements/display/icons/*": "./components/elements/display/icons/*.js",
142
+ "./components/elements/display/json/*": "./components/elements/display/json/*.js",
143
+ "./components/elements/display/markdownPlugins/*.md": "./components/elements/display/markdownPlugins/*.md.js",
144
+ "./components/elements/display/icons/svg/*": "./components/elements/display/icons/svg/*.js",
145
+ "./components/elements/hit/actions/*": "./components/elements/hit/actions/*.js",
146
+ "./components/elements/hit/related/*": "./components/elements/hit/related/*.js",
147
+ "./components/elements/hit/elements/*": "./components/elements/hit/elements/*.js",
148
+ "./components/elements/hit/outlines/*": "./components/elements/hit/outlines/*.js",
149
+ "./components/elements/hit/aggregate/*": "./components/elements/hit/aggregate/*.js",
150
+ "./components/elements/hit/outlines/al/*": "./components/elements/hit/outlines/al/*.js",
151
+ "./components/elements/addons/buttons/*": "./components/elements/addons/buttons/*.js",
152
+ "./components/elements/addons/buttons": "./components/elements/addons/buttons/index.js",
153
+ "./components/elements/addons/lists/*": "./components/elements/addons/lists/*.js",
154
+ "./components/elements/addons/lists": "./components/elements/addons/lists/index.js",
155
+ "./components/elements/addons/search/*": "./components/elements/addons/search/*.js",
156
+ "./components/elements/addons/layout/*": "./components/elements/addons/layout/*.js",
157
+ "./components/elements/addons/lists/table/*": "./components/elements/addons/lists/table/*.js",
158
+ "./components/elements/addons/lists/table": "./components/elements/addons/lists/table/index.js",
159
+ "./components/elements/addons/lists/hooks/*": "./components/elements/addons/lists/hooks/*.js",
160
+ "./components/elements/addons/search/phrase/*": "./components/elements/addons/search/phrase/*.js",
161
+ "./components/elements/addons/search/phrase": "./components/elements/addons/search/phrase/index.js",
162
+ "./components/elements/addons/search/phrase/word/*": "./components/elements/addons/search/phrase/word/*.js",
163
+ "./components/elements/addons/search/phrase/word/consumers/*": "./components/elements/addons/search/phrase/word/consumers/*.js",
164
+ "./components/elements/addons/layout/vsbox/*": "./components/elements/addons/layout/vsbox/*.js",
165
+ "./components/routes/home/*": "./components/routes/home/*.js",
166
+ "./components/routes/home": "./components/routes/home/index.js",
128
167
  "./components/routes/action/*": "./components/routes/action/*.js",
129
- "./components/routes/dossiers/*": "./components/routes/dossiers/*.js",
130
- "./components/routes/settings/*": "./components/routes/settings/*.js",
131
- "./components/routes/advanced/*": "./components/routes/advanced/*.js",
132
168
  "./components/routes/templates/*": "./components/routes/templates/*.js",
169
+ "./components/routes/dossiers/*": "./components/routes/dossiers/*.js",
170
+ "./components/routes/overviews/*": "./components/routes/overviews/*.js",
171
+ "./components/routes/views/*": "./components/routes/views/*.js",
172
+ "./components/routes/hits/*": "./components/routes/hits/*.js",
133
173
  "./components/routes/analytics/*": "./components/routes/analytics/*.js",
134
- "./components/routes/observables/*": "./components/routes/observables/*.js",
174
+ "./components/routes/advanced/*": "./components/routes/advanced/*.js",
135
175
  "./components/routes/help/*": "./components/routes/help/*.js",
176
+ "./components/routes/admin/*": "./components/routes/admin/*.js",
177
+ "./components/routes/settings/*": "./components/routes/settings/*.js",
178
+ "./components/routes/observables/*": "./components/routes/observables/*.js",
136
179
  "./components/routes/cases/*": "./components/routes/cases/*.js",
137
- "./components/routes/overviews/*": "./components/routes/overviews/*.js",
138
- "./components/routes/home/*": "./components/routes/home/*.js",
139
- "./components/routes/home": "./components/routes/home/index.js",
140
- "./components/routes/views/*": "./components/routes/views/*.js",
141
- "./components/routes/admin/users/*": "./components/routes/admin/users/*.js",
142
- "./components/routes/hits/view/*": "./components/routes/hits/view/*.js",
143
- "./components/routes/hits/search/*": "./components/routes/hits/search/*.js",
144
- "./components/routes/hits/search/shared/*": "./components/routes/hits/search/shared/*.js",
145
- "./components/routes/hits/search/grid/*": "./components/routes/hits/search/grid/*.js",
146
- "./components/routes/action/view/*": "./components/routes/action/view/*.js",
147
180
  "./components/routes/action/edit/*": "./components/routes/action/edit/*.js",
181
+ "./components/routes/action/view/*": "./components/routes/action/view/*.js",
148
182
  "./components/routes/action/shared/*": "./components/routes/action/shared/*.js",
149
183
  "./components/routes/action/view/markdown/*.md": "./components/routes/action/view/markdown/*.md.js",
184
+ "./components/routes/overviews/template/*": "./components/routes/overviews/template/*.js",
185
+ "./components/routes/hits/search/*": "./components/routes/hits/search/*.js",
186
+ "./components/routes/hits/view/*": "./components/routes/hits/view/*.js",
187
+ "./components/routes/hits/search/grid/*": "./components/routes/hits/search/grid/*.js",
188
+ "./components/routes/hits/search/shared/*": "./components/routes/hits/search/shared/*.js",
150
189
  "./components/routes/analytics/widgets/*": "./components/routes/analytics/widgets/*.js",
151
- "./components/routes/help/markdown/*.md": "./components/routes/help/markdown/*.md.js",
152
190
  "./components/routes/help/components/*": "./components/routes/help/components/*.js",
191
+ "./components/routes/help/markdown/*.md": "./components/routes/help/markdown/*.md.js",
153
192
  "./components/routes/help/markdown/fr/*.md": "./components/routes/help/markdown/fr/*.md.js",
154
193
  "./components/routes/help/markdown/en/*.md": "./components/routes/help/markdown/en/*.md.js",
194
+ "./components/routes/admin/users/*": "./components/routes/admin/users/*.js",
155
195
  "./components/routes/cases/modals/*": "./components/routes/cases/modals/*.js",
156
196
  "./components/routes/cases/hooks/*": "./components/routes/cases/hooks/*.js",
157
197
  "./components/routes/cases/detail/*": "./components/routes/cases/detail/*.js",
158
198
  "./components/routes/cases/detail/sidebar/*": "./components/routes/cases/detail/sidebar/*.js",
159
199
  "./components/routes/cases/detail/assets/*": "./components/routes/cases/detail/assets/*.js",
160
200
  "./components/routes/cases/detail/aggregates/*": "./components/routes/cases/detail/aggregates/*.js",
161
- "./components/routes/overviews/template/*": "./components/routes/overviews/template/*.js",
162
- "./components/app/hooks/*": "./components/app/hooks/*.js",
163
- "./components/app/drawers/*": "./components/app/drawers/*.js",
164
- "./components/app/providers/*": "./components/app/providers/*.js",
165
- "./components/elements/view/*": "./components/elements/view/*.js",
166
- "./components/elements/addons/*": "./components/elements/addons/*.js",
167
- "./components/elements/record/*": "./components/elements/record/*.js",
168
- "./components/elements/case/*": "./components/elements/case/*.js",
169
- "./components/elements/display/*": "./components/elements/display/*.js",
170
- "./components/elements/hit/*": "./components/elements/hit/*.js",
171
- "./components/elements/observable/*": "./components/elements/observable/*.js",
172
- "./components/elements/addons/search/*": "./components/elements/addons/search/*.js",
173
- "./components/elements/addons/lists/*": "./components/elements/addons/lists/*.js",
174
- "./components/elements/addons/lists": "./components/elements/addons/lists/index.js",
175
- "./components/elements/addons/buttons/*": "./components/elements/addons/buttons/*.js",
176
- "./components/elements/addons/buttons": "./components/elements/addons/buttons/index.js",
177
- "./components/elements/addons/layout/*": "./components/elements/addons/layout/*.js",
178
- "./components/elements/addons/search/phrase/*": "./components/elements/addons/search/phrase/*.js",
179
- "./components/elements/addons/search/phrase": "./components/elements/addons/search/phrase/index.js",
180
- "./components/elements/addons/search/phrase/word/*": "./components/elements/addons/search/phrase/word/*.js",
181
- "./components/elements/addons/search/phrase/word/consumers/*": "./components/elements/addons/search/phrase/word/consumers/*.js",
182
- "./components/elements/addons/lists/hooks/*": "./components/elements/addons/lists/hooks/*.js",
183
- "./components/elements/addons/lists/table/*": "./components/elements/addons/lists/table/*.js",
184
- "./components/elements/addons/lists/table": "./components/elements/addons/lists/table/index.js",
185
- "./components/elements/addons/layout/vsbox/*": "./components/elements/addons/layout/vsbox/*.js",
186
- "./components/elements/display/json/*": "./components/elements/display/json/*.js",
187
- "./components/elements/display/modals/*": "./components/elements/display/modals/*.js",
188
- "./components/elements/display/features/*": "./components/elements/display/features/*.js",
189
- "./components/elements/display/markdownPlugins/*.md": "./components/elements/display/markdownPlugins/*.md.js",
190
- "./components/elements/display/handlebars/*": "./components/elements/display/handlebars/*.js",
191
- "./components/elements/display/icons/*": "./components/elements/display/icons/*.js",
192
- "./components/elements/display/icons/svg/*": "./components/elements/display/icons/svg/*.js",
193
- "./components/elements/hit/outlines/*": "./components/elements/hit/outlines/*.js",
194
- "./components/elements/hit/related/*": "./components/elements/hit/related/*.js",
195
- "./components/elements/hit/actions/*": "./components/elements/hit/actions/*.js",
196
- "./components/elements/hit/elements/*": "./components/elements/hit/elements/*.js",
197
- "./components/elements/hit/aggregate/*": "./components/elements/hit/aggregate/*.js",
198
- "./components/elements/hit/outlines/al/*": "./components/elements/hit/outlines/al/*.js",
199
- "./components/logins/hooks/*": "./components/logins/hooks/*.js",
200
- "./components/logins/auth/*": "./components/logins/auth/*.js",
201
- "./plugins/clue/*": "./plugins/clue/*.js",
202
- "./plugins/clue": "./plugins/clue/index.js",
203
- "./plugins/clue/components/*": "./plugins/clue/components/*.js",
204
- "./plugins/clue/locales/*": "./plugins/clue/locales/*.js",
205
- "./models/socket/*": "./models/socket/*.js",
206
- "./models/entities/*": "./models/entities/*.js",
207
- "./models/entities/generated/*": "./models/entities/generated/*.js",
208
- "./locales/fr/*.json": "./locales/fr/*.json",
209
- "./locales/en/*.json": "./locales/en/*.json",
210
- "./locales/fr/help/*.json": "./locales/fr/help/*.json",
211
- "./locales/en/help/*.json": "./locales/en/help/*.json",
212
201
  "./commons/components/*": "./commons/components/*.js",
213
202
  "./commons/components/breadcrumbs/*": "./commons/components/breadcrumbs/*.js",
203
+ "./commons/components/app/*": "./commons/components/app/*.js",
204
+ "./commons/components/utils/*": "./commons/components/utils/*.js",
214
205
  "./commons/components/notification/*": "./commons/components/notification/*.js",
215
206
  "./commons/components/notification": "./commons/components/notification/index.js",
216
- "./commons/components/topnav/*": "./commons/components/topnav/*.js",
217
- "./commons/components/app/*": "./commons/components/app/*.js",
218
207
  "./commons/components/display/*": "./commons/components/display/*.js",
219
208
  "./commons/components/leftnav/*": "./commons/components/leftnav/*.js",
220
- "./commons/components/pages/*": "./commons/components/pages/*.js",
221
- "./commons/components/utils/*": "./commons/components/utils/*.js",
222
209
  "./commons/components/search/*": "./commons/components/search/*.js",
223
- "./commons/components/notification/elements/*": "./commons/components/notification/elements/*.js",
224
- "./commons/components/notification/elements/item/*": "./commons/components/notification/elements/item/*.js",
210
+ "./commons/components/pages/*": "./commons/components/pages/*.js",
211
+ "./commons/components/topnav/*": "./commons/components/topnav/*.js",
212
+ "./commons/components/app/providers/*": "./commons/components/app/providers/*.js",
225
213
  "./commons/components/app/hooks/*": "./commons/components/app/hooks/*.js",
226
214
  "./commons/components/app/hooks": "./commons/components/app/hooks/index.js",
227
- "./commons/components/app/providers/*": "./commons/components/app/providers/*.js",
215
+ "./commons/components/utils/hooks/*": "./commons/components/utils/hooks/*.js",
216
+ "./commons/components/notification/elements/*": "./commons/components/notification/elements/*.js",
217
+ "./commons/components/notification/elements/item/*": "./commons/components/notification/elements/item/*.js",
228
218
  "./commons/components/display/hooks/*": "./commons/components/display/hooks/*.js",
229
219
  "./commons/components/pages/hooks/*": "./commons/components/pages/hooks/*.js",
230
- "./commons/components/utils/hooks/*": "./commons/components/utils/hooks/*.js",
231
- "./api/action/*": "./api/action/*.js",
232
- "./api/action": "./api/action/index.js",
220
+ "./locales/fr/*.json": "./locales/fr/*.json",
221
+ "./locales/en/*.json": "./locales/en/*.json",
222
+ "./locales/fr/help/*.json": "./locales/fr/help/*.json",
223
+ "./locales/en/help/*.json": "./locales/en/help/*.json",
224
+ "./api/overview/*": "./api/overview/*.js",
225
+ "./api/overview": "./api/overview/index.js",
233
226
  "./api/v2/*": "./api/v2/*.js",
234
227
  "./api/v2": "./api/v2/index.js",
235
- "./api/view/*": "./api/view/*.js",
236
- "./api/view": "./api/view/index.js",
228
+ "./api/action/*": "./api/action/*.js",
229
+ "./api/action": "./api/action/index.js",
230
+ "./api/auth/*": "./api/auth/*.js",
231
+ "./api/auth": "./api/auth/index.js",
237
232
  "./api/notebook/*": "./api/notebook/*.js",
238
233
  "./api/notebook": "./api/notebook/index.js",
234
+ "./api/template/*": "./api/template/*.js",
235
+ "./api/template": "./api/template/index.js",
239
236
  "./api/analytic/*": "./api/analytic/*.js",
240
237
  "./api/analytic": "./api/analytic/index.js",
241
- "./api/hit/*": "./api/hit/*.js",
242
- "./api/hit": "./api/hit/index.js",
243
- "./api/overview/*": "./api/overview/*.js",
244
- "./api/overview": "./api/overview/index.js",
245
- "./api/configs/*": "./api/configs/*.js",
246
- "./api/configs": "./api/configs/index.js",
247
238
  "./api/user/*": "./api/user/*.js",
248
239
  "./api/user": "./api/user/index.js",
249
240
  "./api/dossier/*": "./api/dossier/*.js",
250
241
  "./api/dossier": "./api/dossier/index.js",
251
- "./api/auth/*": "./api/auth/*.js",
252
- "./api/auth": "./api/auth/index.js",
253
242
  "./api/search/*": "./api/search/*.js",
254
243
  "./api/search": "./api/search/index.js",
255
- "./api/template/*": "./api/template/*.js",
256
- "./api/template": "./api/template/index.js",
257
- "./api/v2/case/*": "./api/v2/case/*.js",
258
- "./api/v2/case": "./api/v2/case/index.js",
244
+ "./api/configs/*": "./api/configs/*.js",
245
+ "./api/configs": "./api/configs/index.js",
246
+ "./api/hit/*": "./api/hit/*.js",
247
+ "./api/hit": "./api/hit/index.js",
248
+ "./api/view/*": "./api/view/*.js",
249
+ "./api/view": "./api/view/index.js",
259
250
  "./api/v2/search/*": "./api/v2/search/*.js",
260
251
  "./api/v2/search": "./api/v2/search/index.js",
261
- "./api/analytic/notebooks/*": "./api/analytic/notebooks/*.js",
262
- "./api/analytic/notebooks": "./api/analytic/notebooks/index.js",
252
+ "./api/v2/case/*": "./api/v2/case/*.js",
253
+ "./api/v2/case": "./api/v2/case/index.js",
263
254
  "./api/analytic/comments/*": "./api/analytic/comments/*.js",
264
255
  "./api/analytic/comments": "./api/analytic/comments/index.js",
265
- "./api/hit/comments/*": "./api/hit/comments/*.js",
266
- "./api/hit/comments": "./api/hit/comments/index.js",
256
+ "./api/analytic/notebooks/*": "./api/analytic/notebooks/*.js",
257
+ "./api/analytic/notebooks": "./api/analytic/notebooks/index.js",
267
258
  "./api/user/avatar/*": "./api/user/avatar/*.js",
268
259
  "./api/user/avatar": "./api/user/avatar/index.js",
260
+ "./api/search/eql/*": "./api/search/eql/*.js",
269
261
  "./api/search/histogram/*": "./api/search/histogram/*.js",
270
262
  "./api/search/histogram": "./api/search/histogram/index.js",
271
263
  "./api/search/facet/*": "./api/search/facet/*.js",
272
264
  "./api/search/facet": "./api/search/facet/index.js",
273
- "./api/search/eql/*": "./api/search/eql/*.js",
274
- "./api/search/count/*": "./api/search/count/*.js",
275
- "./api/search/count": "./api/search/count/index.js",
265
+ "./api/search/fields/*": "./api/search/fields/*.js",
266
+ "./api/search/fields": "./api/search/fields/index.js",
276
267
  "./api/search/grouped/*": "./api/search/grouped/*.js",
277
268
  "./api/search/grouped": "./api/search/grouped/index.js",
269
+ "./api/search/sigma/*": "./api/search/sigma/*.js",
278
270
  "./api/search/explain/*": "./api/search/explain/*.js",
279
- "./api/search/fields/*": "./api/search/fields/*.js",
280
- "./api/search/fields": "./api/search/fields/index.js",
281
- "./api/search/sigma/*": "./api/search/sigma/*.js"
271
+ "./api/search/count/*": "./api/search/count/*.js",
272
+ "./api/search/count": "./api/search/count/index.js",
273
+ "./api/hit/comments/*": "./api/hit/comments/*.js",
274
+ "./api/hit/comments": "./api/hit/comments/index.js",
275
+ "./plugins/clue/*": "./plugins/clue/*.js",
276
+ "./plugins/clue": "./plugins/clue/index.js",
277
+ "./plugins/clue/components/*": "./plugins/clue/components/*.js",
278
+ "./plugins/clue/locales/*": "./plugins/clue/locales/*.js",
279
+ "./models/socket/*": "./models/socket/*.js",
280
+ "./models/entities/*": "./models/entities/*.js",
281
+ "./models/entities/generated/*": "./models/entities/generated/*.js"
282
282
  }
283
283
  }