@cccsaurora/howler-ui 2.15.0-dev.307 → 2.15.0-dev.310
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/app/providers/ApiConfigProvider.d.ts +3 -1
- package/components/app/providers/ApiConfigProvider.js +8 -8
- package/components/elements/ThemedEditor.d.ts +3 -1
- package/components/elements/ThemedEditor.js +2 -2
- package/components/routes/advanced/QueryEditor.d.ts +1 -0
- package/components/routes/advanced/QueryEditor.js +2 -2
- package/components/routes/analytics/widgets/Created.js +14 -8
- package/components/routes/analytics/widgets/Stacked.js +4 -0
- package/components/routes/dossiers/DossierEditor.js +67 -15
- package/components/routes/dossiers/DossierEditor.test.d.ts +1 -0
- package/components/routes/dossiers/DossierEditor.test.js +360 -0
- package/components/routes/dossiers/LeadEditor.js +2 -2
- package/components/routes/dossiers/LeadForm.js +2 -2
- package/components/routes/dossiers/PivotForm.js +2 -2
- package/components/routes/hits/search/HitQuery.js +1 -1
- package/components/routes/home/ViewCard.js +2 -1
- package/locales/en/translation.json +15 -1
- package/locales/fr/translation.json +15 -1
- package/package.json +1 -1
|
@@ -5,5 +5,7 @@ export type ApiConfigContextType = {
|
|
|
5
5
|
setConfig: (config: ApiType) => void;
|
|
6
6
|
};
|
|
7
7
|
export declare const ApiConfigContext: import("react").Context<ApiConfigContextType>;
|
|
8
|
-
declare const ApiConfigProvider: FC<PropsWithChildren
|
|
8
|
+
declare const ApiConfigProvider: FC<PropsWithChildren<{
|
|
9
|
+
defaultConfig?: ApiType;
|
|
10
|
+
}>>;
|
|
9
11
|
export default ApiConfigProvider;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useMemo, useState } from 'react';
|
|
3
3
|
export const ApiConfigContext = createContext(null);
|
|
4
|
-
const ApiConfigProvider = ({ children
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const ApiConfigProvider = ({ children, defaultConfig = {
|
|
5
|
+
indexes: null,
|
|
6
|
+
lookups: null,
|
|
7
|
+
configuration: null,
|
|
8
|
+
c12nDef: null,
|
|
9
|
+
mapping: null
|
|
10
|
+
} }) => {
|
|
11
|
+
const [config, setConfig] = useState(defaultConfig);
|
|
12
12
|
const context = useMemo(() => ({
|
|
13
13
|
config,
|
|
14
14
|
setConfig
|
|
@@ -4,7 +4,7 @@ import { useTheme } from '@mui/material';
|
|
|
4
4
|
import useThemeBuilder from '@cccsaurora/howler-ui/commons/components/utils/hooks/useThemeBuilder';
|
|
5
5
|
import useMyTheme from '@cccsaurora/howler-ui/components/hooks/useMyTheme';
|
|
6
6
|
import { memo, useCallback, useEffect, useMemo } from 'react';
|
|
7
|
-
const ThemedEditor = ({ beforeMount, options = {}, ...otherProps }) => {
|
|
7
|
+
const ThemedEditor = ({ beforeMount, options = {}, id, ...otherProps }) => {
|
|
8
8
|
const myTheme = useMyTheme();
|
|
9
9
|
const themeBuilder = useThemeBuilder(myTheme);
|
|
10
10
|
const theme = useTheme();
|
|
@@ -99,6 +99,6 @@ const ThemedEditor = ({ beforeMount, options = {}, ...otherProps }) => {
|
|
|
99
99
|
},
|
|
100
100
|
...options
|
|
101
101
|
}), [options]);
|
|
102
|
-
return (_jsx(Editor, { ...otherProps, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', beforeMount: _beforeMount, options: _options }));
|
|
102
|
+
return (_jsx(Editor, { ...otherProps, wrapperProps: { id }, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', beforeMount: _beforeMount, options: _options }));
|
|
103
103
|
};
|
|
104
104
|
export default memo(ThemedEditor);
|
|
@@ -12,7 +12,7 @@ import useHistoryCompletionProvider from './historyCompletionProvider';
|
|
|
12
12
|
import useLuceneCompletionProvider from './luceneCompletionProvider';
|
|
13
13
|
import LUCENE_TOKEN_PROVIDER from './luceneTokenProvider';
|
|
14
14
|
import useYamlCompletionProvider from './yamlCompletionProvider';
|
|
15
|
-
const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize = 16, height = '100%', width = '100%', editorOptions = {} }) => {
|
|
15
|
+
const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize = 16, height = '100%', width = '100%', editorOptions = {}, id }) => {
|
|
16
16
|
const theme = useTheme();
|
|
17
17
|
const monaco = useMonaco();
|
|
18
18
|
const { config } = useContext(ApiConfigContext);
|
|
@@ -85,6 +85,6 @@ const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize =
|
|
|
85
85
|
setFzfSearch(!fzfSearch);
|
|
86
86
|
}
|
|
87
87
|
}, [fzfSearch, setFzfSearch]);
|
|
88
|
-
return (_jsx(Box, { sx: { flex: 1 }, onKeyDownCapture: handleKeyPress, children: _jsx(ThemedEditor, { height: height, width: width, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: query, onChange: value => setQuery(value), beforeMount: beforeEditorMount, onMount: onMount, options: options }) }));
|
|
88
|
+
return (_jsx(Box, { sx: { flex: 1 }, onKeyDownCapture: handleKeyPress, children: _jsx(ThemedEditor, { height: height, width: width, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: query, onChange: value => setQuery(value), beforeMount: beforeEditorMount, onMount: onMount, options: options, wrapperProps: { id } }) }));
|
|
89
89
|
};
|
|
90
90
|
export default memo(QueryEditor);
|
|
@@ -3,26 +3,32 @@ import { Skeleton } from '@mui/material';
|
|
|
3
3
|
import api from '@cccsaurora/howler-ui/api';
|
|
4
4
|
import 'chartjs-adapter-dayjs-4';
|
|
5
5
|
import useMyChart from '@cccsaurora/howler-ui/components/hooks/useMyChart';
|
|
6
|
-
import { forwardRef, useEffect, useState } from 'react';
|
|
6
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
7
7
|
import { Line } from 'react-chartjs-2';
|
|
8
8
|
import { stringToColor } from '@cccsaurora/howler-ui/utils/utils';
|
|
9
9
|
const Created = forwardRef(({ analytic }, ref) => {
|
|
10
10
|
const { line } = useMyChart();
|
|
11
11
|
const [loading, setLoading] = useState(false);
|
|
12
12
|
const [ingestionData, setIngestionData] = useState({});
|
|
13
|
+
const queryRef = useRef();
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
if (!analytic) {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
setLoading(true);
|
|
18
|
-
|
|
19
|
-
.post('timestamp', {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
if (!queryRef.current) {
|
|
20
|
+
queryRef.current = api.search.histogram.hit.post('timestamp', {
|
|
21
|
+
query: `howler.analytic:("${analytic.name}")`,
|
|
22
|
+
start: 'now-3M',
|
|
23
|
+
gap: '1d',
|
|
24
|
+
mincount: 0
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
queryRef.current
|
|
28
|
+
.then(result => {
|
|
29
|
+
setIngestionData(result);
|
|
30
|
+
queryRef.current = null;
|
|
24
31
|
})
|
|
25
|
-
.then(setIngestionData)
|
|
26
32
|
.finally(() => setLoading(false));
|
|
27
33
|
}, [analytic]);
|
|
28
34
|
return analytic && !loading ? (_jsx(Line, { ref: ref, options: line('route.analytics.ingestion.title'), data: {
|
|
@@ -11,6 +11,9 @@ const Stacked = forwardRef(({ analytic, field, color }, ref) => {
|
|
|
11
11
|
const [loading, setLoading] = useState(false);
|
|
12
12
|
const [datasets, setDatasets] = useState([]);
|
|
13
13
|
const fetchData = useCallback(async () => {
|
|
14
|
+
if (loading) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
14
17
|
try {
|
|
15
18
|
setLoading(true);
|
|
16
19
|
const result = await api.search.facet.hit.post({
|
|
@@ -44,6 +47,7 @@ const Stacked = forwardRef(({ analytic, field, color }, ref) => {
|
|
|
44
47
|
finally {
|
|
45
48
|
setLoading(false);
|
|
46
49
|
}
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
47
51
|
}, [analytic?.name, color, field]);
|
|
48
52
|
useEffect(() => {
|
|
49
53
|
if (!analytic) {
|
|
@@ -52,23 +52,75 @@ const DossierEditor = () => {
|
|
|
52
52
|
if ((dossier.leads ?? []).length < 1 && (dossier.pivots ?? []).length < 1) {
|
|
53
53
|
return t('route.dossiers.manager.validation.error.items');
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
lead.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
for (const lead of dossier.leads ?? []) {
|
|
56
|
+
if (!lead.label) {
|
|
57
|
+
// You have not configured a lead label.
|
|
58
|
+
return t('route.dossiers.manager.validation.error.leads.label');
|
|
59
|
+
}
|
|
60
|
+
if (!lead.label.en) {
|
|
61
|
+
// You have not configured an english lead label.
|
|
62
|
+
return t('route.dossiers.manager.validation.error.leads.label.en');
|
|
63
|
+
}
|
|
64
|
+
if (!lead.label.fr) {
|
|
65
|
+
// You have not configured a french lead label.
|
|
66
|
+
return t('route.dossiers.manager.validation.error.leads.label.fr');
|
|
67
|
+
}
|
|
68
|
+
if (!lead.format) {
|
|
69
|
+
// You have not set the format for the lead with label <label>
|
|
70
|
+
return t('route.dossiers.manager.validation.error.leads.format', { label: lead.label[i18n.language] });
|
|
71
|
+
}
|
|
72
|
+
if (!lead.content) {
|
|
73
|
+
// You have not set the content for the lead with label <label>
|
|
74
|
+
return t('route.dossiers.manager.validation.error.leads.content', { label: lead.label[i18n.language] });
|
|
75
|
+
}
|
|
76
|
+
if (!lead.icon || !iconExists(lead.icon)) {
|
|
77
|
+
// You are missing an icon, or the specified icon does not exist for lead with label <label>
|
|
78
|
+
return t('route.dossiers.manager.validation.error.leads.icon', { label: lead.label[i18n.language] });
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
for (const pivot of dossier.pivots ?? []) {
|
|
82
|
+
if (!pivot.label) {
|
|
83
|
+
// You have not configured a pivot label.
|
|
84
|
+
return t('route.dossiers.manager.validation.error.pivots.label');
|
|
85
|
+
}
|
|
86
|
+
if (!pivot.label.en) {
|
|
87
|
+
// You have not configured an english pivot label.
|
|
88
|
+
return t('route.dossiers.manager.validation.error.pivots.label.en');
|
|
89
|
+
}
|
|
90
|
+
if (!pivot.label.fr) {
|
|
91
|
+
// You have not configured a french pivot label.
|
|
92
|
+
return t('route.dossiers.manager.validation.error.pivots.label.fr');
|
|
93
|
+
}
|
|
94
|
+
if (!pivot.format) {
|
|
95
|
+
// You have not set the format for the pivot with label <label>
|
|
96
|
+
return t('route.dossiers.manager.validation.error.pivots.format', { label: pivot.label[i18n.language] });
|
|
97
|
+
}
|
|
98
|
+
if (!pivot.value) {
|
|
99
|
+
// You have not set the value for the pivot with label <label>
|
|
100
|
+
return t('route.dossiers.manager.validation.error.pivots.value', { label: pivot.label[i18n.language] });
|
|
101
|
+
}
|
|
102
|
+
if (!pivot.icon || !iconExists(pivot.icon)) {
|
|
103
|
+
// You are missing an icon, or the specified icon does not exist for pivot with label <label>
|
|
104
|
+
return t('route.dossiers.manager.validation.error.pivots.icon', { label: pivot.label[i18n.language] });
|
|
105
|
+
}
|
|
106
|
+
if (!pivot.mappings || pivot.mappings.length < 1) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if ((pivot.mappings ?? []).length !== uniqBy(pivot.mappings ?? [], 'key').length) {
|
|
110
|
+
// You have a duplicate for pivot with label <label>
|
|
111
|
+
return t('route.dossiers.manager.validation.error.pivots.duplicate', { label: pivot.label[i18n.language] });
|
|
112
|
+
}
|
|
113
|
+
if (pivot.mappings?.some(mapping => !mapping.key)) {
|
|
114
|
+
// You have not configured a key for a mapping for pivot with label <label>
|
|
115
|
+
return t('route.dossiers.manager.validation.error.pivots.key', { label: pivot.label[i18n.language] });
|
|
116
|
+
}
|
|
117
|
+
if (pivot.mappings?.some(mapping => !mapping.field || (mapping.field === 'custom' && !mapping.custom_value))) {
|
|
118
|
+
// You have not configured a field or custom value for a mapping for pivot with label <label>
|
|
119
|
+
return t('route.dossiers.manager.validation.error.pivots.field', { label: pivot.label[i18n.language] });
|
|
120
|
+
}
|
|
69
121
|
}
|
|
70
122
|
return null;
|
|
71
|
-
}, [dossier, searchDirty, searchTotal, t]);
|
|
123
|
+
}, [dossier, i18n.language, searchDirty, searchTotal, t]);
|
|
72
124
|
const save = useCallback(async () => {
|
|
73
125
|
setLoading(true);
|
|
74
126
|
try {
|
|
@@ -119,7 +171,7 @@ const DossierEditor = () => {
|
|
|
119
171
|
whiteSpace: 'nowrap',
|
|
120
172
|
pointerEvents: 'initial !important',
|
|
121
173
|
...(isNarrow ? { bottom: theme.spacing(1) } : { top: 0 })
|
|
122
|
-
}), onClick: save, children: [loading ? _jsx(CircularProgress, { size: 24, sx: { mr: 1 } }) : _jsx(Save, { sx: { mr: 1 } }), _jsx(Typography, { children: t('save') })] }) }) }), _jsxs(Stack, { spacing: 1, height: "100%", children: [_jsx(Paper, { sx: { p: 1 }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(TextField, { disabled: !dossier || loading, label: "Title", size: "small", value: dossier.title ?? '', onChange: ev => setDossier(_dossier => ({ ..._dossier, title: ev.target.value })), fullWidth: true }), _jsxs(ToggleButtonGroup, { disabled: !dossier || loading, exclusive: true, value: dossier.type ?? 'global', onChange: (_ev, type) => setDossier(_dossier => ({ ..._dossier, type })), children: [_jsx(Tooltip, { title: t('route.dossiers.manager.global'), children: _jsx(ToggleButton, { value: "global", size: "small", children: _jsx(Language, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('route.dossiers.manager.personal'), children: _jsx(ToggleButton, { value: "personal", size: "small", children: _jsx(Person, { fontSize: "small" }) }) })] })] }), _jsx(Typography, { sx: theme => ({
|
|
174
|
+
}), onClick: save, children: [loading ? _jsx(CircularProgress, { size: 24, sx: { mr: 1 } }) : _jsx(Save, { sx: { mr: 1 } }), _jsx(Typography, { children: t('save') })] }) }) }), _jsxs(Stack, { spacing: 1, height: "100%", children: [_jsx(Paper, { sx: { p: 1 }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(TextField, { id: "dossier-title", disabled: !dossier || loading, label: "Title", size: "small", value: dossier.title ?? '', onChange: ev => setDossier(_dossier => ({ ..._dossier, title: ev.target.value })), fullWidth: true }), _jsxs(ToggleButtonGroup, { disabled: !dossier || loading, exclusive: true, value: dossier.type ?? 'global', onChange: (_ev, type) => setDossier(_dossier => ({ ..._dossier, type })), children: [_jsx(Tooltip, { title: t('route.dossiers.manager.global'), children: _jsx(ToggleButton, { value: "global", size: "small", children: _jsx(Language, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('route.dossiers.manager.personal'), children: _jsx(ToggleButton, { value: "personal", size: "small", children: _jsx(Person, { fontSize: "small" }) }) })] })] }), _jsx(Typography, { sx: theme => ({
|
|
123
175
|
color: theme.palette.text.secondary,
|
|
124
176
|
fontSize: '0.9em',
|
|
125
177
|
fontStyle: 'italic',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable import/imports-first */
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import omit from 'lodash-es/omit';
|
|
6
|
+
// Mock the API
|
|
7
|
+
const mockApiSearchHitPost = vi.fn();
|
|
8
|
+
const mockApiDossierGet = vi.fn();
|
|
9
|
+
const mockApiDossierPost = vi.fn();
|
|
10
|
+
const mockApiDossierPut = vi.fn();
|
|
11
|
+
vi.mock('api', () => ({
|
|
12
|
+
default: {
|
|
13
|
+
search: {
|
|
14
|
+
hit: {
|
|
15
|
+
post: (...args) => mockApiSearchHitPost(...args)
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
dossier: {
|
|
19
|
+
get: (...args) => mockApiDossierGet(...args),
|
|
20
|
+
post: (...args) => mockApiDossierPost(...args),
|
|
21
|
+
put: (...args) => mockApiDossierPut(...args)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
// Mock react-router-dom
|
|
26
|
+
vi.mock('react-router-dom', async () => {
|
|
27
|
+
const actual = await vi.importActual('react-router-dom');
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
useParams: vi.fn(),
|
|
31
|
+
useNavigate: () => vi.fn()
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
// Mock ParameterContext
|
|
35
|
+
const mockSetQuery = vi.fn();
|
|
36
|
+
vi.mock('use-context-selector', async () => {
|
|
37
|
+
const actual = await vi.importActual('use-context-selector');
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
useContextSelector: (_context, selector) => {
|
|
41
|
+
const mockContext = { setQuery: mockSetQuery };
|
|
42
|
+
return selector(mockContext);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
vi.mock('components/elements/ThemedEditor', () => ({
|
|
47
|
+
default: ({ value, onChange, id }) => {
|
|
48
|
+
return (_jsx("input", { id: id || 'themed-editor', value: value, onChange: e => {
|
|
49
|
+
onChange?.(e.target.value, true);
|
|
50
|
+
} }));
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
vi.mock('@monaco-editor/react', async () => {
|
|
54
|
+
const actual = await vi.importActual('@monaco-editor/react');
|
|
55
|
+
return { ...actual, useMonaco: vi.fn() };
|
|
56
|
+
});
|
|
57
|
+
// Mock iconExists
|
|
58
|
+
vi.mock('@iconify/react/dist/iconify.js', () => ({
|
|
59
|
+
Icon: ({ ...args }) => _jsx("div", { ...args, children: 'iconify' }),
|
|
60
|
+
iconExists: (icon) => icon?.startsWith('material-symbols:') || icon === 'test-icon'
|
|
61
|
+
}));
|
|
62
|
+
// Mock MUI components
|
|
63
|
+
vi.mock('@mui/material', async () => {
|
|
64
|
+
const actual = await vi.importActual('@mui/material');
|
|
65
|
+
return {
|
|
66
|
+
...actual,
|
|
67
|
+
Autocomplete: ({ ...props }) => {
|
|
68
|
+
return _jsx(actual.Autocomplete, { ...props });
|
|
69
|
+
},
|
|
70
|
+
CircularProgress: ({ ...props }) => _jsx("div", { role: "progressbar", id: "loading", ...omit(props, ['flexItem', 'sx']) }),
|
|
71
|
+
LinearProgress: ({ ...props }) => _jsx("div", { role: "progressbar", id: "loading", ...omit(props, ['flexItem', 'sx']) })
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
// Mock child components
|
|
75
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
76
|
+
default: ({ children, ...props }) => (_jsx("div", { id: "page-center", ...omit(props, ['textAlign', 'maxWidth']), children: children }))
|
|
77
|
+
}));
|
|
78
|
+
vi.mock('../../elements/display/QueryResultText', () => ({
|
|
79
|
+
default: ({ count, query }) => (_jsxs("div", { id: "query-result-text", children: [count, " ", ' results for ', " ", query] }))
|
|
80
|
+
}));
|
|
81
|
+
vi.mock('../hits/search/HitQuery', () => ({
|
|
82
|
+
default: ({ onChange, triggerSearch, disabled }) => {
|
|
83
|
+
return (_jsxs("div", { id: "hit-query", children: [_jsx("input", { id: "query-input", disabled: disabled, onChange: e => {
|
|
84
|
+
onChange?.(e.target.value, false);
|
|
85
|
+
}, onKeyDown: e => {
|
|
86
|
+
if (e.key === 'Enter') {
|
|
87
|
+
triggerSearch(e.target.value);
|
|
88
|
+
}
|
|
89
|
+
} }), _jsx("button", { id: "trigger-search", onClick: () => triggerSearch('howler.id:*'), children: 'search' })] }));
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
import ApiConfigProvider from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
93
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
94
|
+
import { I18nextProvider } from 'react-i18next';
|
|
95
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
96
|
+
import DossierEditor from './DossierEditor';
|
|
97
|
+
const mockUseParams = vi.mocked(useParams);
|
|
98
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
99
|
+
const mockNavigate = vi.mocked(useNavigate());
|
|
100
|
+
// Mock data
|
|
101
|
+
const mockDossier = {
|
|
102
|
+
dossier_id: 'test-dossier-1',
|
|
103
|
+
id: 'test-dossier-1',
|
|
104
|
+
title: 'Test Dossier',
|
|
105
|
+
type: 'global',
|
|
106
|
+
query: 'howler.id:*',
|
|
107
|
+
leads: [
|
|
108
|
+
{
|
|
109
|
+
label: { en: 'Lead 1', fr: 'Piste 1' },
|
|
110
|
+
icon: 'material-symbols:info',
|
|
111
|
+
format: 'markdown',
|
|
112
|
+
content: '# Hello, World'
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
pivots: [
|
|
116
|
+
{
|
|
117
|
+
label: { en: 'Pivot 1', fr: 'Pivot 1' },
|
|
118
|
+
icon: 'material-symbols:link',
|
|
119
|
+
format: 'link',
|
|
120
|
+
value: 'pivot.field',
|
|
121
|
+
mappings: [{ key: 'key1', field: 'howler.id' }]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
const Wrapper = ({ children }) => {
|
|
126
|
+
return (_jsx(I18nextProvider, { i18n: i18n, defaultNS: "translation", children: _jsx(ApiConfigProvider, { defaultConfig: {
|
|
127
|
+
indexes: { hit: { 'howler.id': {} } },
|
|
128
|
+
lookups: {},
|
|
129
|
+
configuration: {},
|
|
130
|
+
c12nDef: {},
|
|
131
|
+
mapping: {}
|
|
132
|
+
}, children: children }) }));
|
|
133
|
+
};
|
|
134
|
+
describe('DossierEditor', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
mockApiSearchHitPost.mockClear();
|
|
137
|
+
mockApiDossierGet.mockClear();
|
|
138
|
+
mockApiDossierPost.mockClear();
|
|
139
|
+
mockApiDossierPut.mockClear();
|
|
140
|
+
mockNavigate.mockClear();
|
|
141
|
+
mockSetQuery.mockClear();
|
|
142
|
+
// Default mock implementations
|
|
143
|
+
mockApiSearchHitPost.mockResolvedValue({ total: 42, items: [] });
|
|
144
|
+
});
|
|
145
|
+
describe('Component initialization', () => {
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
mockApiDossierGet.mockClear();
|
|
148
|
+
});
|
|
149
|
+
it('should render with default values for new dossier', async () => {
|
|
150
|
+
mockUseParams.mockReturnValueOnce({ id: null });
|
|
151
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(screen.getByTestId('dossier-title')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
expect(screen.getByTestId('dossier-title')).toHaveValue('');
|
|
156
|
+
expect(screen.getByTestId('hit-query')).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
it('should load existing dossier when id is provided', async () => {
|
|
159
|
+
mockApiDossierGet.mockResolvedValueOnce(mockDossier);
|
|
160
|
+
mockUseParams.mockReturnValue({ id: 'test-dossier-1' });
|
|
161
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(mockApiDossierGet).toHaveBeenCalledOnce();
|
|
164
|
+
});
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(screen.getByTestId('dossier-title')).toHaveValue('Test Dossier');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
it('should show loading state while fetching dossier', async () => {
|
|
170
|
+
let resolvePromise;
|
|
171
|
+
const delayedPromise = new Promise(resolve => {
|
|
172
|
+
resolvePromise = resolve;
|
|
173
|
+
});
|
|
174
|
+
mockApiDossierGet.mockReturnValue(delayedPromise);
|
|
175
|
+
mockUseParams.mockReturnValue({ id: 'test-dossier-1' });
|
|
176
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
expect(mockApiDossierGet).toHaveBeenCalledOnce();
|
|
179
|
+
});
|
|
180
|
+
rerender(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
// Resolve the promise
|
|
185
|
+
resolvePromise(mockDossier);
|
|
186
|
+
rerender(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(mockApiDossierGet).toHaveBeenCalled();
|
|
189
|
+
expect(screen.getByTestId('dossier-title')).toHaveValue('Test Dossier');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('Form interactions', () => {
|
|
193
|
+
it('should update title field', async () => {
|
|
194
|
+
const user = userEvent.setup();
|
|
195
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
196
|
+
const titleInput = screen.getByTestId('dossier-title');
|
|
197
|
+
await user.click(titleInput);
|
|
198
|
+
await user.keyboard('{Control>}a{/Control}{Backspace}');
|
|
199
|
+
await user.keyboard('My New Dossier');
|
|
200
|
+
rerender(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(titleInput).toHaveValue('My New Dossier');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('should toggle between global and personal type', async () => {
|
|
206
|
+
const user = userEvent.setup();
|
|
207
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
208
|
+
const personalButton = screen.getByRole('button', { name: /personal/i });
|
|
209
|
+
const globalButton = screen.getByRole('button', { name: /global/i });
|
|
210
|
+
await user.click(personalButton);
|
|
211
|
+
// The toggle button should now show personal as selected
|
|
212
|
+
expect(personalButton).toHaveAttribute('aria-pressed', 'true');
|
|
213
|
+
expect(globalButton).toHaveAttribute('aria-pressed', 'false');
|
|
214
|
+
await user.click(globalButton);
|
|
215
|
+
// The toggle button should now show personal as selected
|
|
216
|
+
expect(personalButton).toHaveAttribute('aria-pressed', 'false');
|
|
217
|
+
expect(globalButton).toHaveAttribute('aria-pressed', 'true');
|
|
218
|
+
});
|
|
219
|
+
it('should trigger search when query changes', async () => {
|
|
220
|
+
const user = userEvent.setup();
|
|
221
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
222
|
+
const queryInput = screen.getByTestId('query-input');
|
|
223
|
+
await user.click(queryInput);
|
|
224
|
+
await user.keyboard('{Control>}a{/Control}{Backspace}');
|
|
225
|
+
await user.keyboard('test query');
|
|
226
|
+
await user.keyboard('{Enter}');
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(mockApiSearchHitPost).toHaveBeenCalledWith({
|
|
229
|
+
query: 'test query',
|
|
230
|
+
rows: 0
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it('should display query result count after search', async () => {
|
|
235
|
+
const user = userEvent.setup();
|
|
236
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
237
|
+
const queryInput = screen.getByTestId('query-input');
|
|
238
|
+
await user.type(queryInput, 'test query');
|
|
239
|
+
await user.keyboard('{Enter}');
|
|
240
|
+
await waitFor(() => {
|
|
241
|
+
expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe('Tabs', () => {
|
|
246
|
+
it('should render leads and pivots tabs', async () => {
|
|
247
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(screen.getByRole('tab', { name: /leads/i })).toBeInTheDocument();
|
|
250
|
+
expect(screen.getByRole('tab', { name: /pivots/i })).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
it('should show LeadForm by default', async () => {
|
|
254
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(screen.getByTestId('lead-form')).toBeInTheDocument();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
it('should switch to PivotForm when pivots tab is clicked', async () => {
|
|
260
|
+
const user = userEvent.setup();
|
|
261
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
262
|
+
const pivotsTab = screen.getByRole('tab', { name: /pivots/i });
|
|
263
|
+
await user.click(pivotsTab);
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(screen.getByTestId('pivot-form')).toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('Validation', () => {
|
|
270
|
+
it('should disable save button when title is missing', async () => {
|
|
271
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
274
|
+
expect(saveButton).toBeDisabled();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
it('should disable save button when no leads or pivots exist', async () => {
|
|
278
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
279
|
+
const user = userEvent.setup();
|
|
280
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
281
|
+
const titleInput = screen.getByTestId('dossier-title');
|
|
282
|
+
await user.type(titleInput, 'Test Title');
|
|
283
|
+
const queryInput = screen.getByTestId('query-input');
|
|
284
|
+
user.click(queryInput);
|
|
285
|
+
await user.keyboard('test query');
|
|
286
|
+
await user.keyboard('{Enter}');
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
291
|
+
expect(saveButton).toBeDisabled();
|
|
292
|
+
});
|
|
293
|
+
it('should enable save button when all required fields are filled', async () => {
|
|
294
|
+
const user = userEvent.setup();
|
|
295
|
+
mockUseParams.mockReturnValue({ id: null });
|
|
296
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
297
|
+
// Fill title
|
|
298
|
+
const titleInput = screen.getByTestId('dossier-title');
|
|
299
|
+
await user.type(titleInput, 'Test Title');
|
|
300
|
+
// Add query and trigger search
|
|
301
|
+
const queryInput = screen.getByTestId('query-input');
|
|
302
|
+
await user.click(queryInput);
|
|
303
|
+
await user.keyboard('test query');
|
|
304
|
+
await user.keyboard('{Enter}');
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
|
|
307
|
+
expect(screen.getByTestId('create-lead-alert')).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
// Add a lead
|
|
310
|
+
await user.click(screen.getByTestId('add-lead'));
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(screen.getByTestId('lead-format')).toBeInTheDocument();
|
|
313
|
+
});
|
|
314
|
+
await user.click(screen.getByTestId('lead-format'));
|
|
315
|
+
await user.keyboard('markdown');
|
|
316
|
+
await user.keyboard('[ArrowDown]');
|
|
317
|
+
await user.keyboard('{Enter}');
|
|
318
|
+
await user.click(screen.getByTestId('lead-markdown'));
|
|
319
|
+
await user.keyboard('hello world');
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
322
|
+
expect(saveButton).not.toBeDisabled();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe('Dirty state tracking', () => {
|
|
327
|
+
it('should mark form as dirty when changes are made', async () => {
|
|
328
|
+
const user = userEvent.setup();
|
|
329
|
+
mockUseParams.mockReturnValue({ id: 'test-dossier-1' });
|
|
330
|
+
mockApiDossierGet.mockResolvedValueOnce(mockDossier);
|
|
331
|
+
mockApiSearchHitPost.mockResolvedValueOnce({ total: -1, items: [] });
|
|
332
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(screen.getByTestId('dossier-title')).toHaveValue('Test Dossier');
|
|
335
|
+
});
|
|
336
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
337
|
+
expect(saveButton).toBeDisabled();
|
|
338
|
+
// Make a change
|
|
339
|
+
const queryInput = screen.getByTestId('query-input');
|
|
340
|
+
await user.click(queryInput);
|
|
341
|
+
await user.keyboard('test query');
|
|
342
|
+
await user.keyboard('{Enter}');
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(saveButton).not.toBeDisabled();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
it('should not mark form as dirty when no changes are made', async () => {
|
|
348
|
+
mockUseParams.mockReturnValue({ id: 'test-dossier-1' });
|
|
349
|
+
mockApiDossierGet.mockResolvedValueOnce(mockDossier);
|
|
350
|
+
mockApiSearchHitPost.mockResolvedValueOnce({ total: 10, items: [] });
|
|
351
|
+
render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(screen.getByTestId('dossier-title')).toHaveValue('Test Dossier');
|
|
354
|
+
});
|
|
355
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
356
|
+
expect(saveButton).toBeDisabled();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -25,8 +25,8 @@ const LeadEditor = ({ lead, update }) => {
|
|
|
25
25
|
}
|
|
26
26
|
monaco.editor.getModels().forEach(model => model.setEOL(monaco.editor.EndOfLineSequence.LF));
|
|
27
27
|
}, [monaco]);
|
|
28
|
-
return (_jsxs(Stack, { spacing: 2, pt: 2, sx: { flex: 1 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", position: "relative", children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.icon'), value: icon, disabled: !lead, 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: !lead, 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.label.en'), disabled: !lead, value: lead?.label?.en ?? '', fullWidth: true, onChange: ev => update({ label: { en: ev.target.value } }) }), _jsx(TextField, { size: "small", label: t('route.dossiers.manager.label.fr'), disabled: !lead, value: lead?.label?.fr ?? '', fullWidth: true, onChange: ev => update({ label: { fr: ev.target.value } }) })] }), _jsx(Autocomplete, { disabled: !lead, options: ['markdown', ...howlerPluginStore.leadFormats], renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.format') }), value: lead?.format ??
|
|
29
|
-
(lead.format === 'markdown' ? (_jsx(ThemedEditor, { height: "100%", width: "100%", language: "markdown", theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: lead?.content ?? '', onChange: content => update({ content }), options: {} })) : (pluginStore.executeFunction(`lead.${lead.format}.form`, {
|
|
28
|
+
return (_jsxs(Stack, { spacing: 2, pt: 2, sx: { flex: 1 }, id: "lead-editor", children: [_jsxs(Stack, { direction: "row", alignItems: "center", position: "relative", children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.icon'), value: icon, disabled: !lead, 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: !lead, 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.label.en'), disabled: !lead, value: lead?.label?.en ?? '', fullWidth: true, onChange: ev => update({ label: { en: ev.target.value } }) }), _jsx(TextField, { size: "small", label: t('route.dossiers.manager.label.fr'), disabled: !lead, value: lead?.label?.fr ?? '', fullWidth: true, onChange: ev => update({ label: { fr: ev.target.value } }) })] }), _jsx(Autocomplete, { disabled: !lead, options: ['markdown', ...howlerPluginStore.leadFormats], id: "lead-format", renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.format') }), value: lead?.format ?? null, onChange: (_ev, format) => update({ format, metadata: '{}', content: '' }) }), !!lead?.format &&
|
|
29
|
+
(lead.format === 'markdown' ? (_jsx(ThemedEditor, { id: "lead-markdown", height: "100%", width: "100%", language: "markdown", theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: lead?.content ?? '', onChange: content => update({ content }), options: {} })) : (pluginStore.executeFunction(`lead.${lead.format}.form`, {
|
|
30
30
|
lead,
|
|
31
31
|
metadata,
|
|
32
32
|
update,
|
|
@@ -10,7 +10,7 @@ import LeadEditor from './LeadEditor';
|
|
|
10
10
|
const LeadForm = ({ dossier, setDossier, loading }) => {
|
|
11
11
|
const { t, i18n } = useTranslation();
|
|
12
12
|
const [tab, setTab] = useState(0);
|
|
13
|
-
return (_jsxs(Paper, { sx: { p: 1, display: 'flex', flexDirection: 'column', flex: 1 }, children: [_jsxs(Stack, { direction: "row", children: [!dossier?.leads || dossier.leads.length < 1 ? (_jsx(Alert, { variant: "outlined", severity: "warning", sx: {
|
|
13
|
+
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
14
|
mr: 1,
|
|
15
15
|
px: 1,
|
|
16
16
|
py: 0,
|
|
@@ -24,7 +24,7 @@ const LeadForm = ({ dossier, setDossier, loading }) => {
|
|
|
24
24
|
'& .MuiAlert-message': {
|
|
25
25
|
py: 0.7
|
|
26
26
|
}
|
|
27
|
-
}, children: t('route.dossiers.manager.lead.create') })) : (_jsx(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), sx: { minHeight: '0 !important' }, children: dossier.leads?.map((lead, index) => (_jsx(Tab, { disabled: !dossier || loading, sx: { py: 1, minHeight: '0 !important' }, label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: index }, lead.content))) })), _jsx(Button, { sx: { ml: 'auto', alignSelf: 'end', minWidth: '0 !important' }, size: "small", variant: "contained", onClick: () => {
|
|
27
|
+
}, children: t('route.dossiers.manager.lead.create') })) : (_jsx(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), sx: { minHeight: '0 !important' }, children: dossier.leads?.map((lead, index) => (_jsx(Tab, { disabled: !dossier || loading, sx: { py: 1, minHeight: '0 !important' }, label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: index }, lead.label?.en + lead.content))) })), _jsx(Button, { id: "add-lead", sx: { ml: 'auto', alignSelf: 'end', minWidth: '0 !important' }, size: "small", variant: "contained", onClick: () => {
|
|
28
28
|
setTab(dossier.leads?.length ?? 0);
|
|
29
29
|
setDossier(_dossier => ({
|
|
30
30
|
..._dossier,
|
|
@@ -22,7 +22,7 @@ const LinkForm = ({ pivot, update }) => {
|
|
|
22
22
|
mappings: pivot.mappings.filter((_m, _index) => index !== _index)
|
|
23
23
|
}), children: _jsx(Remove, {}) })] }), _mapping.field === 'custom' && (_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.mapping.custom'), disabled: !pivot, value: _mapping?.custom_value ?? '', onChange: ev => update({
|
|
24
24
|
mappings: pivot.mappings.map((_m, _index) => index === _index ? { ..._m, custom_value: ev.target.value } : _m)
|
|
25
|
-
}) }))] }, index))), _jsx(Button, { disabled: !pivot, sx: { ml: 'auto', alignSelf: 'end', minWidth: '0 !important' }, size: "small", variant: "contained", onClick: () => {
|
|
25
|
+
}) }))] }, index))), _jsx(Button, { id: "add-pivot", disabled: !pivot, sx: { ml: 'auto', alignSelf: 'end', minWidth: '0 !important' }, size: "small", variant: "contained", onClick: () => {
|
|
26
26
|
update({
|
|
27
27
|
mappings: [...(pivot.mappings ?? []), { key: 'key' }]
|
|
28
28
|
});
|
|
@@ -53,7 +53,7 @@ const PivotForm = ({ dossier, setDossier, loading }) => {
|
|
|
53
53
|
})), [setDossier, tab]);
|
|
54
54
|
const pivot = useMemo(() => dossier.pivots?.[tab] ?? null, [dossier.pivots, tab]);
|
|
55
55
|
const icon = useMemo(() => pivot?.icon ?? 'material-symbols:find-in-page', [pivot?.icon]);
|
|
56
|
-
return (_jsx(Paper, { sx: { p: 1, display: 'flex', flexDirection: 'column', flex: 1 }, children: _jsxs(Stack, { spacing: 2, children: [_jsxs(Stack, { direction: "row", children: [!dossier?.pivots || dossier.pivots.length < 1 ? (_jsx(Alert, { variant: "outlined", severity: "warning", sx: {
|
|
56
|
+
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
57
|
mr: 1,
|
|
58
58
|
px: 1,
|
|
59
59
|
py: 0,
|
|
@@ -128,7 +128,7 @@ const HitQuery = ({ searching = false, disabled = false, compact = false, trigge
|
|
|
128
128
|
p: 0.5,
|
|
129
129
|
height: multiline ? `${DEFAULT_MULTILINE_HEIGHT + y}px` : theme.spacing(5)
|
|
130
130
|
}
|
|
131
|
-
], onKeyDown: e => e.stopPropagation(), children: [_jsx(TuiIconButton, { disabled: query.includes('\n#') || disabled, sx: { mr: 1, alignSelf: 'start' }, onClick: () => setMultiline(!multiline), color: multiline ? 'primary' : theme.palette.text.primary, transparent: !multiline, size: compact ? 'small' : 'medium', children: _jsx(Height, { sx: { fontSize: '20px' } }) }), _jsx(QueryEditor, { query: preppedQuery, setQuery: setQuery, language: "lucene", height: multiline ? `${DEFAULT_MULTILINE_HEIGHT - 30}px` : '20px', onMount: onMount, editorOptions: options }), fzfSearch && (_jsx(Tooltip, { title: t('route.history'), children: _jsx(History, {}) })), _jsx(TuiIconButton, { disabled: searching || disabled, onClick: () => setQuery('howler.id:*'), sx: { ml: 1, alignSelf: 'start', flexShrink: 0 }, size: compact ? 'small' : 'medium', children: _jsx(Tooltip, { title: t('route.clear'), children: _jsx(Clear, { sx: { fontSize: '20px' } }) }) }), _jsx(TuiIconButton, { disabled: searching || disabled, onClick: search, sx: { ml: 1, alignSelf: 'start', flexShrink: 0 }, size: compact ? 'small' : 'medium', children: _jsx(Tooltip, { title: t('route.search'), children: _jsx(Badge, { invisible: !isDirty, color: "warning", variant: "dot", children: _jsx(Search, { sx: { fontSize: '20px' } }) }) }) }), !loaded && (_jsx(Skeleton, { variant: "rectangular", sx: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, height: "100%" })), multiline && (_jsx(Box, { sx: {
|
|
131
|
+
], onKeyDown: e => e.stopPropagation(), children: [_jsx(TuiIconButton, { disabled: query.includes('\n#') || disabled, sx: { mr: 1, alignSelf: 'start' }, onClick: () => setMultiline(!multiline), color: multiline ? 'primary' : theme.palette.text.primary, transparent: !multiline, size: compact ? 'small' : 'medium', children: _jsx(Height, { sx: { fontSize: '20px' } }) }), _jsx(QueryEditor, { id: "hit-query", query: preppedQuery, setQuery: setQuery, language: "lucene", height: multiline ? `${DEFAULT_MULTILINE_HEIGHT - 30}px` : '20px', onMount: onMount, editorOptions: options }), fzfSearch && (_jsx(Tooltip, { title: t('route.history'), children: _jsx(History, {}) })), _jsx(TuiIconButton, { disabled: searching || disabled, onClick: () => setQuery('howler.id:*'), sx: { ml: 1, alignSelf: 'start', flexShrink: 0 }, size: compact ? 'small' : 'medium', children: _jsx(Tooltip, { title: t('route.clear'), children: _jsx(Clear, { sx: { fontSize: '20px' } }) }) }), _jsx(TuiIconButton, { disabled: searching || disabled, onClick: search, sx: { ml: 1, alignSelf: 'start', flexShrink: 0 }, size: compact ? 'small' : 'medium', children: _jsx(Tooltip, { title: t('route.search'), children: _jsx(Badge, { invisible: !isDirty, color: "warning", variant: "dot", children: _jsx(Search, { sx: { fontSize: '20px' } }) }) }) }), !loaded && (_jsx(Skeleton, { variant: "rectangular", sx: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, height: "100%" })), multiline && (_jsx(Box, { sx: {
|
|
132
132
|
position: 'absolute',
|
|
133
133
|
left: 0,
|
|
134
134
|
right: 0,
|
|
@@ -29,7 +29,8 @@ const ViewCard = ({ viewId, limit }) => {
|
|
|
29
29
|
const timeout = setTimeout(() => setLoading(true), 200);
|
|
30
30
|
dispatchApi(api.search.hit.post({
|
|
31
31
|
query: view.query,
|
|
32
|
-
rows: limit
|
|
32
|
+
rows: limit,
|
|
33
|
+
metadata: ['analytic']
|
|
33
34
|
}))
|
|
34
35
|
.then(res => setHits(res.items ?? []))
|
|
35
36
|
.finally(() => {
|
|
@@ -639,8 +639,22 @@
|
|
|
639
639
|
"route.dossiers.manager.validation.search": "You must search to validate your query before submitting.",
|
|
640
640
|
"route.dossiers.manager.validation.error.missing": "{{field}} is invalid.",
|
|
641
641
|
"route.dossiers.manager.validation.error.leads": "A lead is misconfigured.",
|
|
642
|
+
"route.dossiers.manager.validation.error.leads.label": "You have not configured a lead label.",
|
|
643
|
+
"route.dossiers.manager.validation.error.leads.label.en": "You have not configured an English lead label.",
|
|
644
|
+
"route.dossiers.manager.validation.error.leads.label.fr": "You have not configured a French lead label.",
|
|
645
|
+
"route.dossiers.manager.validation.error.leads.format": "You have not set the format for the lead '{{label}}'.",
|
|
646
|
+
"route.dossiers.manager.validation.error.leads.content": "You have not set the content for the lead '{{label}}'.",
|
|
647
|
+
"route.dossiers.manager.validation.error.leads.icon": "You are missing an icon, or the specified icon does not exist for lead '{{label}}'.",
|
|
642
648
|
"route.dossiers.manager.validation.error.pivots": "A pivot is misconfigured.",
|
|
643
|
-
"route.dossiers.manager.validation.error.pivots.
|
|
649
|
+
"route.dossiers.manager.validation.error.pivots.label": "You have not configured a pivot label.",
|
|
650
|
+
"route.dossiers.manager.validation.error.pivots.label.en": "You have not configured an English pivot label.",
|
|
651
|
+
"route.dossiers.manager.validation.error.pivots.label.fr": "You have not configured a French pivot label.",
|
|
652
|
+
"route.dossiers.manager.validation.error.pivots.format": "You have not set the format for the pivot '{{label}}'.",
|
|
653
|
+
"route.dossiers.manager.validation.error.pivots.value": "You have not set the value for the pivot '{{label}}'.",
|
|
654
|
+
"route.dossiers.manager.validation.error.pivots.icon": "You are missing an icon, or the specified icon does not exist for pivot '{{label}}'.",
|
|
655
|
+
"route.dossiers.manager.validation.error.pivots.duplicate": "The pivot '{{label}}' has a duplicate key.",
|
|
656
|
+
"route.dossiers.manager.validation.error.pivots.key": "You have not configured a key for a mapping in the pivot '{{label}}'.",
|
|
657
|
+
"route.dossiers.manager.validation.error.pivots.field": "You have not configured a field or custom value for a mapping in the pivot '{{label}}'.",
|
|
644
658
|
"route.dossiers.manager.validation.error.items": "You have not configured any leads or pivots.",
|
|
645
659
|
"route.views": "Views",
|
|
646
660
|
"route.views.create.success": "View Created.",
|
|
@@ -640,8 +640,22 @@
|
|
|
640
640
|
"route.dossiers.manager.validation.search": "Vous devez effectuer une recherche pour valider votre requête avant de la soumettre.",
|
|
641
641
|
"route.dossiers.manager.validation.error.missing": "{{field}} est invalide.",
|
|
642
642
|
"route.dossiers.manager.validation.error.leads": "Une piste est mal configuré.",
|
|
643
|
+
"route.dossiers.manager.validation.error.leads.label": "Vous n'avez pas configuré d'étiquette de piste.",
|
|
644
|
+
"route.dossiers.manager.validation.error.leads.label.en": "Vous n'avez pas configuré d'étiquette de piste en anglais.",
|
|
645
|
+
"route.dossiers.manager.validation.error.leads.label.fr": "Vous n'avez pas configuré d'étiquette de piste en français.",
|
|
646
|
+
"route.dossiers.manager.validation.error.leads.format": "Vous n'avez pas défini le format pour la piste '{{label}}'.",
|
|
647
|
+
"route.dossiers.manager.validation.error.leads.content": "Vous n'avez pas défini le contenu pour la piste '{{label}}'.",
|
|
648
|
+
"route.dossiers.manager.validation.error.leads.icon": "Il manque une icône, ou l'icône spécifiée n'existe pas pour la piste '{{label}}'.",
|
|
643
649
|
"route.dossiers.manager.validation.error.pivots": "Un pivot est mal configuré.",
|
|
644
|
-
"route.dossiers.manager.validation.error.pivots.
|
|
650
|
+
"route.dossiers.manager.validation.error.pivots.label": "Vous n'avez pas configuré d'étiquette de pivot.",
|
|
651
|
+
"route.dossiers.manager.validation.error.pivots.label.en": "Vous n'avez pas configuré d'étiquette de pivot en anglais.",
|
|
652
|
+
"route.dossiers.manager.validation.error.pivots.label.fr": "Vous n'avez pas configuré d'étiquette de pivot en français.",
|
|
653
|
+
"route.dossiers.manager.validation.error.pivots.format": "Vous n'avez pas défini le format pour le pivot '{{label}}'.",
|
|
654
|
+
"route.dossiers.manager.validation.error.pivots.value": "Vous n'avez pas défini la valeur pour le pivot '{{label}}'.",
|
|
655
|
+
"route.dossiers.manager.validation.error.pivots.icon": "Il manque une icône, ou l'icône spécifiée n'existe pas pour le pivot '{{label}}'.",
|
|
656
|
+
"route.dossiers.manager.validation.error.pivots.duplicate": "Le pivot '{{label}}' a une clé dupliquée.",
|
|
657
|
+
"route.dossiers.manager.validation.error.pivots.key": "Vous n'avez pas configuré de clé pour un mappage dans le pivot '{{label}}'.",
|
|
658
|
+
"route.dossiers.manager.validation.error.pivots.field": "Vous n'avez pas configuré de champ ou de valeur personnalisée pour un mappage dans le pivot '{{label}}'.",
|
|
645
659
|
"route.dossiers.manager.validation.error.items": "Vous n'avez pas configuré de pistes ou de pivots.",
|
|
646
660
|
"route.views.create.success": "Vue crée.",
|
|
647
661
|
"route.views.update.success": "Vue actualisée.",
|