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

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.
@@ -3,3 +3,4 @@ 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
5
  export declare const del: (id: string, values: string | string[]) => Promise<Case>;
6
+ export declare const patch: (id: string, value: string, newPath: string) => Promise<Case>;
@@ -1,5 +1,5 @@
1
1
  // eslint-disable-next-line import/no-cycle
2
- import { hdelete, hpost, joinUri } from '@cccsaurora/howler-ui/api';
2
+ import { hdelete, hpatch, hpost, joinUri } from '@cccsaurora/howler-ui/api';
3
3
  import { uri as parentUri } from '@cccsaurora/howler-ui/api/v2/case';
4
4
  export const uri = (id) => {
5
5
  return joinUri(parentUri(id), 'items');
@@ -13,3 +13,6 @@ export const del = (id, values) => {
13
13
  }
14
14
  return hdelete(uri(id), { values });
15
15
  };
16
+ export const patch = (id, value, newPath) => {
17
+ return hpatch(uri(id), { value, new_path: newPath });
18
+ };
@@ -1,6 +1,7 @@
1
1
  import type { FC, PropsWithChildren, ReactNode } from 'react';
2
2
  export interface ModalOptions {
3
3
  disableClose?: boolean;
4
+ height?: number | string | null;
4
5
  maxWidth?: string;
5
6
  maxHeight?: string;
6
7
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Modal as MuiModal } from '@mui/material';
3
3
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
4
+ import { has } from 'lodash-es';
4
5
  import { useCallback, useContext } from 'react';
5
6
  const Modal = () => {
6
7
  const { content, setContent, options } = useContext(ModalContext);
@@ -15,7 +16,7 @@ const Modal = () => {
15
16
  left: '50%',
16
17
  maxWidth: options.maxWidth || '1200px',
17
18
  maxHeight: options.maxHeight || '400px',
18
- height: '100%',
19
+ height: has(options, 'height') ? options.height : '100%',
19
20
  transform: 'translate(-50%, -50%)',
20
21
  backgroundColor: 'background.paper',
21
22
  borderRadius: theme.shape.borderRadius,
@@ -56,6 +56,6 @@ const CaseSidebar = ({ case: _case, update }) => {
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, onItemRemoved: update }) }) }))] }));
59
+ }, children: _jsx(Box, { position: "absolute", sx: { left: 0, right: 0 }, children: _jsx(CaseFolder, { case: _case, onItemUpdated: update }) }) }))] }));
60
60
  };
61
61
  export default CaseSidebar;
@@ -8,7 +8,7 @@ interface CaseFolderProps {
8
8
  step?: number;
9
9
  rootCaseId?: string;
10
10
  pathPrefix?: string;
11
- onItemRemoved?: (newCase: Case) => void;
11
+ onItemUpdated?: (newCase: Case) => void;
12
12
  }
13
13
  declare const CaseFolder: FC<CaseFolderProps>;
14
14
  export default CaseFolder;
@@ -18,7 +18,7 @@ const ICON_FOR_TYPE = {
18
18
  lead: Lightbulb,
19
19
  reference: LinkIcon
20
20
  };
21
- const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '', onItemRemoved }) => {
21
+ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPrefix = '', onItemUpdated }) => {
22
22
  const theme = useTheme();
23
23
  const location = useLocation();
24
24
  const { dispatchApi } = useMyApi();
@@ -76,7 +76,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
76
76
  setCaseStates(current => ({ ...current, [resolvedKey]: { ...current[resolvedKey], loading: false } }));
77
77
  });
78
78
  }, [caseStates, dispatchApi]);
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: {
79
+ return (_jsxs(Stack, { sx: { overflow: 'visible' }, children: [name && (_jsx(CaseFolderContextMenu, { _case: _case, tree: tree, onUpdate: onItemUpdated, children: _jsxs(Stack, { direction: "row", pl: step * 1.5, py: 0.25, sx: {
80
80
  cursor: 'pointer',
81
81
  transition: theme.transitions.create('background', { duration: 50 }),
82
82
  background: 'transparent',
@@ -86,7 +86,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
86
86
  }, onClick: () => setOpen(_open => !_open), children: [_jsx(ChevronRight, { fontSize: "small", color: "disabled", sx: [
87
87
  { transition: theme.transitions.create('transform', { duration: 100 }), transform: 'rotate(0deg)' },
88
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
+ ] }), _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, onItemUpdated: onItemUpdated }, `${_case?.case_id}-${path}`))), tree.leaves?.map(leaf => {
90
90
  const itemType = leaf.type?.toLowerCase();
91
91
  const isCase = itemType === 'case';
92
92
  const fullRelativePath = [pathPrefix, leaf.path].filter(Boolean).join('/');
@@ -104,7 +104,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
104
104
  const iconColor = escalationColor ?? 'inherit';
105
105
  const leafColor = escalationColor ? `${escalationColor}.light` : 'text.secondary';
106
106
  const Icon = ICON_FOR_TYPE[itemType ?? ''] ?? Article;
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: [
107
+ return (_jsx(CaseFolderContextMenu, { _case: _case, leaf: leaf, onUpdate: onItemUpdated, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", pl: step * 1.5 + 1, py: 0.25, sx: [
108
108
  {
109
109
  cursor: 'pointer',
110
110
  overflow: 'visible',
@@ -127,7 +127,7 @@ const CaseFolder = ({ case: _case, folder, name, step = -1, rootCaseId, pathPref
127
127
  transition: theme.transitions.create('transform', { duration: 100 }),
128
128
  transform: isCaseOpen ? 'rotate(90deg)' : 'rotate(0deg)'
129
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
+ ] }), _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, onItemUpdated: onItemUpdated }))] }) }, `${_case?.case_id}-${leaf.value}-${leaf.path}`));
131
131
  })] }))] }));
132
132
  };
133
133
  export default CaseFolder;
@@ -22,8 +22,8 @@ export interface CaseFolderContextMenuProps extends PropsWithChildren {
22
22
  leaf?: Item;
23
23
  /** Present when the context menu is for a folder (all leaves within it will be removed). */
24
24
  tree?: Tree;
25
- /** Called after item(s) have been successfully removed. */
26
- onRemoved?: (updatedCase: Case) => void;
25
+ /** Called after item(s) have been updated (renamed, removed). */
26
+ onUpdate?: (updatedCase: Case) => void;
27
27
  }
28
28
  /**
29
29
  * Wraps its children with a right-click context menu providing:
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Delete, OpenInNew } from '@mui/icons-material';
2
+ import { Delete, DriveFileRenameOutline, OpenInNew } from '@mui/icons-material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
4
5
  import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
5
6
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
- import { useMemo } from 'react';
7
+ import RenameItemModal from '@cccsaurora/howler-ui/components/routes/cases/modals/RenameItemModal';
8
+ import { useContext, useMemo } from 'react';
7
9
  import { useTranslation } from 'react-i18next';
8
10
  /**
9
11
  * Recursively collects all leaf items from a folder tree.
@@ -46,9 +48,10 @@ export const getOpenUrl = (leaf) => {
46
48
  * - **Open item** – opens the item in a new tab (only for leaf items with a navigable URL).
47
49
  * - **Remove item / Remove folder** – deletes the leaf item or all items under a folder.
48
50
  */
49
- const CaseFolderContextMenu = ({ _case, leaf, tree, onRemoved, children }) => {
51
+ const CaseFolderContextMenu = ({ _case, leaf, tree, onUpdate, children }) => {
50
52
  const { dispatchApi } = useMyApi();
51
53
  const { t } = useTranslation();
54
+ const { showModal } = useContext(ModalContext);
52
55
  const items = useMemo(() => {
53
56
  const entries = [];
54
57
  if (leaf) {
@@ -62,6 +65,13 @@ const CaseFolderContextMenu = ({ _case, leaf, tree, onRemoved, children }) => {
62
65
  onClick: () => window.open(openUrl, '_blank', 'noopener noreferrer')
63
66
  });
64
67
  }
68
+ entries.push({
69
+ kind: 'item',
70
+ id: 'rename-item',
71
+ label: t('page.cases.sidebar.item.rename'),
72
+ icon: _jsx(DriveFileRenameOutline, { fontSize: "small" }),
73
+ onClick: () => showModal(_jsx(RenameItemModal, { _case: _case, leaf: leaf, onRenamed: onUpdate }), { height: null })
74
+ });
65
75
  }
66
76
  if (entries.length > 0) {
67
77
  entries.push({ kind: 'divider', id: 'divider-remove' });
@@ -83,13 +93,13 @@ const CaseFolderContextMenu = ({ _case, leaf, tree, onRemoved, children }) => {
83
93
  }
84
94
  dispatchApi(api.v2.case.items.del(_case.case_id, values), { throwError: false }).then(updatedCase => {
85
95
  if (updatedCase) {
86
- onRemoved?.(updatedCase);
96
+ onUpdate?.(updatedCase);
87
97
  }
88
98
  });
89
99
  }
90
100
  });
91
101
  return entries;
92
- }, [_case, leaf, tree, dispatchApi, onRemoved, t]);
102
+ }, [_case, leaf, tree, dispatchApi, onUpdate, showModal, t]);
93
103
  return _jsx(ContextMenu, { items: items, children: children });
94
104
  };
95
105
  export default CaseFolderContextMenu;
@@ -21,17 +21,29 @@ vi.mock('components/hooks/useMyApi', () => ({
21
21
  default: () => ({ dispatchApi: mockDispatchApi })
22
22
  }));
23
23
  const mockDel = vi.hoisted(() => vi.fn());
24
+ const mockPatch = vi.hoisted(() => vi.fn());
24
25
  vi.mock('api', () => ({
25
26
  default: {
26
27
  v2: {
27
28
  case: {
28
29
  items: {
29
- del: (...args) => mockDel(...args)
30
+ del: (...args) => mockDel(...args),
31
+ patch: (...args) => mockPatch(...args)
30
32
  }
31
33
  }
32
34
  }
33
35
  }
34
36
  }));
37
+ const mockShowModal = vi.hoisted(() => vi.fn());
38
+ vi.mock('components/app/providers/ModalProvider', async () => {
39
+ const { createContext } = await import('react');
40
+ return {
41
+ ModalContext: createContext({ showModal: mockShowModal, close: vi.fn(), setContent: vi.fn() })
42
+ };
43
+ });
44
+ vi.mock('components/routes/cases/modals/RenameItemModal', () => ({
45
+ default: () => _jsx("div", { id: "rename-item-modal" })
46
+ }));
35
47
  // ---------------------------------------------------------------------------
36
48
  // Imports (after mocks so that module registry picks up the stubs)
37
49
  // ---------------------------------------------------------------------------
@@ -52,9 +64,12 @@ const renderMenu = (props) => render(_jsx(CaseFolderContextMenu, { _case: mockCa
52
64
  // ---------------------------------------------------------------------------
53
65
  beforeEach(() => {
54
66
  mockDel.mockClear();
67
+ mockPatch.mockClear();
55
68
  mockDispatchApi.mockClear();
69
+ mockShowModal.mockClear();
56
70
  mockDispatchApi.mockImplementation((p) => p);
57
71
  mockDel.mockResolvedValue(mockCase);
72
+ mockPatch.mockResolvedValue(mockCase);
58
73
  vi.spyOn(window, 'open').mockReturnValue(null);
59
74
  });
60
75
  // ---------------------------------------------------------------------------
@@ -159,11 +174,13 @@ describe('CaseFolderContextMenu', () => {
159
174
  renderMenu({ leaf: hitLeaf });
160
175
  expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.item.remove');
161
176
  });
162
- it('shows a divider only when "Open item" is also present', () => {
177
+ it('shows a divider for all leaf types (between leaf actions and remove)', () => {
163
178
  const { container: withOpen } = renderMenu({ leaf: hitLeaf });
164
179
  expect(withOpen.querySelector('hr')).not.toBeNull();
165
180
  const { container: withoutOpen } = renderMenu({ leaf: tableLeaf });
166
- expect(withoutOpen.querySelector('hr')).toBeNull();
181
+ expect(withoutOpen.querySelector('hr')).not.toBeNull();
182
+ const { container: withFolder } = renderMenu({ tree: { leaves: [hitLeaf] } });
183
+ expect(withFolder.querySelector('hr')).toBeNull();
167
184
  });
168
185
  });
169
186
  describe('menu items for folders', () => {
@@ -218,14 +235,14 @@ describe('CaseFolderContextMenu', () => {
218
235
  expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123']);
219
236
  });
220
237
  });
221
- it('calls onRemoved with the updated case after the delete resolves', async () => {
222
- const onRemoved = vi.fn();
223
- renderMenu({ leaf: hitLeaf, onRemoved });
238
+ it('calls onUpdate with the updated case after the delete resolves', async () => {
239
+ const onUpdate = vi.fn();
240
+ renderMenu({ leaf: hitLeaf, onUpdate: onUpdate });
224
241
  act(() => {
225
242
  fireEvent.click(screen.getByTestId('remove-item'));
226
243
  });
227
244
  await waitFor(() => {
228
- expect(onRemoved).toHaveBeenCalledWith(mockCase);
245
+ expect(onUpdate).toHaveBeenCalledWith(mockCase);
229
246
  });
230
247
  });
231
248
  it('does not call the API when case_id is missing', () => {
@@ -246,6 +263,44 @@ describe('CaseFolderContextMenu', () => {
246
263
  });
247
264
  });
248
265
  });
266
+ describe('"Rename item" action', () => {
267
+ it('shows "Rename item" entry for a hit leaf', () => {
268
+ renderMenu({ leaf: hitLeaf });
269
+ expect(screen.getByTestId('rename-item')).toBeInTheDocument();
270
+ });
271
+ it('shows "Rename item" for a table leaf', () => {
272
+ renderMenu({ leaf: tableLeaf });
273
+ expect(screen.getByTestId('rename-item')).toBeInTheDocument();
274
+ });
275
+ it('does not show "Rename item" for a folder', () => {
276
+ renderMenu({ tree: { leaves: [hitLeaf] } });
277
+ expect(screen.queryByTestId('rename-item')).not.toBeInTheDocument();
278
+ });
279
+ it('calls showModal when "Rename item" is clicked', () => {
280
+ renderMenu({ leaf: hitLeaf });
281
+ act(() => {
282
+ fireEvent.click(screen.getByTestId('rename-item'));
283
+ });
284
+ expect(mockShowModal).toHaveBeenCalledTimes(1);
285
+ });
286
+ it('passes the current case and leaf to the rename modal', () => {
287
+ const onUpdate = vi.fn();
288
+ renderMenu({ leaf: hitLeaf, onUpdate: onUpdate });
289
+ act(() => {
290
+ fireEvent.click(screen.getByTestId('rename-item'));
291
+ });
292
+ const [modalElement] = mockShowModal.mock.calls[0];
293
+ expect(modalElement.props._case).toBe(mockCase);
294
+ expect(modalElement.props.leaf).toBe(hitLeaf);
295
+ });
296
+ it('works fine when onUpdate is not provided', () => {
297
+ renderMenu({ leaf: hitLeaf });
298
+ act(() => {
299
+ fireEvent.click(screen.getByTestId('rename-item'));
300
+ });
301
+ expect(mockShowModal).toHaveBeenCalledTimes(1);
302
+ });
303
+ });
249
304
  describe('"Remove folder" action', () => {
250
305
  it('calls dispatchApi with all leaf values in a single batch call', async () => {
251
306
  const folderTree = { leaves: [hitLeaf, referenceLeaf] };
@@ -272,25 +327,25 @@ describe('CaseFolderContextMenu', () => {
272
327
  expect(mockDel).toHaveBeenCalledTimes(1);
273
328
  });
274
329
  });
275
- it('calls onRemoved with the updated case after deletion', async () => {
276
- const onRemoved = vi.fn();
330
+ it('calls onUpdate with the updated case after deletion', async () => {
331
+ const onUpdate = vi.fn();
277
332
  const folderTree = { leaves: [hitLeaf, referenceLeaf] };
278
- renderMenu({ tree: folderTree, onRemoved });
333
+ renderMenu({ tree: folderTree, onUpdate: onUpdate });
279
334
  act(() => {
280
335
  fireEvent.click(screen.getByTestId('remove-item'));
281
336
  });
282
337
  await waitFor(() => {
283
- expect(onRemoved).toHaveBeenCalledWith(mockCase);
338
+ expect(onUpdate).toHaveBeenCalledWith(mockCase);
284
339
  });
285
340
  });
286
- it('does not call the API or onRemoved for an empty folder', () => {
287
- const onRemoved = vi.fn();
288
- renderMenu({ tree: { leaves: [] }, onRemoved });
341
+ it('does not call the API or onUpdate for an empty folder', () => {
342
+ const onUpdate = vi.fn();
343
+ renderMenu({ tree: { leaves: [] }, onUpdate: onUpdate });
289
344
  act(() => {
290
345
  fireEvent.click(screen.getByTestId('remove-item'));
291
346
  });
292
347
  expect(mockDel).not.toHaveBeenCalled();
293
- expect(onRemoved).not.toHaveBeenCalled();
348
+ expect(onUpdate).not.toHaveBeenCalled();
294
349
  });
295
350
  });
296
351
  });
@@ -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;
@@ -315,6 +315,12 @@
315
315
  "modal.cases.add_to_case.select_case": "Search Cases",
316
316
  "modal.cases.add_to_case.select_path": "Select Folder Path",
317
317
  "modal.cases.add_to_case.title": "Item Title",
318
+ "modal.cases.rename_item": "Rename Item",
319
+ "modal.cases.rename_item.error.empty": "Name cannot be empty",
320
+ "modal.cases.rename_item.error.slash": "Name cannot contain '/'",
321
+ "modal.cases.rename_item.error.taken": "An item already exists at this path",
322
+ "modal.cases.rename_item.folder_path": "Folder: {{path}}",
323
+ "modal.cases.rename_item.new_name": "New Name",
318
324
  "modal.cases.resolve": "Resolve Case",
319
325
  "modal.cases.resolve.description": "When resolving a case, you must either assess all open alerts, or add an assessment to the alerts.",
320
326
  "modal.confirm.delete.description": "Are you sure you want to delete this item?",
@@ -387,6 +393,7 @@
387
393
  "page.cases.sidebar.folder.remove": "Remove folder",
388
394
  "page.cases.sidebar.item.open": "Open item",
389
395
  "page.cases.sidebar.item.remove": "Remove item",
396
+ "page.cases.sidebar.item.rename": "Rename item",
390
397
  "page.cases.sources": "Sources",
391
398
  "page.cases.updated": "Updated",
392
399
  "page.dashboard.settings.edit": "Edit Dashboard",
@@ -315,6 +315,12 @@
315
315
  "modal.cases.add_to_case.select_case": "Rechercher des cas",
316
316
  "modal.cases.add_to_case.select_path": "Sélectionner le chemin du dossier",
317
317
  "modal.cases.add_to_case.title": "Titre de l'élément",
318
+ "modal.cases.rename_item": "Renommer l'élément",
319
+ "modal.cases.rename_item.error.empty": "Le nom ne peut pas être vide",
320
+ "modal.cases.rename_item.error.slash": "Le nom ne peut pas contenir '/'",
321
+ "modal.cases.rename_item.error.taken": "Un élément existe déjà à ce chemin",
322
+ "modal.cases.rename_item.folder_path": "Dossier : {{path}}",
323
+ "modal.cases.rename_item.new_name": "Nouveau nom",
318
324
  "modal.cases.resolve": "Résoudre le cas",
319
325
  "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
326
  "modal.confirm.delete.description": "Êtes-vous sûr de vouloir supprimer cet élément ?",
@@ -387,6 +393,7 @@
387
393
  "page.cases.sidebar.folder.remove": "Supprimer le dossier",
388
394
  "page.cases.sidebar.item.open": "Ouvrir l'élément",
389
395
  "page.cases.sidebar.item.remove": "Supprimer l'élément",
396
+ "page.cases.sidebar.item.rename": "Renommer l'élément",
390
397
  "page.cases.sources": "Sources",
391
398
  "page.cases.updated": "Mis à jour",
392
399
  "page.dashboard.settings.edit": "Modifier le tableau de bord",
package/package.json CHANGED
@@ -101,7 +101,7 @@
101
101
  "internal-slot": "1.0.7"
102
102
  },
103
103
  "type": "module",
104
- "version": "2.18.0-dev.695",
104
+ "version": "2.18.0-dev.700",
105
105
  "exports": {
106
106
  "./i18n": "./i18n.js",
107
107
  "./index.css": "./index.css",