@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.
@@ -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
- const [config, setConfig] = useState({
6
- indexes: null,
7
- lookups: null,
8
- configuration: null,
9
- c12nDef: null,
10
- mapping: null
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
@@ -1,3 +1,5 @@
1
1
  import type { EditorProps } from '@monaco-editor/react';
2
- declare const _default: import("react").NamedExoticComponent<EditorProps>;
2
+ declare const _default: import("react").NamedExoticComponent<EditorProps & {
3
+ id?: string;
4
+ }>;
3
5
  export default _default;
@@ -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);
@@ -9,6 +9,7 @@ interface QueryEditorProps {
9
9
  height?: string;
10
10
  width?: string;
11
11
  editorOptions?: editor.IStandaloneEditorConstructionOptions;
12
+ id?: string;
12
13
  }
13
14
  declare const _default: import("react").NamedExoticComponent<QueryEditorProps>;
14
15
  export default _default;
@@ -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
- api.search.histogram.hit
19
- .post('timestamp', {
20
- query: `howler.analytic:("${analytic.name}")`,
21
- start: 'now-3M',
22
- gap: '1d',
23
- mincount: 0
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
- if (!(dossier.leads ?? [])?.every(lead => lead.icon &&
56
- iconExists(lead.icon) &&
57
- lead.label?.en &&
58
- lead.label?.fr &&
59
- lead.label &&
60
- lead.format &&
61
- lead.content)) {
62
- return t('route.dossiers.manager.validation.error.leads');
63
- }
64
- if (!(dossier.pivots ?? []).every(pivot => pivot.icon && iconExists(pivot.icon) && pivot.label && pivot.label.en && pivot.label.fr && pivot.format)) {
65
- return t('route.dossiers.manager.validation.error.pivots');
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
- if (!dossier.pivots?.every(pivot => (pivot.mappings ?? []).length === uniqBy(pivot.mappings ?? [], 'key').length)) {
68
- return t('route.dossiers.manager.validation.error.pivots.duplicate');
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 ?? '', onChange: (_ev, format) => update({ format, metadata: '{}', content: '' }) }), !!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.duplicate": "A pivot has a duplicate key.",
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.duplicate": "Un pivot a une clé dupliquée.",
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.",
package/package.json CHANGED
@@ -96,7 +96,7 @@
96
96
  "internal-slot": "1.0.7"
97
97
  },
98
98
  "type": "module",
99
- "version": "2.15.0-dev.307",
99
+ "version": "2.15.0-dev.310",
100
100
  "exports": {
101
101
  "./i18n": "./i18n.js",
102
102
  "./index.css": "./index.css",