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

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.
@@ -0,0 +1,351 @@
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
+ const mockPatch = vi.hoisted(() => vi.fn());
25
+ vi.mock('api', () => ({
26
+ default: {
27
+ v2: {
28
+ case: {
29
+ items: {
30
+ del: (...args) => mockDel(...args),
31
+ patch: (...args) => mockPatch(...args)
32
+ }
33
+ }
34
+ }
35
+ }
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
+ }));
47
+ // ---------------------------------------------------------------------------
48
+ // Imports (after mocks so that module registry picks up the stubs)
49
+ // ---------------------------------------------------------------------------
50
+ import CaseFolderContextMenu, { collectAllLeaves, getOpenUrl } from './CaseFolderContextMenu';
51
+ // ---------------------------------------------------------------------------
52
+ // Fixtures
53
+ // ---------------------------------------------------------------------------
54
+ const mockCase = { case_id: 'case-1', title: 'Test Case', items: [] };
55
+ const hitLeaf = { type: 'hit', value: 'hit-123', path: 'folder/hit-item' };
56
+ const referenceLeaf = { type: 'reference', value: 'https://example.com', path: 'folder/ref-item' };
57
+ const observableLeaf = { type: 'observable', value: 'obs-456', path: 'folder/obs-item' };
58
+ const caseLeaf = { type: 'case', value: 'nested-case-id', path: 'folder/case-item' };
59
+ const tableLeaf = { type: 'table', value: 'table-789', path: 'folder/table-item' };
60
+ const leadLeaf = { type: 'lead', value: 'lead-999', path: 'folder/lead-item' };
61
+ const renderMenu = (props) => render(_jsx(CaseFolderContextMenu, { _case: mockCase, ...props, children: _jsx("div", { id: "child", children: "child" }) }));
62
+ // ---------------------------------------------------------------------------
63
+ // Setup
64
+ // ---------------------------------------------------------------------------
65
+ beforeEach(() => {
66
+ mockDel.mockClear();
67
+ mockPatch.mockClear();
68
+ mockDispatchApi.mockClear();
69
+ mockShowModal.mockClear();
70
+ mockDispatchApi.mockImplementation((p) => p);
71
+ mockDel.mockResolvedValue(mockCase);
72
+ mockPatch.mockResolvedValue(mockCase);
73
+ vi.spyOn(window, 'open').mockReturnValue(null);
74
+ });
75
+ // ---------------------------------------------------------------------------
76
+ // Unit tests for exported utilities
77
+ // ---------------------------------------------------------------------------
78
+ describe('collectAllLeaves', () => {
79
+ it('returns leaves at the root level', () => {
80
+ const tree = { leaves: [hitLeaf, referenceLeaf] };
81
+ expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
82
+ });
83
+ it('returns leaves from nested subfolders', () => {
84
+ const tree = {
85
+ leaves: [hitLeaf],
86
+ subfolder: { leaves: [referenceLeaf] }
87
+ };
88
+ expect(collectAllLeaves(tree)).toEqual([hitLeaf, referenceLeaf]);
89
+ });
90
+ it('returns leaves from deeply nested subfolders', () => {
91
+ const tree = {
92
+ leaves: [],
93
+ level1: {
94
+ leaves: [hitLeaf],
95
+ level2: { leaves: [referenceLeaf] }
96
+ }
97
+ };
98
+ const result = collectAllLeaves(tree);
99
+ expect(result).toContain(hitLeaf);
100
+ expect(result).toContain(referenceLeaf);
101
+ });
102
+ it('returns an empty array for an empty tree', () => {
103
+ expect(collectAllLeaves({ leaves: [] })).toEqual([]);
104
+ });
105
+ });
106
+ describe('getOpenUrl', () => {
107
+ it('returns the value directly for a reference item', () => {
108
+ expect(getOpenUrl(referenceLeaf)).toBe('https://example.com');
109
+ });
110
+ it('returns /hits/<id> for a hit item', () => {
111
+ expect(getOpenUrl(hitLeaf)).toBe('/hits/hit-123');
112
+ });
113
+ it('returns /observables/<id> for an observable item', () => {
114
+ expect(getOpenUrl(observableLeaf)).toBe('/observables/obs-456');
115
+ });
116
+ it('returns /cases/<id> for a case item', () => {
117
+ expect(getOpenUrl(caseLeaf)).toBe('/cases/nested-case-id');
118
+ });
119
+ it('returns null for a table item', () => {
120
+ expect(getOpenUrl(tableLeaf)).toBeNull();
121
+ });
122
+ it('returns null for a lead item', () => {
123
+ expect(getOpenUrl(leadLeaf)).toBeNull();
124
+ });
125
+ it('returns null when value is undefined', () => {
126
+ expect(getOpenUrl({ type: 'hit' })).toBeNull();
127
+ });
128
+ it('returns null when type is undefined', () => {
129
+ expect(getOpenUrl({ value: 'something' })).toBeNull();
130
+ });
131
+ });
132
+ // ---------------------------------------------------------------------------
133
+ // Component tests
134
+ // ---------------------------------------------------------------------------
135
+ describe('CaseFolderContextMenu', () => {
136
+ describe('renders children', () => {
137
+ it('renders children content', () => {
138
+ renderMenu({ leaf: hitLeaf });
139
+ expect(screen.getByTestId('child')).toBeInTheDocument();
140
+ });
141
+ });
142
+ describe('menu items for leaf types', () => {
143
+ it('shows "Open item" and "Remove item" for a hit leaf', () => {
144
+ renderMenu({ leaf: hitLeaf });
145
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
146
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
147
+ });
148
+ it('shows "Open item" and "Remove item" for a reference leaf', () => {
149
+ renderMenu({ leaf: referenceLeaf });
150
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
151
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
152
+ });
153
+ it('shows "Open item" and "Remove item" for an observable leaf', () => {
154
+ renderMenu({ leaf: observableLeaf });
155
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
156
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
157
+ });
158
+ it('shows "Open item" and "Remove item" for a case leaf', () => {
159
+ renderMenu({ leaf: caseLeaf });
160
+ expect(screen.getByTestId('open-item')).toBeInTheDocument();
161
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
162
+ });
163
+ it('shows only "Remove item" for a table leaf (no open URL)', () => {
164
+ renderMenu({ leaf: tableLeaf });
165
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
166
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
167
+ });
168
+ it('shows only "Remove item" for a lead leaf (no open URL)', () => {
169
+ renderMenu({ leaf: leadLeaf });
170
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
171
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
172
+ });
173
+ it('labels the remove button "Remove item" for a leaf', () => {
174
+ renderMenu({ leaf: hitLeaf });
175
+ expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.item.remove');
176
+ });
177
+ it('shows a divider for all leaf types (between leaf actions and remove)', () => {
178
+ const { container: withOpen } = renderMenu({ leaf: hitLeaf });
179
+ expect(withOpen.querySelector('hr')).not.toBeNull();
180
+ const { container: withoutOpen } = renderMenu({ leaf: tableLeaf });
181
+ expect(withoutOpen.querySelector('hr')).not.toBeNull();
182
+ const { container: withFolder } = renderMenu({ tree: { leaves: [hitLeaf] } });
183
+ expect(withFolder.querySelector('hr')).toBeNull();
184
+ });
185
+ });
186
+ describe('menu items for folders', () => {
187
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
188
+ it('shows only "Remove folder" for a folder (no open URL)', () => {
189
+ renderMenu({ tree: folderTree });
190
+ expect(screen.queryByTestId('open-item')).not.toBeInTheDocument();
191
+ expect(screen.getByTestId('remove-item')).toBeInTheDocument();
192
+ });
193
+ it('labels the remove button "Remove folder" for a tree', () => {
194
+ renderMenu({ tree: folderTree });
195
+ expect(screen.getByTestId('remove-item')).toHaveTextContent('page.cases.sidebar.folder.remove');
196
+ });
197
+ });
198
+ describe('"Open item" action', () => {
199
+ it('calls window.open with the hit URL', () => {
200
+ renderMenu({ leaf: hitLeaf });
201
+ act(() => {
202
+ fireEvent.click(screen.getByTestId('open-item'));
203
+ });
204
+ expect(window.open).toHaveBeenCalledWith('/hits/hit-123', '_blank', 'noopener noreferrer');
205
+ });
206
+ it('calls window.open with the reference URL directly', () => {
207
+ renderMenu({ leaf: referenceLeaf });
208
+ act(() => {
209
+ fireEvent.click(screen.getByTestId('open-item'));
210
+ });
211
+ expect(window.open).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener noreferrer');
212
+ });
213
+ it('calls window.open with the observable URL', () => {
214
+ renderMenu({ leaf: observableLeaf });
215
+ act(() => {
216
+ fireEvent.click(screen.getByTestId('open-item'));
217
+ });
218
+ expect(window.open).toHaveBeenCalledWith('/observables/obs-456', '_blank', 'noopener noreferrer');
219
+ });
220
+ it('calls window.open with the case URL', () => {
221
+ renderMenu({ leaf: caseLeaf });
222
+ act(() => {
223
+ fireEvent.click(screen.getByTestId('open-item'));
224
+ });
225
+ expect(window.open).toHaveBeenCalledWith('/cases/nested-case-id', '_blank', 'noopener noreferrer');
226
+ });
227
+ });
228
+ describe('"Remove item" action for a leaf', () => {
229
+ it('calls dispatchApi with the delete call for the leaf', async () => {
230
+ renderMenu({ leaf: hitLeaf });
231
+ act(() => {
232
+ fireEvent.click(screen.getByTestId('remove-item'));
233
+ });
234
+ await waitFor(() => {
235
+ expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123']);
236
+ });
237
+ });
238
+ it('calls onUpdate with the updated case after the delete resolves', async () => {
239
+ const onUpdate = vi.fn();
240
+ renderMenu({ leaf: hitLeaf, onUpdate: onUpdate });
241
+ act(() => {
242
+ fireEvent.click(screen.getByTestId('remove-item'));
243
+ });
244
+ await waitFor(() => {
245
+ expect(onUpdate).toHaveBeenCalledWith(mockCase);
246
+ });
247
+ });
248
+ it('does not call the API when case_id is missing', () => {
249
+ renderMenu({ _case: { title: 'No ID' }, leaf: hitLeaf });
250
+ act(() => {
251
+ fireEvent.click(screen.getByTestId('remove-item'));
252
+ });
253
+ expect(mockDel).not.toHaveBeenCalled();
254
+ });
255
+ it('skips items with no value', async () => {
256
+ const noValueLeaf = { type: 'hit', path: 'folder/no-value' };
257
+ renderMenu({ leaf: noValueLeaf });
258
+ act(() => {
259
+ fireEvent.click(screen.getByTestId('remove-item'));
260
+ });
261
+ await waitFor(() => {
262
+ expect(mockDel).not.toHaveBeenCalled();
263
+ });
264
+ });
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
+ });
304
+ describe('"Remove folder" action', () => {
305
+ it('calls dispatchApi with all leaf values in a single batch call', async () => {
306
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
307
+ renderMenu({ tree: folderTree });
308
+ act(() => {
309
+ fireEvent.click(screen.getByTestId('remove-item'));
310
+ });
311
+ await waitFor(() => {
312
+ expect(mockDel).toHaveBeenCalledWith('case-1', ['hit-123', 'https://example.com']);
313
+ expect(mockDel).toHaveBeenCalledTimes(1);
314
+ });
315
+ });
316
+ it('calls dispatchApi with leaves from nested subfolders in a single batch call', async () => {
317
+ const nestedTree = {
318
+ leaves: [hitLeaf],
319
+ subfolder: { leaves: [referenceLeaf] }
320
+ };
321
+ renderMenu({ tree: nestedTree });
322
+ act(() => {
323
+ fireEvent.click(screen.getByTestId('remove-item'));
324
+ });
325
+ await waitFor(() => {
326
+ expect(mockDel).toHaveBeenCalledWith('case-1', expect.arrayContaining(['hit-123', 'https://example.com']));
327
+ expect(mockDel).toHaveBeenCalledTimes(1);
328
+ });
329
+ });
330
+ it('calls onUpdate with the updated case after deletion', async () => {
331
+ const onUpdate = vi.fn();
332
+ const folderTree = { leaves: [hitLeaf, referenceLeaf] };
333
+ renderMenu({ tree: folderTree, onUpdate: onUpdate });
334
+ act(() => {
335
+ fireEvent.click(screen.getByTestId('remove-item'));
336
+ });
337
+ await waitFor(() => {
338
+ expect(onUpdate).toHaveBeenCalledWith(mockCase);
339
+ });
340
+ });
341
+ it('does not call the API or onUpdate for an empty folder', () => {
342
+ const onUpdate = vi.fn();
343
+ renderMenu({ tree: { leaves: [] }, onUpdate: onUpdate });
344
+ act(() => {
345
+ fireEvent.click(screen.getByTestId('remove-item'));
346
+ });
347
+ expect(mockDel).not.toHaveBeenCalled();
348
+ expect(onUpdate).not.toHaveBeenCalled();
349
+ });
350
+ });
351
+ });
@@ -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;
@@ -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;
@@ -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,19 @@
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.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",
324
+ "modal.cases.resolve": "Resolve Case",
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?",
321
327
  "modal.confirm.delete.title": "Confirm Deletion",
322
328
  "modal.rationale.description": "Provide a rationale that succinctly explains to other analysts why you are confident in this assessment.",
@@ -384,6 +390,10 @@
384
390
  "page.cases.detail.properties": "Properties",
385
391
  "page.cases.detail.status": "Status",
386
392
  "page.cases.escalation": "Escalation",
393
+ "page.cases.sidebar.folder.remove": "Remove folder",
394
+ "page.cases.sidebar.item.open": "Open item",
395
+ "page.cases.sidebar.item.remove": "Remove item",
396
+ "page.cases.sidebar.item.rename": "Rename item",
387
397
  "page.cases.sources": "Sources",
388
398
  "page.cases.updated": "Updated",
389
399
  "page.dashboard.settings.edit": "Edit Dashboard",
@@ -310,13 +310,19 @@
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.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",
324
+ "modal.cases.resolve": "Résoudre le cas",
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 ?",
321
327
  "modal.confirm.delete.title": "Confirmer la suppression",
322
328
  "modal.rationale.description": "Fournissez une justification qui explique succinctement aux autres analystes les raisons pour lesquelles vous êtes confiant dans cette évaluation.",
@@ -384,6 +390,10 @@
384
390
  "page.cases.detail.properties": "Propriétés",
385
391
  "page.cases.detail.status": "Statut",
386
392
  "page.cases.escalation": "Escalade",
393
+ "page.cases.sidebar.folder.remove": "Supprimer le dossier",
394
+ "page.cases.sidebar.item.open": "Ouvrir l'élément",
395
+ "page.cases.sidebar.item.remove": "Supprimer l'élément",
396
+ "page.cases.sidebar.item.rename": "Renommer l'élément",
387
397
  "page.cases.sources": "Sources",
388
398
  "page.cases.updated": "Mis à jour",
389
399
  "page.dashboard.settings.edit": "Modifier le tableau de bord",