data-primals-engine 1.4.3 → 1.5.1
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/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
|
@@ -1,259 +1,355 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { useQuery, useQueryClient } from 'react-query';
|
|
3
|
-
import { useModelContext } from './contexts/ModelContext.jsx';
|
|
4
|
-
import { TextField } from './Field.jsx';
|
|
5
|
-
import { useAuthContext } from './contexts/AuthContext.jsx';
|
|
6
|
-
import { getDataAsString } from '../../src/data.js';
|
|
7
|
-
import { FaEdit, FaTrash } from 'react-icons/fa';
|
|
8
|
-
import Button from './Button.jsx';
|
|
9
|
-
import { mainFieldsTypes } from "../../src/constants.js";
|
|
10
|
-
import Draggable from "./Draggable.jsx";
|
|
11
|
-
import { useTranslation } from "react-i18next";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useQuery, useQueryClient } from 'react-query';
|
|
3
|
+
import { ModelProvider, useModelContext } from './contexts/ModelContext.jsx';
|
|
4
|
+
import { TextField } from './Field.jsx';
|
|
5
|
+
import { useAuthContext } from './contexts/AuthContext.jsx';
|
|
6
|
+
import { getDataAsString } from '../../src/data.js';
|
|
7
|
+
import { FaEdit, FaTrash } from 'react-icons/fa';
|
|
8
|
+
import Button from './Button.jsx';
|
|
9
|
+
import { mainFieldsTypes } from "../../src/constants.js";
|
|
10
|
+
import Draggable from "./Draggable.jsx";
|
|
11
|
+
import { useTranslation } from "react-i18next";
|
|
12
|
+
import { Dialog, DialogProvider } from "./Dialog.jsx";
|
|
13
|
+
import RelationSelectorWidget from "./RelationSelectorWidget.jsx";
|
|
14
|
+
|
|
15
|
+
const RelationField = ({ field, help, onFocus, onBlur, onChange, value = null }) => {
|
|
16
|
+
const { models, dataByModel, setOnSuccessCallbacks, relationIds, setRelationIds } = useModelContext();
|
|
17
|
+
const { name, relation: modelName } = field;
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const [selectedValues, setSelectedValues] = useState([]);
|
|
20
|
+
const [showResults, setResultsVisible] = useState(false);
|
|
21
|
+
const [searchValue, setSearchValue] = useState('');
|
|
22
|
+
const [isSelectorOpen, setSelectorOpen] = useState(false);
|
|
23
|
+
const [history, setHistory] = useState([]);
|
|
24
|
+
const { me } = useAuthContext();
|
|
25
|
+
const tr = useTranslation();
|
|
26
|
+
const { t, i18n } = tr;
|
|
27
|
+
const model = models?.find(f => f.name === modelName && f._user === me?.username);
|
|
28
|
+
|
|
29
|
+
const { isLoading: isLoadingInitialData } = useQuery(
|
|
30
|
+
// Clé de requête unique pour les données initiales de cette relation
|
|
31
|
+
['initialRelationData', modelName, Array.isArray(value) ? value.join(',') : value],
|
|
32
|
+
async () => {
|
|
33
|
+
// Détermine les IDs qu'il faut réellement aller chercher
|
|
34
|
+
const idsToFetch = (Array.isArray(value) ? value : (value ? [value] : []))
|
|
35
|
+
.filter(id =>
|
|
36
|
+
id && // S'assurer que l'ID n'est pas nul/undefined
|
|
37
|
+
!history.some(h => h._id === id) &&
|
|
38
|
+
!dataByModel[modelName]?.some(d => d._id === id)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!model || idsToFetch.length === 0) {
|
|
42
|
+
return []; // Rien à charger
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const params = new URLSearchParams();
|
|
46
|
+
params.append('model', model.name);
|
|
47
|
+
params.append('ids', idsToFetch.join(','));
|
|
48
|
+
params.append('depth', '1'); // On a besoin des données complètes
|
|
49
|
+
|
|
50
|
+
const response = await fetch(`/api/data/search?${params.toString()}`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ filter: {} }) // Le filtre peut être vide quand on cherche par IDs
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) throw new Error('Network response was not ok for initial relation data.');
|
|
57
|
+
const result = await response.json();
|
|
58
|
+
return result.data || [];
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
enabled: !!model && !!value && (Array.isArray(value) ? value.length > 0 : true),
|
|
62
|
+
onSuccess: (fetchedData) => {
|
|
63
|
+
if (fetchedData?.length > 0) {
|
|
64
|
+
// Ajoute les données fraîchement récupérées à l'historique local pour l'affichage
|
|
65
|
+
setHistory(current => [...current, ...fetchedData.filter(fd => !current.some(c => c._id === fd._id))]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
// Fetch related data based on search value
|
|
71
|
+
const { data: results = [], isError, refetch } = useQuery(
|
|
72
|
+
// La clé de la requête inclut maintenant le filtre de relation pour une mise en cache correcte
|
|
73
|
+
['api/search', model?.name, field?.name, searchValue, JSON.stringify(field.relationFilter)],
|
|
74
|
+
async ({ signal }) => {
|
|
75
|
+
if (!model) return [];
|
|
76
|
+
|
|
77
|
+
// --- DÉBUT DE LA NOUVELLE LOGIQUE DE FILTRAGE ---
|
|
78
|
+
|
|
79
|
+
// 1. On récupère le filtre permanent défini dans le modèle.
|
|
80
|
+
// S'il n'y en a pas, on utilise un objet vide qui n'aura aucun effet.
|
|
81
|
+
const permanentFilter = field.relationFilter || {};
|
|
82
|
+
|
|
83
|
+
// 2. On construit le filtre basé sur la recherche de l'utilisateur.
|
|
84
|
+
const searchFilter = {};
|
|
85
|
+
if (searchValue) {
|
|
86
|
+
const orConditions = [];
|
|
87
|
+
// Recherche sur les champs principaux (asMain)
|
|
88
|
+
model.fields.forEach(f => {
|
|
89
|
+
if (f.asMain && mainFieldsTypes.includes(f.type)) {
|
|
90
|
+
orConditions.push({"$regexMatch": {input: "$" + f.name, regex: searchValue}});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Si aucun champ principal, recherche sur les champs texte
|
|
94
|
+
if (orConditions.length === 0) {
|
|
95
|
+
model.fields.forEach(f => {
|
|
96
|
+
if (["string", "string_t", "richtext", "url"].includes(f.type)) {
|
|
97
|
+
orConditions.push({"$regexMatch": {input: "$" + f.name, regex: searchValue}});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Si toujours rien, on cherche sur l'ID (utile pour les développeurs)
|
|
102
|
+
if (orConditions.length === 0) {
|
|
103
|
+
orConditions.push({ "_id": searchValue });
|
|
104
|
+
}
|
|
105
|
+
searchFilter['$or'] = orConditions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. On combine les deux filtres avec un opérateur $and.
|
|
109
|
+
// Un document devra correspondre au filtre permanent ET au filtre de recherche.
|
|
110
|
+
const finalFilter = {
|
|
111
|
+
"$and": [
|
|
112
|
+
permanentFilter,
|
|
113
|
+
searchFilter
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// --- FIN DE LA NOUVELLE LOGIQUE DE FILTRAGE ---
|
|
118
|
+
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
params.append('model', field.relation);
|
|
121
|
+
params.append('limit', '100'); // Limite raisonnable pour les suggestions
|
|
122
|
+
params.append('depth', '2');
|
|
123
|
+
|
|
124
|
+
return fetch(`/api/data/search?${params.toString()}`, {
|
|
125
|
+
// On envoie le filtre final et complet
|
|
126
|
+
body: JSON.stringify({ filter: finalFilter }),
|
|
127
|
+
method: 'POST',
|
|
128
|
+
signal: signal,
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
})
|
|
131
|
+
.then(e => e.json())
|
|
132
|
+
.then(e => e.data);
|
|
133
|
+
},
|
|
134
|
+
{ enabled: !!model && showResults } // La requête ne s'exécute que si le panneau de résultats est visible
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ... (le reste du composant reste identique) ...
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setSearchValue('');
|
|
141
|
+
setHistory([]);
|
|
142
|
+
}, [modelName]);
|
|
143
|
+
|
|
144
|
+
useEffect(()=>{
|
|
145
|
+
if(searchValue==='' && !value && !field.multiple ){
|
|
146
|
+
onChange({ name, value: null });
|
|
147
|
+
}
|
|
148
|
+
},[searchValue])
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
// Add new results to history, avoiding duplicates.
|
|
152
|
+
if (results?.length > 0)
|
|
153
|
+
setHistory(currentHistory => {
|
|
154
|
+
const newItems = results.filter(
|
|
155
|
+
resultItem => !currentHistory.some(historyItem => historyItem._id === resultItem._id)
|
|
156
|
+
);
|
|
157
|
+
if (newItems.length > 0) return [...currentHistory, ...newItems];
|
|
158
|
+
return currentHistory;
|
|
159
|
+
});
|
|
160
|
+
}, [results]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
updateValue();
|
|
164
|
+
}, [history]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if( value)
|
|
168
|
+
updateValue();
|
|
169
|
+
}, [value]);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if( value === null ){
|
|
172
|
+
onChange({name, value: null});
|
|
173
|
+
setSearchValue('');
|
|
174
|
+
}
|
|
175
|
+
}, [value]);
|
|
176
|
+
|
|
177
|
+
const updateValue = () => {
|
|
178
|
+
if (!field.multiple) {
|
|
179
|
+
const v = history.find(d => d._id === value) || dataByModel[modelName]?.find(d => d._id === value);
|
|
180
|
+
if (v) {
|
|
181
|
+
setSearchValue(getDataAsString(model, v, tr, models) || '');
|
|
182
|
+
onChange({ name, value });
|
|
183
|
+
} else {
|
|
184
|
+
// Ne rien faire si la valeur n'est pas dans l'historique pour éviter d'effacer
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
setSelectedValues(value);
|
|
189
|
+
onChange({ name, value });
|
|
190
|
+
} else {
|
|
191
|
+
setSelectedValues([]);
|
|
192
|
+
onChange({ name, value: [] });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleClick = (e, data) => {
|
|
198
|
+
if (!field.multiple) {
|
|
199
|
+
setSearchValue(getDataAsString(model, history.find(d => d._id === data._id), tr, models) || '');
|
|
200
|
+
onChange({ name, value: data._id });
|
|
201
|
+
} else {
|
|
202
|
+
if (!selectedValues.includes(data._id)) {
|
|
203
|
+
setSelectedValues(values => {
|
|
204
|
+
const value = [...values, data._id];
|
|
205
|
+
onChange({ name, value });
|
|
206
|
+
return value;
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
onChange({ name, value: selectedValues });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
setResultsVisible(false);
|
|
213
|
+
ref.current?.focus();
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleRemove = (element) => {
|
|
218
|
+
if (!field.multiple) return;
|
|
219
|
+
setSelectedValues(values => {
|
|
220
|
+
const value = values.filter(f => f !== element);
|
|
221
|
+
onChange({ name, value });
|
|
222
|
+
return value;
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleValidateSelection = (selectedItems) => {
|
|
227
|
+
// Add the newly selected full objects to our local history
|
|
228
|
+
// so we can display them immediately.
|
|
229
|
+
setHistory(currentHistory => {
|
|
230
|
+
const newItems = selectedItems.filter(
|
|
231
|
+
selectedItem => !currentHistory.some(historyItem => historyItem._id === selectedItem._id)
|
|
232
|
+
);
|
|
233
|
+
if (newItems.length > 0) return [...currentHistory, ...newItems];
|
|
234
|
+
return currentHistory;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (field.multiple) {
|
|
238
|
+
const selectedIds = selectedItems.map(item => item._id);
|
|
239
|
+
onChange({ name, value: selectedIds });
|
|
240
|
+
setSelectedValues(selectedIds);
|
|
241
|
+
} else {
|
|
242
|
+
const selectedItem = selectedItems[0] || null;
|
|
243
|
+
onChange({ name, value: selectedItem?._id || null });
|
|
244
|
+
if (selectedItem) {
|
|
245
|
+
setSearchValue(getDataAsString(model, selectedItem, tr, models) || '');
|
|
246
|
+
} else {
|
|
247
|
+
setSearchValue('');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
setSelectorOpen(false);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const ref = useRef();
|
|
254
|
+
const inputRef = useRef();
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if( inputRef?.current ){
|
|
258
|
+
inputRef.current.ref.setAttribute('autocomplete', 'off');
|
|
259
|
+
}
|
|
260
|
+
}, [inputRef]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div onFocus={onFocus} onBlur={onBlur} className="field field-relation flex flex-row flex-start flex-1">
|
|
264
|
+
{help && <div className={"flex help"}>{help}</div>}
|
|
265
|
+
|
|
266
|
+
<div className="inner">
|
|
267
|
+
<TextField
|
|
268
|
+
required={!field.multiple && field.required}
|
|
269
|
+
ref={inputRef}
|
|
270
|
+
multiline={field.multiline}
|
|
271
|
+
autocomplete="none"
|
|
272
|
+
name={field.name}
|
|
273
|
+
id={field.name}
|
|
274
|
+
onFocus={e => {
|
|
275
|
+
setResultsVisible(true);
|
|
276
|
+
}}
|
|
277
|
+
value={searchValue}
|
|
278
|
+
onChange={e => {
|
|
279
|
+
if (!e.target.value) onChange({ name, value: null });
|
|
280
|
+
setResultsVisible(true);
|
|
281
|
+
setSearchValue(e.target.value);
|
|
282
|
+
}}
|
|
283
|
+
onBlur={(e) => {
|
|
284
|
+
setTimeout(() => setResultsVisible(false), 150); // Léger délai pour permettre le clic sur un résultat
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
<Button
|
|
288
|
+
type="button" className="btn-form btn-last" onClick={() => setSelectorOpen(true)}><FaEdit /></Button>
|
|
289
|
+
|
|
290
|
+
{showResults && (
|
|
291
|
+
<div className="results" onKeyDown={e => {
|
|
292
|
+
if( e.key === 'Escape' )
|
|
293
|
+
setResultsVisible(false);
|
|
294
|
+
}} >
|
|
295
|
+
{!isError &&
|
|
296
|
+
(results || []).map(r => {
|
|
297
|
+
const v = getDataAsString(models.find(m => m.name === modelName && m._user === me?.username), r, tr, models);
|
|
298
|
+
return (
|
|
299
|
+
<div tabIndex={0} onKeyDown={e => {
|
|
300
|
+
if( e.key === 'Enter')
|
|
301
|
+
handleClick(e, r);
|
|
302
|
+
}} onMouseDown={e => handleClick(e, r)} className="item" key={r._id}>
|
|
303
|
+
{v}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
<DialogProvider>
|
|
310
|
+
{isSelectorOpen && (
|
|
311
|
+
<Dialog
|
|
312
|
+
title={t('relationField.select.title', 'Sélectionner {{modelName}}', { modelName: model?.name || modelName })}
|
|
313
|
+
onClose={() => setSelectorOpen(false)}
|
|
314
|
+
isModal={true}
|
|
315
|
+
className="relation-selector-dialog"
|
|
316
|
+
>
|
|
317
|
+
<ModelProvider>
|
|
318
|
+
<RelationSelectorWidget
|
|
319
|
+
modelName={field.relation}
|
|
320
|
+
initialSelection={field.multiple ? selectedValues : (value ? [value] : [])}
|
|
321
|
+
isMultiple={field.multiple}
|
|
322
|
+
onValidate={handleValidateSelection}
|
|
323
|
+
onCancel={() => setSelectorOpen(false)}
|
|
324
|
+
/>
|
|
325
|
+
</ModelProvider>
|
|
326
|
+
</Dialog>
|
|
327
|
+
)}
|
|
328
|
+
</DialogProvider>
|
|
329
|
+
</div>
|
|
330
|
+
{field.multiple && selectedValues.length > 0 && (
|
|
331
|
+
<div ref={ref} tabIndex={0} className="selected-values flex flex-border flex-row flex-no-gap flex-start flex-1">
|
|
332
|
+
<Draggable items={selectedValues} renderItem={(id,i) =>{
|
|
333
|
+
const val = history.find(f => f._id === id) || dataByModel[modelName]?.find(f => f._id === id);
|
|
334
|
+
if (isLoadingInitialData && !val) {
|
|
335
|
+
return <div className="flex selected-value-loading" key={id}>...</div>;
|
|
336
|
+
}
|
|
337
|
+
if (!val) {
|
|
338
|
+
return <div className="flex" key={id}>data non chargée<FaTrash onClick={() => handleRemove(id)} /></div>;
|
|
339
|
+
}
|
|
340
|
+
const v = getDataAsString(models.find(m => m.name === modelName && m._user === me?.username), val, tr, models);
|
|
341
|
+
return <div onClick={() => handleRemove(id)} className="selected-value flex" key={id}>
|
|
342
|
+
<span className="flex-1">{v}</span>
|
|
343
|
+
<FaTrash className="cursor-pointer" />
|
|
344
|
+
</div>;
|
|
345
|
+
}} onChange={(arr) => {
|
|
346
|
+
setSelectedValues(arr);
|
|
347
|
+
onChange({ name, value: arr });
|
|
348
|
+
}} />
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
259
355
|
export default RelationField;
|