@cccsaurora/howler-ui 2.15.0-dev.333 → 2.15.0-dev.339
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/elements/display/HowlerAvatar.js +4 -2
- package/components/routes/dossiers/DossierCard.js +6 -5
- package/components/routes/dossiers/DossierCard.test.d.ts +1 -0
- package/components/routes/dossiers/DossierCard.test.js +593 -0
- package/components/routes/dossiers/DossierEditor.js +10 -2
- package/components/routes/dossiers/DossierEditor.test.js +123 -1
- package/components/routes/dossiers/LeadForm.js +13 -3
- package/components/routes/dossiers/PivotForm.js +13 -3
- package/locales/en/translation.json +1 -0
- package/locales/fr/translation.json +1 -0
- package/package.json +1 -1
- package/tests/utils.d.ts +2 -0
- package/tests/utils.js +9 -0
|
@@ -2,9 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Avatar, Tooltip, useTheme } from '@mui/material';
|
|
3
3
|
import { AvatarContext } from '@cccsaurora/howler-ui/components/app/providers/AvatarProvider';
|
|
4
4
|
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
5
6
|
import { nameToInitials } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
6
7
|
import { stringToColor } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
8
|
const HowlerAvatar = ({ userId, ...avatarProps }) => {
|
|
9
|
+
const { t } = useTranslation();
|
|
8
10
|
const { getAvatar } = useContext(AvatarContext);
|
|
9
11
|
const theme = useTheme();
|
|
10
12
|
const [props, setProps] = useState();
|
|
@@ -35,10 +37,10 @@ const HowlerAvatar = ({ userId, ...avatarProps }) => {
|
|
|
35
37
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
38
|
}, [userId]);
|
|
37
39
|
if (userId) {
|
|
38
|
-
return (_jsx(Tooltip, { title: userId, children: _jsx(Avatar, { ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
|
|
40
|
+
return (_jsx(Tooltip, { title: userId, children: _jsx(Avatar, { "aria-label": userId, ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
|
|
39
41
|
}
|
|
40
42
|
else {
|
|
41
|
-
return (_jsx(Avatar, { ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }));
|
|
43
|
+
return (_jsx(Avatar, { "aria-label": t('unknown'), ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }));
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
export default memo(HowlerAvatar);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Delete, Language, Person } from '@mui/icons-material';
|
|
3
|
-
import { Box, Card, IconButton, Stack, Tooltip, Typography } from '@mui/material';
|
|
4
|
-
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
2
|
+
import { Delete, Language, ManageSearch, Person } from '@mui/icons-material';
|
|
3
|
+
import { Box, Card, Chip, Divider, IconButton, Stack, Tooltip, Typography } from '@mui/material';
|
|
5
4
|
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
5
|
+
import { isEmpty } from 'lodash-es';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Link } from 'react-router-dom';
|
|
7
8
|
const DossierCard = ({ dossier, className, onDelete }) => {
|
|
8
|
-
const { t } = useTranslation();
|
|
9
|
-
return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1 }, className: className, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", display: "flex", alignItems: "start", children: [_jsx(Tooltip, { title: t(`route.dossiers.manager.${dossier.type}`), children: dossier.type === 'personal' ? _jsx(Person, { fontSize: "small" }) : _jsx(Language, { fontSize: "small" }) }), _jsx(Box, { component: "span", ml: 1, children: dossier.title })] }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: _jsx("code", { children: dossier.query }) })] }), _jsx(
|
|
9
|
+
const { t, i18n } = useTranslation();
|
|
10
|
+
return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1 }, className: className, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsxs(Stack, { sx: { flex: 1 }, children: [_jsxs(Typography, { variant: "body1", display: "flex", alignItems: "start", children: [_jsx(Tooltip, { title: t(`route.dossiers.manager.${dossier.type}`), children: dossier.type === 'personal' ? _jsx(Person, { fontSize: "small" }) : _jsx(Language, { fontSize: "small" }) }), _jsx(Box, { component: "span", ml: 1, children: dossier.title })] }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: _jsx("code", { children: dossier.query }) }), _jsxs(Stack, { spacing: 1, direction: "row", sx: { mt: 1 }, children: [dossier.leads?.map((lead, index) => (_jsx(Chip, { clickable: true, label: `${lead.label?.[i18n.language] ?? t('unknown')} (${lead.format})`, size: "small", component: Link, to: `/dossiers/${dossier.dossier_id}/edit?tab=leads&lead=${index}`, onClick: e => e.stopPropagation() }, lead.format + lead.label?.en))), !isEmpty(dossier.leads) && !isEmpty(dossier.pivots) && _jsx(Divider, { flexItem: true, orientation: "vertical" }), dossier.pivots?.map((pivot, index) => (_jsx(Chip, { clickable: true, label: `${pivot.label?.[i18n.language] ?? t('unknown')} (${pivot.format})`, size: "small", component: Link, to: `/dossiers/${dossier.dossier_id}/edit?tab=pivots&pivot=${index}`, onClick: e => e.stopPropagation() }, pivot.format + pivot.label?.en)))] })] }), _jsx(HowlerAvatar, { sx: { height: '28px', width: '28px' }, userId: dossier.owner }), _jsx(Tooltip, { title: t('route.dossiers.manager.openinsearch'), children: _jsx(IconButton, { component: Link, to: `/search?query=${dossier.query}`, onClick: e => e.stopPropagation(), children: _jsx(ManageSearch, {}) }) }), onDelete && (_jsx(Tooltip, { title: t('route.dossiers.manager.delete'), children: _jsx(IconButton, { onClick: e => onDelete(e, dossier.dossier_id), children: _jsx(Delete, {}) }) }))] }) }, dossier.dossier_id));
|
|
10
11
|
};
|
|
11
12
|
export default DossierCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react/jsx-no-literals */
|
|
3
|
+
/* eslint-disable import/imports-first */
|
|
4
|
+
/// <reference types="vitest" />
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
7
|
+
import { AvatarContext } from '@cccsaurora/howler-ui/components/app/providers/AvatarProvider';
|
|
8
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
9
|
+
import React, { act } from 'react';
|
|
10
|
+
import { I18nextProvider } from 'react-i18next';
|
|
11
|
+
import { createMockDossier } from '@cccsaurora/howler-ui/tests/utils';
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import DossierCard from './DossierCard';
|
|
14
|
+
// Mock react-router-dom
|
|
15
|
+
const mockNavigate = vi.hoisted(() => vi.fn());
|
|
16
|
+
vi.mock('react-router-dom', async () => {
|
|
17
|
+
const actual = await vi.importActual('react-router-dom');
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
useNavigate: () => mockNavigate,
|
|
21
|
+
Link: React.forwardRef(({ to, children, ...props }, ref) => (_jsx("a", { ref: ref, "data-to": to, ...props, children: children })))
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
25
|
+
const mockAvatarContext = {
|
|
26
|
+
getAvatar: vi.fn(userId => Promise.resolve('https://images.example.com/' + userId))
|
|
27
|
+
};
|
|
28
|
+
// Test wrapper
|
|
29
|
+
const Wrapper = ({ children }) => {
|
|
30
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(AvatarContext.Provider, { value: mockAvatarContext, children: children }) }));
|
|
31
|
+
};
|
|
32
|
+
describe('DossierCard', () => {
|
|
33
|
+
let user;
|
|
34
|
+
let mockOnDelete;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
user = userEvent.setup();
|
|
37
|
+
mockOnDelete = vi.fn();
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
describe('Rendering Conditions', () => {
|
|
41
|
+
it('should render with required props only', async () => {
|
|
42
|
+
const dossier = createMockDossier();
|
|
43
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
expect(screen.getByText('Test Dossier')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should render with all props', async () => {
|
|
49
|
+
const dossier = createMockDossier();
|
|
50
|
+
render(_jsx(DossierCard, { dossier: dossier, className: "custom-class", onDelete: mockOnDelete }), {
|
|
51
|
+
wrapper: Wrapper
|
|
52
|
+
});
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(screen.getByText('Test Dossier')).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('should apply custom className when provided', async () => {
|
|
58
|
+
const dossier = createMockDossier();
|
|
59
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier, className: "custom-class" }), { wrapper: Wrapper });
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
const card = container.querySelector('.custom-class');
|
|
62
|
+
expect(card).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('should render without className when not provided', async () => {
|
|
66
|
+
const dossier = createMockDossier();
|
|
67
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(container.querySelector('.MuiCard-root')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('UI Element Display', () => {
|
|
74
|
+
it('should display dossier title', async () => {
|
|
75
|
+
const dossier = createMockDossier({ title: 'My Custom Dossier' });
|
|
76
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(screen.getByText('My Custom Dossier')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('should display dossier query in code block', async () => {
|
|
82
|
+
const dossier = createMockDossier({ query: 'howler.id:*' });
|
|
83
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
const codeElement = screen.getByText('howler.id:*');
|
|
86
|
+
expect(codeElement).toBeInTheDocument();
|
|
87
|
+
expect(codeElement.tagName).toBe('CODE');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it('should display Person icon for personal dossier type', async () => {
|
|
91
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
92
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
// Check for Person icon via MUI's data-testid or by checking the tooltip
|
|
95
|
+
const tooltip = screen.getByLabelText(/personal/i);
|
|
96
|
+
expect(tooltip).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('should display Language icon for global dossier type', async () => {
|
|
100
|
+
const dossier = createMockDossier({ type: 'global' });
|
|
101
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
// Check for Language icon via tooltip
|
|
104
|
+
const tooltip = screen.getByLabelText(/global/i);
|
|
105
|
+
expect(tooltip).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it('should display HowlerAvatar with correct userId', async () => {
|
|
109
|
+
const dossier = createMockDossier({ owner: 'john.doe' });
|
|
110
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(screen.getByLabelText('john.doe')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it('should display delete button when onDelete is provided', async () => {
|
|
116
|
+
const dossier = createMockDossier();
|
|
117
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
const deleteButton = screen.getByRole('button');
|
|
120
|
+
expect(deleteButton).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('should not display delete button when onDelete is not provided', async () => {
|
|
124
|
+
const dossier = createMockDossier();
|
|
125
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
it('should have tooltip on type icon', async () => {
|
|
131
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
132
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
it('should have tooltip on delete button', async () => {
|
|
138
|
+
const dossier = createMockDossier();
|
|
139
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
it('should display "Open in Search" button', async () => {
|
|
145
|
+
const dossier = createMockDossier();
|
|
146
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(screen.getByLabelText(/open in search/i)).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should have correct href for "Open in Search" button', async () => {
|
|
152
|
+
const dossier = createMockDossier({ query: 'howler.status:open' });
|
|
153
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
const openButton = screen.getByLabelText(/open in search/i);
|
|
156
|
+
expect(openButton).toHaveAttribute('data-to', '/search?query=howler.status:open');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('Button States & Interactions', () => {
|
|
161
|
+
it('should call onDelete with correct parameters when delete button is clicked', async () => {
|
|
162
|
+
const dossier = createMockDossier({ dossier_id: 'my-dossier-123' });
|
|
163
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
const deleteButton = screen.getByRole('button');
|
|
168
|
+
await user.click(deleteButton);
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(mockOnDelete).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(mockOnDelete).toHaveBeenCalledWith(expect.any(Object), 'my-dossier-123');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
it('should call onDelete with event object', async () => {
|
|
175
|
+
const dossier = createMockDossier();
|
|
176
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
const deleteButton = screen.getByRole('button');
|
|
181
|
+
await user.click(deleteButton);
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
const eventArg = mockOnDelete.mock.calls[0][0];
|
|
184
|
+
expect(eventArg).toBeDefined();
|
|
185
|
+
expect(eventArg.type).toBe('click');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('should not crash when delete button is clicked multiple times', async () => {
|
|
189
|
+
const dossier = createMockDossier();
|
|
190
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
const deleteButton = screen.getByRole('button');
|
|
195
|
+
await user.click(deleteButton);
|
|
196
|
+
await user.click(deleteButton);
|
|
197
|
+
await user.click(deleteButton);
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(mockOnDelete).toHaveBeenCalledTimes(3);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('should stop event propagation when Open in Search button is clicked', async () => {
|
|
203
|
+
const dossier = createMockDossier();
|
|
204
|
+
const mockParentClick = vi.fn();
|
|
205
|
+
render(_jsx("div", { onClick: mockParentClick, children: _jsx(DossierCard, { dossier: dossier }) }), { wrapper: Wrapper });
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.getByLabelText(/open in search/i)).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
const openButton = screen.getByLabelText(/open in search/i);
|
|
210
|
+
await user.click(openButton);
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(mockParentClick).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it('should stop event propagation when lead chip is clicked', async () => {
|
|
216
|
+
const dossier = createMockDossier({
|
|
217
|
+
leads: [{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } }]
|
|
218
|
+
});
|
|
219
|
+
const mockParentClick = vi.fn();
|
|
220
|
+
render(_jsx("div", { onClick: mockParentClick, children: _jsx(DossierCard, { dossier: dossier }) }), { wrapper: Wrapper });
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(screen.getByText(/Lead 1/)).toBeInTheDocument();
|
|
223
|
+
});
|
|
224
|
+
const chip = screen.getByText(/Lead 1/);
|
|
225
|
+
await user.click(chip);
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(mockParentClick).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
it('should stop event propagation when pivot chip is clicked', async () => {
|
|
231
|
+
const dossier = createMockDossier({
|
|
232
|
+
pivots: [{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } }]
|
|
233
|
+
});
|
|
234
|
+
const mockParentClick = vi.fn();
|
|
235
|
+
render(_jsx("div", { onClick: mockParentClick, children: _jsx(DossierCard, { dossier: dossier }) }), { wrapper: Wrapper });
|
|
236
|
+
await waitFor(() => {
|
|
237
|
+
expect(screen.getByText(/Pivot 1/)).toBeInTheDocument();
|
|
238
|
+
});
|
|
239
|
+
const chip = screen.getByText(/Pivot 1/);
|
|
240
|
+
await user.click(chip);
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(mockParentClick).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('Edge Cases', () => {
|
|
247
|
+
it('should handle empty title', async () => {
|
|
248
|
+
const dossier = createMockDossier({ title: '' });
|
|
249
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(container).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should handle empty query', async () => {
|
|
255
|
+
const dossier = createMockDossier({ query: '' });
|
|
256
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
const codeElement = container.querySelector('code');
|
|
259
|
+
expect(codeElement).toBeEmptyDOMElement();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
it('should handle very long title', async () => {
|
|
263
|
+
const longTitle = 'A'.repeat(200);
|
|
264
|
+
const dossier = createMockDossier({ title: longTitle });
|
|
265
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
expect(screen.getByText(longTitle)).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
it('should handle very long query', async () => {
|
|
271
|
+
const longQuery = 'howler.id:*' + ' AND howler.status:open'.repeat(50);
|
|
272
|
+
const dossier = createMockDossier({ query: longQuery });
|
|
273
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(screen.getByText(longQuery)).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
it('should handle special characters in title', async () => {
|
|
279
|
+
const dossier = createMockDossier({ title: '<script>alert("xss")</script>' });
|
|
280
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.getByText('<script>alert("xss")</script>')).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
it('should handle special characters in query', async () => {
|
|
286
|
+
const dossier = createMockDossier({ query: 'howler.id:*&query=<test>' });
|
|
287
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(screen.getByText('howler.id:*&query=<test>')).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
it('should handle undefined owner', async () => {
|
|
293
|
+
const dossier = createMockDossier({ owner: undefined });
|
|
294
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
295
|
+
await waitFor(() => {
|
|
296
|
+
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
it('should handle null owner', async () => {
|
|
300
|
+
const dossier = createMockDossier({ owner: null });
|
|
301
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
describe('Leads and Pivots Display', () => {
|
|
308
|
+
it('should display lead chips when leads are present', async () => {
|
|
309
|
+
const dossier = createMockDossier({
|
|
310
|
+
leads: [
|
|
311
|
+
{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } },
|
|
312
|
+
{ format: 'lead2', label: { en: 'Lead 2', fr: 'Piste 2' } }
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(screen.getByText(/Lead 1/)).toBeInTheDocument();
|
|
318
|
+
expect(screen.getByText(/Lead 2/)).toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
it('should display pivot chips when pivots are present', async () => {
|
|
322
|
+
const dossier = createMockDossier({
|
|
323
|
+
pivots: [
|
|
324
|
+
{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } },
|
|
325
|
+
{ format: 'pivot2', label: { en: 'Pivot 2', fr: 'Pivot 2' } }
|
|
326
|
+
]
|
|
327
|
+
});
|
|
328
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
expect(screen.getByText(/Pivot 1/)).toBeInTheDocument();
|
|
331
|
+
expect(screen.getByText(/Pivot 2/)).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
it('should not display chips when leads and pivots are empty', async () => {
|
|
335
|
+
const dossier = createMockDossier({
|
|
336
|
+
leads: [],
|
|
337
|
+
pivots: []
|
|
338
|
+
});
|
|
339
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
expect(container.querySelectorAll('.MuiChip-root')).toHaveLength(0);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
it('should display divider when both leads and pivots are present', async () => {
|
|
345
|
+
const dossier = createMockDossier({
|
|
346
|
+
leads: [{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } }],
|
|
347
|
+
pivots: [{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } }]
|
|
348
|
+
});
|
|
349
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
350
|
+
await waitFor(() => {
|
|
351
|
+
expect(container.querySelector('.MuiDivider-root')).toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
it('should not display divider when only leads are present', async () => {
|
|
355
|
+
const dossier = createMockDossier({
|
|
356
|
+
leads: [{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } }],
|
|
357
|
+
pivots: []
|
|
358
|
+
});
|
|
359
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(container.querySelector('.MuiDivider-root')).not.toBeInTheDocument();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
it('should not display divider when only pivots are present', async () => {
|
|
365
|
+
const dossier = createMockDossier({
|
|
366
|
+
leads: [],
|
|
367
|
+
pivots: [{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } }]
|
|
368
|
+
});
|
|
369
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(container.querySelector('.MuiDivider-root')).not.toBeInTheDocument();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
it('should have correct link for lead chips', async () => {
|
|
375
|
+
const dossier = createMockDossier({
|
|
376
|
+
dossier_id: 'dossier-123',
|
|
377
|
+
leads: [{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } }]
|
|
378
|
+
});
|
|
379
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
const chip = screen.getByText(/Lead 1/).closest('a');
|
|
382
|
+
expect(chip).toHaveAttribute('data-to', '/dossiers/dossier-123/edit?tab=leads&lead=0');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
it('should have correct link for pivot chips', async () => {
|
|
386
|
+
const dossier = createMockDossier({
|
|
387
|
+
dossier_id: 'dossier-456',
|
|
388
|
+
pivots: [{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } }]
|
|
389
|
+
});
|
|
390
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
391
|
+
await waitFor(() => {
|
|
392
|
+
const chip = screen.getByText(/Pivot 1/).closest('a');
|
|
393
|
+
expect(chip).toHaveAttribute('data-to', '/dossiers/dossier-456/edit?tab=pivots&pivot=0');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
it('should display lead format in chip label', async () => {
|
|
397
|
+
const dossier = createMockDossier({
|
|
398
|
+
leads: [{ format: 'custom-format', label: { en: 'Custom Lead', fr: 'Piste personnalisée' } }]
|
|
399
|
+
});
|
|
400
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect(screen.getByText(/Custom Lead \(custom-format\)/)).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
it('should display pivot format in chip label', async () => {
|
|
406
|
+
const dossier = createMockDossier({
|
|
407
|
+
pivots: [{ format: 'custom-pivot', label: { en: 'Custom Pivot', fr: 'Pivot personnalisé' } }]
|
|
408
|
+
});
|
|
409
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
410
|
+
await waitFor(() => {
|
|
411
|
+
expect(screen.getByText(/Custom Pivot \(custom-pivot\)/)).toBeInTheDocument();
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
describe('Dossier Types', () => {
|
|
416
|
+
it('should render correctly for personal type', async () => {
|
|
417
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
418
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
419
|
+
await waitFor(() => {
|
|
420
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
it('should render correctly for global type', async () => {
|
|
424
|
+
const dossier = createMockDossier({ type: 'global' });
|
|
425
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
426
|
+
await waitFor(() => {
|
|
427
|
+
expect(screen.getByLabelText(/global/i)).toBeInTheDocument();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
it('should handle switching between types', async () => {
|
|
431
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ type: 'personal' }) }), {
|
|
432
|
+
wrapper: Wrapper
|
|
433
|
+
});
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
436
|
+
});
|
|
437
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ type: 'global' }) }));
|
|
438
|
+
await waitFor(() => {
|
|
439
|
+
expect(screen.getByLabelText(/global/i)).toBeInTheDocument();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
describe('Integration Tests', () => {
|
|
444
|
+
it('should render complete card with all elements', async () => {
|
|
445
|
+
const dossier = createMockDossier({
|
|
446
|
+
dossier_id: 'full-test',
|
|
447
|
+
title: 'Complete Dossier',
|
|
448
|
+
query: 'howler.status:open AND howler.assigned:me',
|
|
449
|
+
type: 'personal',
|
|
450
|
+
owner: 'admin',
|
|
451
|
+
leads: [{ format: 'lead1', label: { en: 'Lead 1', fr: 'Piste 1' } }],
|
|
452
|
+
pivots: [{ format: 'pivot1', label: { en: 'Pivot 1', fr: 'Pivot 1' } }]
|
|
453
|
+
});
|
|
454
|
+
render(_jsx(DossierCard, { dossier: dossier, className: "test-class", onDelete: mockOnDelete }), {
|
|
455
|
+
wrapper: Wrapper
|
|
456
|
+
});
|
|
457
|
+
await waitFor(() => {
|
|
458
|
+
// Check all elements are present
|
|
459
|
+
expect(screen.getByText('Complete Dossier')).toBeInTheDocument();
|
|
460
|
+
expect(screen.getByText('howler.status:open AND howler.assigned:me')).toBeInTheDocument();
|
|
461
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
462
|
+
expect(screen.getByLabelText('admin')).toBeInTheDocument();
|
|
463
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
464
|
+
expect(screen.getByText(/Lead 1/)).toBeInTheDocument();
|
|
465
|
+
expect(screen.getByText(/Pivot 1/)).toBeInTheDocument();
|
|
466
|
+
expect(screen.getByLabelText(/open in search/i)).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
it('should handle multiple dossier cards with different data', async () => {
|
|
470
|
+
const dossiers = [
|
|
471
|
+
createMockDossier({ dossier_id: 'dossier-1', title: 'Dossier 1', type: 'personal' }),
|
|
472
|
+
createMockDossier({ dossier_id: 'dossier-2', title: 'Dossier 2', type: 'global' }),
|
|
473
|
+
createMockDossier({ dossier_id: 'dossier-3', title: 'Dossier 3', type: 'personal' })
|
|
474
|
+
];
|
|
475
|
+
render(_jsx(Wrapper, { children: dossiers.map(dossier => (_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }, dossier.dossier_id))) }));
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(screen.getByText('Dossier 1')).toBeInTheDocument();
|
|
478
|
+
expect(screen.getByText('Dossier 2')).toBeInTheDocument();
|
|
479
|
+
expect(screen.getByText('Dossier 3')).toBeInTheDocument();
|
|
480
|
+
expect(screen.getAllByRole('button')).toHaveLength(3);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
it('should work with different owners', async () => {
|
|
484
|
+
const owners = ['user1', 'user2', 'admin'];
|
|
485
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ owner: owners[0] }) }), {
|
|
486
|
+
wrapper: Wrapper
|
|
487
|
+
});
|
|
488
|
+
for (const owner of owners) {
|
|
489
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ owner }) }));
|
|
490
|
+
await waitFor(() => {
|
|
491
|
+
expect(screen.getByLabelText(owner)).toBeInTheDocument();
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
describe('Accessibility', () => {
|
|
497
|
+
it('should have accessible delete button', async () => {
|
|
498
|
+
const dossier = createMockDossier();
|
|
499
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
500
|
+
await waitFor(() => {
|
|
501
|
+
const deleteButton = screen.getByRole('button');
|
|
502
|
+
expect(deleteButton).toBeInTheDocument();
|
|
503
|
+
expect(deleteButton).toHaveAccessibleName();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
it('should have tooltips for icons', async () => {
|
|
507
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
508
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
509
|
+
await waitFor(() => {
|
|
510
|
+
// Type icon tooltip
|
|
511
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
512
|
+
// Delete button tooltip
|
|
513
|
+
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
514
|
+
// Open in Search button tooltip
|
|
515
|
+
expect(screen.getByLabelText(/open in search/i)).toBeInTheDocument();
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
it('should have proper semantic HTML structure', async () => {
|
|
519
|
+
const dossier = createMockDossier();
|
|
520
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
521
|
+
await waitFor(() => {
|
|
522
|
+
// Should have a card
|
|
523
|
+
expect(container.querySelector('.MuiCard-root')).toBeInTheDocument();
|
|
524
|
+
// Should have code element for query
|
|
525
|
+
expect(container.querySelector('code')).toBeInTheDocument();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
it('should maintain focus on delete button', async () => {
|
|
529
|
+
const dossier = createMockDossier();
|
|
530
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
531
|
+
await waitFor(() => {
|
|
532
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
533
|
+
});
|
|
534
|
+
const deleteButton = screen.getByRole('button');
|
|
535
|
+
act(() => {
|
|
536
|
+
deleteButton.focus();
|
|
537
|
+
});
|
|
538
|
+
await waitFor(() => {
|
|
539
|
+
expect(deleteButton).toHaveFocus();
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
describe('Prop Changes', () => {
|
|
544
|
+
it('should update when dossier prop changes', async () => {
|
|
545
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ title: 'Original' }) }), {
|
|
546
|
+
wrapper: Wrapper
|
|
547
|
+
});
|
|
548
|
+
await waitFor(() => {
|
|
549
|
+
expect(screen.getByText('Original')).toBeInTheDocument();
|
|
550
|
+
});
|
|
551
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ title: 'Updated' }) }));
|
|
552
|
+
await waitFor(() => {
|
|
553
|
+
expect(screen.getByText('Updated')).toBeInTheDocument();
|
|
554
|
+
expect(screen.queryByText('Original')).not.toBeInTheDocument();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
it('should update when onDelete prop is added', async () => {
|
|
558
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier() }), { wrapper: Wrapper });
|
|
559
|
+
await waitFor(() => {
|
|
560
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
561
|
+
});
|
|
562
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier(), onDelete: mockOnDelete }));
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
it('should update when onDelete prop is removed', async () => {
|
|
568
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier(), onDelete: mockOnDelete }), {
|
|
569
|
+
wrapper: Wrapper
|
|
570
|
+
});
|
|
571
|
+
await waitFor(() => {
|
|
572
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
573
|
+
});
|
|
574
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier() }));
|
|
575
|
+
await waitFor(() => {
|
|
576
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
it('should update when className changes', async () => {
|
|
580
|
+
const { container, rerender } = render(_jsx(DossierCard, { dossier: createMockDossier(), className: "class-1" }), {
|
|
581
|
+
wrapper: Wrapper
|
|
582
|
+
});
|
|
583
|
+
await waitFor(() => {
|
|
584
|
+
expect(container.querySelector('.class-1')).toBeInTheDocument();
|
|
585
|
+
});
|
|
586
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier(), className: "class-2" }));
|
|
587
|
+
await waitFor(() => {
|
|
588
|
+
expect(container.querySelector('.class-2')).toBeInTheDocument();
|
|
589
|
+
expect(container.querySelector('.class-1')).not.toBeInTheDocument();
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
});
|
|
@@ -9,7 +9,7 @@ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
|
9
9
|
import { isEqual, omit, uniqBy } from 'lodash-es';
|
|
10
10
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
12
|
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
13
13
|
import { useContextSelector } from 'use-context-selector';
|
|
14
14
|
import QueryResultText from '../../elements/display/QueryResultText';
|
|
15
15
|
import HitQuery from '../hits/search/HitQuery';
|
|
@@ -20,6 +20,7 @@ const DossierEditor = () => {
|
|
|
20
20
|
const params = useParams();
|
|
21
21
|
const { dispatchApi } = useMyApi();
|
|
22
22
|
const navigate = useNavigate();
|
|
23
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
23
24
|
const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
|
|
24
25
|
const isNarrow = useMediaQuery(`(max-width: ${i18n.language === 'en' ? 1275 : 1375}px)`);
|
|
25
26
|
const [originalDossier, setOriginalDossier] = useState();
|
|
@@ -28,7 +29,7 @@ const DossierEditor = () => {
|
|
|
28
29
|
leads: [],
|
|
29
30
|
pivots: []
|
|
30
31
|
});
|
|
31
|
-
const [tab, setTab] = useState('leads');
|
|
32
|
+
const [tab, setTab] = useState(searchParams.get('tab') ?? 'leads');
|
|
32
33
|
const [searchTotal, setSearchTotal] = useState(-1);
|
|
33
34
|
const [searchDirty, setSearchDirty] = useState(false);
|
|
34
35
|
const [loading, setLoading] = useState(false);
|
|
@@ -164,6 +165,13 @@ const DossierEditor = () => {
|
|
|
164
165
|
}
|
|
165
166
|
})();
|
|
166
167
|
}, [dispatchApi, dossier.query, setQuery]);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (searchParams.get('tab') !== tab) {
|
|
170
|
+
searchParams.set('tab', tab);
|
|
171
|
+
}
|
|
172
|
+
setSearchParams(searchParams, { replace: true });
|
|
173
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
+
}, [setSearchParams, tab]);
|
|
167
175
|
return (_jsx(PageCenter, { maxWidth: "1000px", width: "100%", textAlign: "left", height: "97%", children: _jsxs(Box, { position: "relative", height: "100%", children: [_jsx(Tooltip, { title: validationError, children: _jsx("span", { children: _jsxs(Fab, { variant: "extended", size: "large", color: "primary", disabled: !dirty || !!validationError || loading, sx: theme => ({
|
|
168
176
|
textTransform: 'none',
|
|
169
177
|
position: 'absolute',
|
|
@@ -29,6 +29,7 @@ vi.mock('react-router-dom', async () => {
|
|
|
29
29
|
return {
|
|
30
30
|
...actual,
|
|
31
31
|
useParams: vi.fn(),
|
|
32
|
+
useSearchParams: vi.fn(() => [new URLSearchParams(), () => { }]),
|
|
32
33
|
useNavigate: () => vi.fn()
|
|
33
34
|
};
|
|
34
35
|
});
|
|
@@ -93,9 +94,10 @@ vi.mock('../hits/search/HitQuery', () => ({
|
|
|
93
94
|
import ApiConfigProvider from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
94
95
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
95
96
|
import { I18nextProvider } from 'react-i18next';
|
|
96
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
97
|
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
97
98
|
import DossierEditor from './DossierEditor';
|
|
98
99
|
const mockUseParams = vi.mocked(useParams);
|
|
100
|
+
const mockUseSearchParams = vi.mocked(useSearchParams);
|
|
99
101
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
100
102
|
const mockNavigate = vi.mocked(useNavigate());
|
|
101
103
|
// Mock data
|
|
@@ -357,5 +359,125 @@ describe('DossierEditor', () => {
|
|
|
357
359
|
expect(saveButton).toBeDisabled();
|
|
358
360
|
});
|
|
359
361
|
});
|
|
362
|
+
describe('URL parameter synchronization', () => {
|
|
363
|
+
it('should initialize tab from URL search params', async () => {
|
|
364
|
+
const searchParams = new URLSearchParams('tab=pivots');
|
|
365
|
+
const mockSetSearchParams = vi.fn();
|
|
366
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
367
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
368
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
371
|
+
expect(pivotsTab).toHaveAttribute('aria-selected', 'true');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
it('should default to leads tab when no tab param in URL', async () => {
|
|
375
|
+
const searchParams = new URLSearchParams();
|
|
376
|
+
const mockSetSearchParams = vi.fn();
|
|
377
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
378
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
379
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
const leadsTab = screen.getByRole('tab', { name: /leads/i });
|
|
382
|
+
expect(leadsTab).toHaveAttribute('aria-selected', 'true');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
it('should update URL params when tab is changed', async () => {
|
|
386
|
+
const user = userEvent.setup();
|
|
387
|
+
const searchParams = new URLSearchParams();
|
|
388
|
+
const mockSetSearchParams = vi.fn();
|
|
389
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
390
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
391
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
expect(screen.getByRole('tab', { name: /pivots/i })).toBeInTheDocument();
|
|
394
|
+
});
|
|
395
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
396
|
+
await user.click(pivotsTab);
|
|
397
|
+
await waitFor(() => {
|
|
398
|
+
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
399
|
+
const callArgs = mockSetSearchParams.mock.calls[mockSetSearchParams.mock.calls.length - 1];
|
|
400
|
+
const updatedParams = callArgs[0];
|
|
401
|
+
expect(updatedParams.get('tab')).toBe('pivots');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
it('should update search params with replace option', async () => {
|
|
405
|
+
const user = userEvent.setup();
|
|
406
|
+
const searchParams = new URLSearchParams();
|
|
407
|
+
const mockSetSearchParams = vi.fn();
|
|
408
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
409
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
410
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
411
|
+
await waitFor(() => {
|
|
412
|
+
expect(screen.getByRole('tab', { name: /pivots/i })).toBeInTheDocument();
|
|
413
|
+
});
|
|
414
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
415
|
+
await user.click(pivotsTab);
|
|
416
|
+
await waitFor(() => {
|
|
417
|
+
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
418
|
+
const callArgs = mockSetSearchParams.mock.calls[mockSetSearchParams.mock.calls.length - 1];
|
|
419
|
+
const options = callArgs[1];
|
|
420
|
+
expect(options).toEqual({ replace: true });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
it('should set tab param when switching from leads to pivots', async () => {
|
|
424
|
+
const user = userEvent.setup();
|
|
425
|
+
const searchParams = new URLSearchParams('tab=leads');
|
|
426
|
+
const mockSetSearchParams = vi.fn();
|
|
427
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
428
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
429
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(screen.getByRole('tab', { name: /leads/i })).toHaveAttribute('aria-selected', 'true');
|
|
432
|
+
});
|
|
433
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
434
|
+
await user.click(pivotsTab);
|
|
435
|
+
await waitFor(() => {
|
|
436
|
+
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
437
|
+
const callArgs = mockSetSearchParams.mock.calls[mockSetSearchParams.mock.calls.length - 1];
|
|
438
|
+
const updatedParams = callArgs[0];
|
|
439
|
+
expect(updatedParams.get('tab')).toBe('pivots');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
it('should set tab param when switching from pivots to leads', async () => {
|
|
443
|
+
const user = userEvent.setup();
|
|
444
|
+
const searchParams = new URLSearchParams('tab=pivots');
|
|
445
|
+
const mockSetSearchParams = vi.fn();
|
|
446
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
447
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
448
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
449
|
+
await waitFor(() => {
|
|
450
|
+
expect(screen.getByRole('tab', { name: /pivots/i })).toHaveAttribute('aria-selected', 'true');
|
|
451
|
+
});
|
|
452
|
+
const leadsTab = screen.getByRole('tab', { name: /leads/i });
|
|
453
|
+
await user.click(leadsTab);
|
|
454
|
+
await waitFor(() => {
|
|
455
|
+
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
456
|
+
const callArgs = mockSetSearchParams.mock.calls[mockSetSearchParams.mock.calls.length - 1];
|
|
457
|
+
const updatedParams = callArgs[0];
|
|
458
|
+
expect(updatedParams.get('tab')).toBe('leads');
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
it('should preserve existing URL params when changing tabs', async () => {
|
|
462
|
+
const user = userEvent.setup();
|
|
463
|
+
const searchParams = new URLSearchParams('tab=leads&other=value');
|
|
464
|
+
const mockSetSearchParams = vi.fn();
|
|
465
|
+
mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
|
|
466
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
467
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
468
|
+
await waitFor(() => {
|
|
469
|
+
expect(screen.getByRole('tab', { name: /pivots/i })).toBeInTheDocument();
|
|
470
|
+
});
|
|
471
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
472
|
+
await user.click(pivotsTab);
|
|
473
|
+
await waitFor(() => {
|
|
474
|
+
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
475
|
+
const callArgs = mockSetSearchParams.mock.calls[mockSetSearchParams.mock.calls.length - 1];
|
|
476
|
+
const updatedParams = callArgs[0];
|
|
477
|
+
expect(updatedParams.get('tab')).toBe('pivots');
|
|
478
|
+
expect(updatedParams.get('other')).toBe('value');
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|
|
360
482
|
});
|
|
361
483
|
});
|
|
@@ -4,12 +4,22 @@ import { Add } from '@mui/icons-material';
|
|
|
4
4
|
import { Alert, Button, Paper, Stack, Tab, Tabs } from '@mui/material';
|
|
5
5
|
import isNull from 'lodash-es/isNull';
|
|
6
6
|
import merge from 'lodash-es/merge';
|
|
7
|
-
import { useState } from 'react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { useSearchParams } from 'react-router-dom';
|
|
9
10
|
import LeadEditor from './LeadEditor';
|
|
10
11
|
const LeadForm = ({ dossier, setDossier, loading }) => {
|
|
11
12
|
const { t, i18n } = useTranslation();
|
|
12
|
-
const [
|
|
13
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
14
|
+
const [tab, setTab] = useState(parseInt(searchParams.get('lead') ?? '0'));
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
searchParams.delete('pivot');
|
|
17
|
+
if (searchParams.get('lead') !== tab.toString()) {
|
|
18
|
+
searchParams.set('lead', tab.toString());
|
|
19
|
+
}
|
|
20
|
+
setSearchParams(searchParams, { replace: true });
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, [setSearchParams, tab]);
|
|
13
23
|
return (_jsxs(Paper, { sx: { p: 1, display: 'flex', flexDirection: 'column', flex: 1 }, id: "lead-form", children: [_jsxs(Stack, { direction: "row", children: [!dossier?.leads || dossier.leads.length < 1 ? (_jsx(Alert, { id: "create-lead-alert", variant: "outlined", severity: "warning", sx: {
|
|
14
24
|
mr: 1,
|
|
15
25
|
px: 1,
|
|
@@ -33,7 +43,7 @@ const LeadForm = ({ dossier, setDossier, loading }) => {
|
|
|
33
43
|
{ icon: 'material-symbols:add-ad', label: { en: 'New Lead', fr: 'Nouvelle Piste' } }
|
|
34
44
|
]
|
|
35
45
|
}));
|
|
36
|
-
}, children: _jsx(Add, {}) })] }), _jsx(LeadEditor, { lead: (dossier.leads ?? [])[tab], update: data => setDossier(_dossier => ({
|
|
46
|
+
}, disabled: !dossier || loading, children: _jsx(Add, {}) })] }), _jsx(LeadEditor, { lead: (dossier.leads ?? [])[tab], update: data => setDossier(_dossier => ({
|
|
37
47
|
..._dossier,
|
|
38
48
|
leads: (_dossier.leads ?? [])
|
|
39
49
|
.map((lead, index) => {
|
|
@@ -6,9 +6,10 @@ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers
|
|
|
6
6
|
import isNull from 'lodash-es/isNull';
|
|
7
7
|
import merge from 'lodash-es/merge';
|
|
8
8
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
9
|
-
import { Fragment, useCallback, useContext, useMemo, useState } from 'react';
|
|
9
|
+
import { Fragment, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import { usePluginStore } from 'react-pluggable';
|
|
12
|
+
import { useSearchParams } from 'react-router-dom';
|
|
12
13
|
const LinkForm = ({ pivot, update }) => {
|
|
13
14
|
const { t } = useTranslation();
|
|
14
15
|
const { config } = useContext(ApiConfigContext);
|
|
@@ -32,7 +33,8 @@ const PivotForm = ({ dossier, setDossier, loading }) => {
|
|
|
32
33
|
const theme = useTheme();
|
|
33
34
|
const { t, i18n } = useTranslation();
|
|
34
35
|
const pluginStore = usePluginStore();
|
|
35
|
-
const [
|
|
36
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
37
|
+
const [tab, setTab] = useState(parseInt(searchParams.get('pivot') ?? '0'));
|
|
36
38
|
const update = useCallback((data) => setDossier(_dossier => ({
|
|
37
39
|
..._dossier,
|
|
38
40
|
pivots: (_dossier.pivots ?? [])
|
|
@@ -53,6 +55,14 @@ const PivotForm = ({ dossier, setDossier, loading }) => {
|
|
|
53
55
|
})), [setDossier, tab]);
|
|
54
56
|
const pivot = useMemo(() => dossier.pivots?.[tab] ?? null, [dossier.pivots, tab]);
|
|
55
57
|
const icon = useMemo(() => pivot?.icon ?? 'material-symbols:find-in-page', [pivot?.icon]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
searchParams.delete('lead');
|
|
60
|
+
if (searchParams.get('pivot') !== tab.toString()) {
|
|
61
|
+
searchParams.set('pivot', tab.toString());
|
|
62
|
+
}
|
|
63
|
+
setSearchParams(searchParams, { replace: true });
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [setSearchParams, tab]);
|
|
56
66
|
return (_jsx(Paper, { sx: { p: 1, display: 'flex', flexDirection: 'column', flex: 1 }, id: "pivot-form", children: _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { direction: "row", children: [!dossier?.pivots || dossier.pivots.length < 1 ? (_jsx(Alert, { variant: "outlined", severity: "warning", sx: {
|
|
57
67
|
mr: 1,
|
|
58
68
|
px: 1,
|
|
@@ -76,7 +86,7 @@ const PivotForm = ({ dossier, setDossier, loading }) => {
|
|
|
76
86
|
{ icon: 'material-symbols:add-ad', label: { en: 'New Pivot', fr: 'Nouvelle pivot' } }
|
|
77
87
|
]
|
|
78
88
|
}));
|
|
79
|
-
}, children: _jsx(Add, {}) })] }), _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", position: "relative", children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.icon'), value: icon, disabled: !pivot, fullWidth: true, error: !iconExists(icon), sx: { '& input': { paddingLeft: '2.25rem' } }, onChange: ev => update({ icon: ev.target.value }) }), _jsx(Icon, { fontSize: "1.75rem", icon: icon, style: { position: 'absolute', left: '0.5rem' } }), _jsx(Button, { variant: "outlined", color: "error", disabled: !pivot, sx: { minWidth: '0 !important', ml: 1 }, onClick: () => update(null), children: _jsx(Delete, {}) })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", sx: { mt: `${theme.spacing(0.5)} !important` }, children: [_jsx(Typography, { color: "text.secondary", children: t('route.dossiers.manager.icon.description') }), _jsx(IconButton, { size: "small", component: "a", href: "https://icon-sets.iconify.design/", children: _jsx(OpenInNew, { fontSize: "small" }) })] }), _jsxs(Stack, { direction: "row", spacing: 2, children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.label.en'), disabled: !pivot, value: pivot?.label?.en ?? '', fullWidth: true, onChange: ev => update({ label: { en: ev.target.value } }) }), _jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.label.fr'), disabled: !pivot, value: pivot?.label?.fr ?? '', fullWidth: true, onChange: ev => update({ label: { fr: ev.target.value } }) })] }), _jsx(Autocomplete, { disabled: !pivot, options: ['link', ...howlerPluginStore.pivotFormats], renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.pivot.format') })), value: pivot?.format ??
|
|
89
|
+
}, disabled: !dossier || loading, children: _jsx(Add, {}) })] }), _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", position: "relative", children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.icon'), value: icon, disabled: !pivot, fullWidth: true, error: !iconExists(icon), sx: { '& input': { paddingLeft: '2.25rem' } }, onChange: ev => update({ icon: ev.target.value }) }), _jsx(Icon, { fontSize: "1.75rem", icon: icon, style: { position: 'absolute', left: '0.5rem' } }), _jsx(Button, { variant: "outlined", color: "error", disabled: !pivot, sx: { minWidth: '0 !important', ml: 1 }, onClick: () => update(null), children: _jsx(Delete, {}) })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", sx: { mt: `${theme.spacing(0.5)} !important` }, children: [_jsx(Typography, { color: "text.secondary", children: t('route.dossiers.manager.icon.description') }), _jsx(IconButton, { size: "small", component: "a", href: "https://icon-sets.iconify.design/", children: _jsx(OpenInNew, { fontSize: "small" }) })] }), _jsxs(Stack, { direction: "row", spacing: 2, children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.label.en'), disabled: !pivot, value: pivot?.label?.en ?? '', fullWidth: true, onChange: ev => update({ label: { en: ev.target.value } }) }), _jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.label.fr'), disabled: !pivot, value: pivot?.label?.fr ?? '', fullWidth: true, onChange: ev => update({ label: { fr: ev.target.value } }) })] }), _jsx(Autocomplete, { disabled: !pivot, options: ['link', ...howlerPluginStore.pivotFormats], renderInput: params => (_jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.pivot.format') })), value: pivot?.format ?? null, onChange: (_ev, format) => update({ format, value: '', mappings: [] }) }), !!pivot?.format &&
|
|
80
90
|
(pivot.format === 'link' ? (_jsx(LinkForm, { pivot: pivot, update: update })) : (pluginStore.executeFunction(`pivot.${pivot.format}.form`, { pivot, update })))] })] }) }));
|
|
81
91
|
};
|
|
82
92
|
export default PivotForm;
|
|
@@ -628,6 +628,7 @@
|
|
|
628
628
|
"route.dossiers.manager.format": "Lead Format",
|
|
629
629
|
"route.dossiers.manager.tabs.leads": "Leads",
|
|
630
630
|
"route.dossiers.manager.tabs.pivots": "Pivots",
|
|
631
|
+
"route.dossiers.manager.openinsearch": "Open in Search",
|
|
631
632
|
"route.dossiers.manager.pivot.create": "You currently have no pivots configured. Press the add button to the right to create a new one.",
|
|
632
633
|
"route.dossiers.manager.pivot.icon": "Pivot Icon",
|
|
633
634
|
"route.dossiers.manager.pivot.label.en": "English Title",
|
|
@@ -629,6 +629,7 @@
|
|
|
629
629
|
"route.dossiers.manager.format": "Format de la piste",
|
|
630
630
|
"route.dossiers.manager.tabs.leads": "Pistes",
|
|
631
631
|
"route.dossiers.manager.tabs.pivots": "Pivots",
|
|
632
|
+
"route.dossiers.manager.openinsearch": "Ouvrir en recherche",
|
|
632
633
|
"route.dossiers.manager.pivot.create": "Vous n'avez actuellement aucun pivot configuré. Appuyez sur le bouton Ajouter à droite pour en créer un nouveau.",
|
|
633
634
|
"route.dossiers.manager.pivot.icon": "Icône du pivot",
|
|
634
635
|
"route.dossiers.manager.pivot.label.en": "English Title",
|
package/package.json
CHANGED
package/tests/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Action } from '@cccsaurora/howler-ui/models/entities/generated/Action';
|
|
2
2
|
import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
|
|
3
|
+
import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
|
|
3
4
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
4
5
|
import type { Template } from '@cccsaurora/howler-ui/models/entities/generated/Template';
|
|
5
6
|
import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
|
|
@@ -11,4 +12,5 @@ export declare const createMockAnalytic: (overrides?: Partial<Analytic>) => Anal
|
|
|
11
12
|
export declare const createMockTemplate: (overrides?: Partial<Template>) => Template;
|
|
12
13
|
export declare const createMockAction: (overrides?: Partial<Action>) => Action;
|
|
13
14
|
export declare const createMockView: (overrides?: Partial<View>) => View;
|
|
15
|
+
export declare const createMockDossier: (overrides?: Partial<Dossier>) => Dossier;
|
|
14
16
|
export {};
|
package/tests/utils.js
CHANGED
|
@@ -52,3 +52,12 @@ export const createMockView = (overrides) => ({
|
|
|
52
52
|
},
|
|
53
53
|
...overrides
|
|
54
54
|
});
|
|
55
|
+
// Helper function to create mock dossiers
|
|
56
|
+
export const createMockDossier = (overrides) => ({
|
|
57
|
+
dossier_id: 'test-dossier-id',
|
|
58
|
+
title: 'Test Dossier',
|
|
59
|
+
query: 'howler.status:open',
|
|
60
|
+
type: 'personal',
|
|
61
|
+
owner: 'test-user',
|
|
62
|
+
...overrides
|
|
63
|
+
});
|