@cccsaurora/howler-ui 2.15.0-dev.335 → 2.15.0-dev.341
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/hit/related/PivotLink.js +25 -11
- package/components/routes/dossiers/DossierCard.js +6 -5
- package/components/routes/dossiers/DossierCard.test.js +185 -2
- 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/components/routes/hits/search/InformationPane.js +7 -0
- package/locales/en/translation.json +1 -0
- package/locales/fr/translation.json +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ErrorOutline } from '@mui/icons-material';
|
|
3
|
+
import { Card, Tooltip } from '@mui/material';
|
|
3
4
|
import Handlebars from 'handlebars';
|
|
4
5
|
import { isEmpty } from 'lodash-es';
|
|
5
|
-
import { useMemo } from 'react';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
6
7
|
import { useTranslation } from 'react-i18next';
|
|
7
8
|
import { usePluginStore } from 'react-pluggable';
|
|
8
9
|
import { flattenDeep } from '@cccsaurora/howler-ui/utils/utils';
|
|
@@ -33,17 +34,30 @@ const PivotLink = ({ pivot, hit, compact = false }) => {
|
|
|
33
34
|
if (href) {
|
|
34
35
|
return _jsx(RelatedLink, { title: pivot.label[i18n.language], href: href, compact: compact, icon: pivot.icon });
|
|
35
36
|
}
|
|
37
|
+
// Hide a relatively useless console error, we'll show a UI component instead
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
const oldError = console.error;
|
|
40
|
+
let pluginPivot = null;
|
|
36
41
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.error = () => { };
|
|
44
|
+
pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
finally {
|
|
43
47
|
// eslint-disable-next-line no-console
|
|
44
|
-
console.
|
|
45
|
-
|
|
48
|
+
console.error = oldError;
|
|
49
|
+
}
|
|
50
|
+
if (pluginPivot) {
|
|
51
|
+
return pluginPivot;
|
|
46
52
|
}
|
|
47
|
-
return _jsx(
|
|
53
|
+
return (_jsx(Card, { variant: "outlined", sx: { display: 'flex', alignItems: 'center', px: 1 }, children: _jsx(Tooltip, { title: _jsxs(_Fragment, { children: [_jsx("span", { children: `Missing Pivot Implementation ${pivot.format}` }), _jsx("code", { children: _jsx("pre", { children: JSON.stringify(pivot, null, 4) }) })] }), slotProps: {
|
|
54
|
+
popper: {
|
|
55
|
+
sx: {
|
|
56
|
+
'& > .MuiTooltip-tooltip': {
|
|
57
|
+
maxWidth: '90vw !important'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, children: _jsx(ErrorOutline, { color: "error" }) }) }));
|
|
48
62
|
};
|
|
49
63
|
export default PivotLink;
|
|
@@ -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;
|
|
@@ -6,11 +6,21 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
6
6
|
import userEvent, {} from '@testing-library/user-event';
|
|
7
7
|
import { AvatarContext } from '@cccsaurora/howler-ui/components/app/providers/AvatarProvider';
|
|
8
8
|
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
9
|
-
import { act } from 'react';
|
|
9
|
+
import React, { act } from 'react';
|
|
10
10
|
import { I18nextProvider } from 'react-i18next';
|
|
11
11
|
import { createMockDossier } from '@cccsaurora/howler-ui/tests/utils';
|
|
12
12
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
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
|
+
});
|
|
14
24
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
25
|
const mockAvatarContext = {
|
|
16
26
|
getAvatar: vi.fn(userId => Promise.resolve('https://images.example.com/' + userId))
|
|
@@ -131,6 +141,21 @@ describe('DossierCard', () => {
|
|
|
131
141
|
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
132
142
|
});
|
|
133
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
|
+
});
|
|
134
159
|
});
|
|
135
160
|
describe('Button States & Interactions', () => {
|
|
136
161
|
it('should call onDelete with correct parameters when delete button is clicked', async () => {
|
|
@@ -174,6 +199,49 @@ describe('DossierCard', () => {
|
|
|
174
199
|
expect(mockOnDelete).toHaveBeenCalledTimes(3);
|
|
175
200
|
});
|
|
176
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
|
+
});
|
|
177
245
|
});
|
|
178
246
|
describe('Edge Cases', () => {
|
|
179
247
|
it('should handle empty title', async () => {
|
|
@@ -236,6 +304,114 @@ describe('DossierCard', () => {
|
|
|
236
304
|
});
|
|
237
305
|
});
|
|
238
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
|
+
});
|
|
239
415
|
describe('Dossier Types', () => {
|
|
240
416
|
it('should render correctly for personal type', async () => {
|
|
241
417
|
const dossier = createMockDossier({ type: 'personal' });
|
|
@@ -271,7 +447,9 @@ describe('DossierCard', () => {
|
|
|
271
447
|
title: 'Complete Dossier',
|
|
272
448
|
query: 'howler.status:open AND howler.assigned:me',
|
|
273
449
|
type: 'personal',
|
|
274
|
-
owner: 'admin'
|
|
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' } }]
|
|
275
453
|
});
|
|
276
454
|
render(_jsx(DossierCard, { dossier: dossier, className: "test-class", onDelete: mockOnDelete }), {
|
|
277
455
|
wrapper: Wrapper
|
|
@@ -283,6 +461,9 @@ describe('DossierCard', () => {
|
|
|
283
461
|
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
284
462
|
expect(screen.getByLabelText('admin')).toBeInTheDocument();
|
|
285
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();
|
|
286
467
|
});
|
|
287
468
|
});
|
|
288
469
|
it('should handle multiple dossier cards with different data', async () => {
|
|
@@ -330,6 +511,8 @@ describe('DossierCard', () => {
|
|
|
330
511
|
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
331
512
|
// Delete button tooltip
|
|
332
513
|
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
514
|
+
// Open in Search button tooltip
|
|
515
|
+
expect(screen.getByLabelText(/open in search/i)).toBeInTheDocument();
|
|
333
516
|
});
|
|
334
517
|
});
|
|
335
518
|
it('should have proper semantic HTML structure', async () => {
|
|
@@ -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;
|
|
@@ -77,6 +77,13 @@ const InformationPane = ({ onClose }) => {
|
|
|
77
77
|
setUserIds(getUserList(hit));
|
|
78
78
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
79
|
}, [getHit, selected]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (selected) {
|
|
82
|
+
setAnalytic(null);
|
|
83
|
+
setDossiers(null);
|
|
84
|
+
setHasOverview(false);
|
|
85
|
+
}
|
|
86
|
+
}, [selected]);
|
|
80
87
|
useEffect(() => {
|
|
81
88
|
if (hit && !analytic) {
|
|
82
89
|
getMatchingAnalytic(hit).then(setAnalytic);
|
|
@@ -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",
|