data-primals-engine 1.0.10 → 1.1.0
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/client/README.md +8 -0
- package/client/index.html +31 -0
- package/client/index.js +25 -0
- package/client/package-lock.json +11664 -0
- package/client/package.json +70 -0
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
- package/client/public/doc/API.postman_collection.json +214 -0
- package/client/public/github.svg +1 -0
- package/client/public/profilCompany.jpg +0 -0
- package/client/public/profilDeveloper.jpg +0 -0
- package/client/public/profilEngineer.jpg +0 -0
- package/client/public/profilPersonal.jpg +0 -0
- package/client/public/profils .psd +0 -0
- package/client/public/react.svg +9 -0
- package/client/src/APIInfo.jsx +328 -0
- package/client/src/AddWidgetTypeModal.jsx +44 -0
- package/client/src/AddWidgetTypeModal.scss +87 -0
- package/client/src/App.css +42 -0
- package/client/src/App.jsx +586 -0
- package/client/src/App.scss +1466 -0
- package/client/src/AssistantChat.jsx +280 -0
- package/client/src/AssistantChat.scss +229 -0
- package/client/src/Button.jsx +12 -0
- package/client/src/Button.scss +197 -0
- package/client/src/CalculationBuilder.jsx +338 -0
- package/client/src/CalculationStepRow.jsx +199 -0
- package/client/src/CalendarConfigModal.jsx +49 -0
- package/client/src/ChartConfigModal.jsx +402 -0
- package/client/src/ConditionBuilder.jsx +622 -0
- package/client/src/ConditionBuilder.scss +672 -0
- package/client/src/ConditionBuilder2.jsx +757 -0
- package/client/src/ContentView.jsx +274 -0
- package/client/src/CronBuilder.jsx +126 -0
- package/client/src/CronBuilder.scss +128 -0
- package/client/src/CronPartBuilder.jsx +150 -0
- package/client/src/Dashboard.jsx +210 -0
- package/client/src/Dashboard.scss +445 -0
- package/client/src/DashboardChart.jsx +362 -0
- package/client/src/DashboardFlexViewItem.jsx +129 -0
- package/client/src/DashboardView.jsx +554 -0
- package/client/src/DataChart.jsx +349 -0
- package/client/src/DataEditor.jsx +556 -0
- package/client/src/DataLayout.jsx +709 -0
- package/client/src/DataTable.jsx +996 -0
- package/client/src/DataTable.scss +73 -0
- package/client/src/Dialog.jsx +90 -0
- package/client/src/Dialog.scss +117 -0
- package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
- package/client/src/DocumentationPageLayout.jsx +93 -0
- package/client/src/DocumentationPageLayout.scss +262 -0
- package/client/src/Draggable.jsx +222 -0
- package/client/src/Draggable.scss +49 -0
- package/client/src/ExportDialog.jsx +172 -0
- package/client/src/Field.jsx +1558 -0
- package/client/src/FlexBuilder.jsx +269 -0
- package/client/src/FlexBuilder.scss +226 -0
- package/client/src/FlexBuilderControls.jsx +170 -0
- package/client/src/FlexBuilderModal.jsx +75 -0
- package/client/src/FlexBuilderModal.scss +109 -0
- package/client/src/FlexBuilderPreview.jsx +48 -0
- package/client/src/FlexBuilderUtils.js +84 -0
- package/client/src/FlexDataRenderer.jsx +170 -0
- package/client/src/FlexDataRenderer.scss +80 -0
- package/client/src/FlexNode.jsx +290 -0
- package/client/src/FlexTreeUtils.js +129 -0
- package/client/src/HiddenableCell.jsx +46 -0
- package/client/src/HiddenableCell.scss +36 -0
- package/client/src/KPIDialog.jsx +39 -0
- package/client/src/KPIWidget.jsx +139 -0
- package/client/src/KanbanCard.jsx +92 -0
- package/client/src/KanbanColumn.jsx +29 -0
- package/client/src/KanbanConfigModal.jsx +115 -0
- package/client/src/KanbanView.jsx +113 -0
- package/client/src/KanbanView.scss +107 -0
- package/client/src/MessageRotator.jsx +77 -0
- package/client/src/MessageRotator.scss +14 -0
- package/client/src/ModelCreator.jsx +640 -0
- package/client/src/ModelCreator.scss +154 -0
- package/client/src/ModelCreatorField.jsx +810 -0
- package/client/src/ModelImporter.jsx +120 -0
- package/client/src/ModelList.jsx +205 -0
- package/client/src/Notification.jsx +137 -0
- package/client/src/Notification.scss +126 -0
- package/client/src/NotificationProvider.jsx +73 -0
- package/client/src/PackGallery.jsx +211 -0
- package/client/src/PackGallery.scss +156 -0
- package/client/src/Pagination.jsx +141 -0
- package/client/src/RTE.jsx +415 -0
- package/client/src/RTETrans.jsx +103 -0
- package/client/src/RelationField.jsx +247 -0
- package/client/src/RelationValue.jsx +230 -0
- package/client/src/RestoreConfirmationModal.jsx +183 -0
- package/client/src/RestoreDialog.jsx +134 -0
- package/client/src/RestoreDialog.scss +67 -0
- package/client/src/RichTextEditorModal.jsx +34 -0
- package/client/src/RichTextEditorModal.scss +54 -0
- package/client/src/TourSpotlight.jsx +234 -0
- package/client/src/TourSpotlight.scss +100 -0
- package/client/src/TutorialsMenu.jsx +83 -0
- package/client/src/TutorialsMenu.scss +217 -0
- package/client/src/TutorialsRewardModal.jsx +65 -0
- package/client/src/Utils.jsx +78 -0
- package/client/src/ViewSwitcher.jsx +54 -0
- package/client/src/ViewSwitcher.scss +74 -0
- package/client/src/Webpage.jsx +69 -0
- package/client/src/_variables.scss +21 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/constants.js +68 -0
- package/client/src/contexts/AuthContext.jsx +28 -0
- package/client/src/contexts/ModelContext.jsx +328 -0
- package/client/src/contexts/UIContext.jsx +52 -0
- package/client/src/core/data.js +35 -0
- package/client/src/entry-client.jsx +15 -0
- package/client/src/entry-server.jsx +13 -0
- package/client/src/filter.js +222 -0
- package/client/src/hooks/data.js +101 -0
- package/client/src/hooks/useDebounce.js +20 -0
- package/client/src/hooks/useDragAndDrop.js +59 -0
- package/client/src/hooks/useLocalStorage.js +100 -0
- package/client/src/hooks/useMount.js +42 -0
- package/client/src/hooks/useTutorials.jsx +227 -0
- package/client/src/hooks/useWindowSize.js +24 -0
- package/client/src/i18n.js +47 -0
- package/client/src/index.css +90 -0
- package/client/src/index.js +10 -0
- package/client/src/main.jsx +11 -0
- package/client/src/translations.js +15940 -0
- package/client/src/tutorials.js +112 -0
- package/client/vite.config.js +30 -0
- package/eslint.config.js +50 -0
- package/package.json +48 -27
- package/server.js +9 -0
- package/src/constants.js +12 -23
- package/src/core.js +9 -8
- package/src/data.js +81 -80
- package/src/defaultModels.js +114 -115
- package/src/email.js +8 -9
- package/src/engine.js +5 -5
- package/src/events.js +1 -2
- package/src/i18n.js +5390 -211
- package/src/middlewares/middleware-mongodb.js +94 -93
- package/src/migrate.js +24 -24
- package/src/modules/assistant.js +55 -55
- package/src/modules/bucket.js +10 -9
- package/src/modules/data.js +387 -357
- package/src/modules/file.js +3 -6
- package/src/modules/mongodb.js +6 -7
- package/src/modules/user.js +24 -47
- package/src/modules/workflow.js +68 -67
- package/src/openai.jobs.js +2 -2
- package/src/packs.js +30 -31
- package/src/providers.js +101 -3
- package/src/setenv.js +1 -1
- package/src/user.js +0 -17
- package/src/workers/crypto-worker.js +21 -20
- package/src/workers/import-export-worker.js +18 -18
- package/test/data.backup.integration.test.js +7 -12
- package/test/data.integration.test.js +27 -25
- package/test/globalSetup.js +2 -2
- package/test/import_export.integration.test.js +6 -6
- package/test/model.integration.test.js +21 -22
- package/test/workflow.integration.test.js +2 -5
- package/test/workflow.robustness.test.js +6 -13
- package/vitest.config.js +2 -2
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
2
|
+
import { Tooltip } from 'react-tooltip';
|
|
3
|
+
import {FaPlus, FaProjectDiagram, FaTrash, FaInfoCircle, FaEdit, FaTags} from 'react-icons/fa';
|
|
4
|
+
import { Trans } from 'react-i18next';
|
|
5
|
+
import {convertInputValue, MONGO_CALC_OPERATORS} from "./filter.js";
|
|
6
|
+
import i18n from "i18next";
|
|
7
|
+
import {FaRepeat} from "react-icons/fa6";
|
|
8
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const OperatorSelector = ({ onSelect }) => {
|
|
12
|
+
const [search, setSearch] = useState('');
|
|
13
|
+
|
|
14
|
+
const filteredOps = Object.entries(MONGO_CALC_OPERATORS)
|
|
15
|
+
.filter(([key, config]) =>
|
|
16
|
+
key.toLowerCase().includes(search.toLowerCase()) ||
|
|
17
|
+
config.label.toLowerCase().includes(search.toLowerCase())
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="operator-selector">
|
|
22
|
+
<input
|
|
23
|
+
type="text"
|
|
24
|
+
placeholder={i18n.t("cb.searchOperator")}
|
|
25
|
+
value={search}
|
|
26
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
27
|
+
autoFocus
|
|
28
|
+
/>
|
|
29
|
+
<div className="operator-list">
|
|
30
|
+
{filteredOps.map(([key, config]) => (
|
|
31
|
+
<div
|
|
32
|
+
key={'ol'+key}
|
|
33
|
+
className="operator-item"
|
|
34
|
+
onClick={() => onSelect(key)}
|
|
35
|
+
data-tooltip-id="op-tooltip"
|
|
36
|
+
data-tooltip-content={config.description}
|
|
37
|
+
>
|
|
38
|
+
<span className="op-key">{key}</span>
|
|
39
|
+
<span className="op-label">{config.label}</span>
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
<Tooltip id="op-tooltip" />
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = false, models = [], currentModelFields = [], isInFindContext = false }) => {
|
|
49
|
+
const [editing, setEditing] = useState(false);
|
|
50
|
+
const [currentValue, setCurrentValue] = useState(value);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setCurrentValue(value);
|
|
53
|
+
}, [value]);
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
const openDoc =(op) =>{
|
|
57
|
+
window.open(`https://www.mongodb.com/docs/manual/reference/operator/aggregation/${op}`, '_blank');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleOperatorSelect = (operator) => {
|
|
61
|
+
const opConfig = MONGO_CALC_OPERATORS[operator];
|
|
62
|
+
let newValue;
|
|
63
|
+
if (operator === '$find') {
|
|
64
|
+
// Cas spécial pour créer un $find au niveau racine dans un champ
|
|
65
|
+
const newValue = { "": { "$find": { "$eq": ["", ""] } } };
|
|
66
|
+
onChange(newValue, path);
|
|
67
|
+
setCurrentValue(newValue);
|
|
68
|
+
setEditing(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor') {
|
|
72
|
+
// Cas spécial pour les opérateurs logiques - conserve la structure existante si possible
|
|
73
|
+
if (currentValue && typeof currentValue === 'object' && !Array.isArray(currentValue)) {
|
|
74
|
+
const existingKey = Object.keys(currentValue)[0];
|
|
75
|
+
if (existingKey === '$and' || existingKey === '$or' || existingKey === '$not' || existingKey === '$nor') {
|
|
76
|
+
// Si on transforme un opérateur logique en un autre, on garde le contenu
|
|
77
|
+
newValue = { [operator]: currentValue[existingKey] };
|
|
78
|
+
} else {
|
|
79
|
+
// Sinon, on crée une nouvelle structure avec l'opérateur logique
|
|
80
|
+
newValue = { [operator]: [currentValue] };
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Par défaut, création d'une nouvelle structure logique
|
|
84
|
+
newValue = { [operator]: [{ $eq: ["", ""] }] };
|
|
85
|
+
}
|
|
86
|
+
}else if (operator === '$find') {
|
|
87
|
+
// Cas spécial pour $find qui prend une expression de recherche
|
|
88
|
+
newValue = { [operator]: { $eq: ["", ""] } };
|
|
89
|
+
}else if (opConfig.converter || (!opConfig.multi && !opConfig.args)) {
|
|
90
|
+
newValue = { [operator]: "" };
|
|
91
|
+
} else {
|
|
92
|
+
// Nouvelle logique qui prend en compte args et multi
|
|
93
|
+
if (opConfig.args) {
|
|
94
|
+
// Cas où on a un nombre fixe d'arguments
|
|
95
|
+
const argsArray = Array(opConfig.args).fill("");
|
|
96
|
+
newValue = { [operator]: argsArray };
|
|
97
|
+
} else if (opConfig.multi) {
|
|
98
|
+
// Cas multi-arguments (tableau avec un élément par défaut)
|
|
99
|
+
newValue = { [operator]: [""] };
|
|
100
|
+
} else {
|
|
101
|
+
// Cas mono-argument (valeur directe)
|
|
102
|
+
newValue = { [operator]: "" };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
gtag('event', '[cb] operator ' + operator);
|
|
107
|
+
onChange(newValue, path);
|
|
108
|
+
setCurrentValue(newValue);
|
|
109
|
+
setEditing(false);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
const renderArgument = (arg, index, parentOperator, args) => {
|
|
114
|
+
const isSimpleValue = typeof arg !== 'object' || arg === null || Array.isArray(arg);
|
|
115
|
+
|
|
116
|
+
// Fonction pour mettre à jour cet argument spécifique
|
|
117
|
+
const handleArgChange = (newArgValue) => {
|
|
118
|
+
const updatedArgs = [...args];
|
|
119
|
+
|
|
120
|
+
gtag('event', '[cb] advanced');
|
|
121
|
+
updatedArgs[index] = newArgValue;
|
|
122
|
+
onChange({ [parentOperator]: updatedArgs }, path);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Fonction pour supprimer cet argument
|
|
126
|
+
const removeArgument = () => {
|
|
127
|
+
// Vérifier si args est un tableau avant de tenter de le modifier
|
|
128
|
+
if (!Array.isArray(args)) {
|
|
129
|
+
console.error("Impossible de supprimer - args n'est pas un tableau", args);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const updatedArgs = [...args]; // Crée une copie du tableau
|
|
134
|
+
updatedArgs.splice(index, 1); // Supprime l'élément
|
|
135
|
+
|
|
136
|
+
// Si c'est le dernier argument, on supprime complètement
|
|
137
|
+
if (updatedArgs.length === 0) {
|
|
138
|
+
onChange(null, path); // Demande de suppression
|
|
139
|
+
} else {
|
|
140
|
+
onChange({ [parentOperator]: updatedArgs }, path);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const parentOpConfig = MONGO_CALC_OPERATORS[parentOperator];
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div key={`arg-${index}`} className="expression-arg">
|
|
147
|
+
<div className="arg-content">
|
|
148
|
+
{isSimpleValue ? (
|
|
149
|
+
// Cas 1: C'est une valeur simple (texte, nombre, $champ)
|
|
150
|
+
<div className="simple-value-container">
|
|
151
|
+
<FieldInput
|
|
152
|
+
value={arg}
|
|
153
|
+
onChange={(newValue) => {
|
|
154
|
+
const updatedArgs = [...args];
|
|
155
|
+
updatedArgs[index] = newValue;
|
|
156
|
+
onChange({ [parentOperator]: updatedArgs }, path);
|
|
157
|
+
}}
|
|
158
|
+
fieldNames={fieldNames}
|
|
159
|
+
isInFindContext={isInFindContext}
|
|
160
|
+
/>
|
|
161
|
+
{!parentOpConfig?.disableAdvancedValue && (<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="switch-to-expr"
|
|
164
|
+
onClick={() => handleArgChange({ $eq: [arg, ""] })}
|
|
165
|
+
title={i18n.t("cb.convertToExpression")}
|
|
166
|
+
>
|
|
167
|
+
<FaProjectDiagram />
|
|
168
|
+
</button>)}
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={removeArgument}
|
|
172
|
+
className="remove-arg"
|
|
173
|
+
title={i18n.t("cb.deleteArgument")}
|
|
174
|
+
>
|
|
175
|
+
<FaTrash/>
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
) : (
|
|
179
|
+
// Cas 2: C'est une expression imbriquée
|
|
180
|
+
<ExpressionField
|
|
181
|
+
value={arg}
|
|
182
|
+
onChange={handleArgChange}
|
|
183
|
+
fieldNames={fieldNames}
|
|
184
|
+
path={[...path, parentOperator, index]}
|
|
185
|
+
currentModelFields={currentModelFields}
|
|
186
|
+
models={models}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (isRoot && (!currentValue || Object.keys(currentValue).length === 0)) {
|
|
195
|
+
const addRootGroup = (operator) => {
|
|
196
|
+
onChange({ [operator]: [{ $eq: ["", ""] }] });
|
|
197
|
+
};
|
|
198
|
+
const addRootCondition = () => {
|
|
199
|
+
onChange({ $eq: ["", ""] });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="empty-state">
|
|
204
|
+
<div className="empty-actions">
|
|
205
|
+
<button type="button" onClick={addRootCondition}>
|
|
206
|
+
<FaPlus/> <Trans i18nKey={"cb.condition"}>Condition</Trans>
|
|
207
|
+
</button>
|
|
208
|
+
<button type="button" onClick={() => addRootGroup('$and')}>
|
|
209
|
+
<FaProjectDiagram/> <Trans i18nKey={"conditionBuilder.andGroup"} />
|
|
210
|
+
</button>
|
|
211
|
+
<button type="button" onClick={() => addRootGroup('$or')}>
|
|
212
|
+
<FaProjectDiagram/> <Trans i18nKey={"conditionBuilder.orGroup"} />
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof currentValue === 'object' && currentValue !== null && !Array.isArray(currentValue)) {
|
|
220
|
+
|
|
221
|
+
const operator = Object.keys(currentValue).find(k => k.startsWith('$'));
|
|
222
|
+
const args = currentValue[operator];
|
|
223
|
+
const isFieldWithFind = !operator && Object.values(currentValue)[0]?.$find;
|
|
224
|
+
|
|
225
|
+
// Cas unique pour un champ contenant $find
|
|
226
|
+
if (isFieldWithFind) {
|
|
227
|
+
const fieldName = Object.keys(currentValue)[0];
|
|
228
|
+
const findExpr = currentValue[fieldName].$find;
|
|
229
|
+
const findCondition = findExpr || { $eq: ["", ""] };
|
|
230
|
+
|
|
231
|
+
// --- Logique corrigée pour trouver les champs du modèle lié ---
|
|
232
|
+
const fieldDef = currentModelFields.find(f => f.name === fieldName);
|
|
233
|
+
let relatedFieldNames = [];
|
|
234
|
+
let relatedModelFields = [];
|
|
235
|
+
if (fieldDef && fieldDef.type === 'relation' && fieldDef.relation) {
|
|
236
|
+
const relatedModel = models.find(m => m.name === fieldDef.relation);
|
|
237
|
+
if (relatedModel) {
|
|
238
|
+
relatedFieldNames = relatedModel.fields.map(f => f.name);
|
|
239
|
+
relatedModelFields = relatedModel.fields;
|
|
240
|
+
}
|
|
241
|
+
console.log({models,relatedModel})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Fin de la correction ---
|
|
245
|
+
|
|
246
|
+
const handleFieldChange = (newField) => {
|
|
247
|
+
const newValue = {
|
|
248
|
+
[newField]: {
|
|
249
|
+
$find: findCondition
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
onChange(newValue, path);
|
|
253
|
+
setCurrentValue(newValue);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleConditionChange = (newCondition) => {
|
|
257
|
+
const newValue = {
|
|
258
|
+
[fieldName]: {
|
|
259
|
+
$find: newCondition
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
onChange(newValue, path);
|
|
263
|
+
setCurrentValue(newValue);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="expression-block field-with-find">
|
|
268
|
+
<div className="field-header">
|
|
269
|
+
<div className="expression-header">
|
|
270
|
+
{/* L'opérateur est implicite ($find), on affiche directement le sélecteur de champ */}
|
|
271
|
+
<FieldInput
|
|
272
|
+
value={fieldName}
|
|
273
|
+
onChange={handleFieldChange}
|
|
274
|
+
fieldNames={fieldNames}
|
|
275
|
+
isFieldName={true}
|
|
276
|
+
currentModelFields={currentModelFields} // Passer les champs complets
|
|
277
|
+
onlyRelations={true}
|
|
278
|
+
/>
|
|
279
|
+
<div className="expression-actions">
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={() => setEditing(!editing)}
|
|
283
|
+
title={i18n.t("cb.changeOperator")}
|
|
284
|
+
>
|
|
285
|
+
<FaRepeat/>
|
|
286
|
+
</button>
|
|
287
|
+
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={() => onChange(null, path)}
|
|
291
|
+
title={i18n.t("cb.deleteBlock")}
|
|
292
|
+
>
|
|
293
|
+
<FaTrash/>
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{editing && (
|
|
300
|
+
<div className="operator-selector-overlay">
|
|
301
|
+
<OperatorSelector onSelect={handleOperatorSelect}/>
|
|
302
|
+
<button type="button" onClick={() => setEditing(false)}>Annuler</button>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
<div className="find-content">
|
|
307
|
+
<div className="find-condition">
|
|
308
|
+
<ExpressionField
|
|
309
|
+
value={findCondition}
|
|
310
|
+
onChange={handleConditionChange}
|
|
311
|
+
// On passe les champs du modèle lié s'ils existent
|
|
312
|
+
fieldNames={relatedFieldNames.length > 0 ? relatedFieldNames : fieldNames}
|
|
313
|
+
path={[...path, fieldName, '$find']}
|
|
314
|
+
models={models}
|
|
315
|
+
currentModelFields={relatedModelFields.length > 0 ? relatedModelFields : currentModelFields}
|
|
316
|
+
isInFindContext={true} // On active le contexte $$this pour les enfants
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Gestion spéciale pour $and et $or
|
|
325
|
+
let opConfig;
|
|
326
|
+
if (operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor') {
|
|
327
|
+
opConfig = {
|
|
328
|
+
label: operator === '$and' ? 'ET logique'
|
|
329
|
+
: operator === '$or' ? 'OU logique'
|
|
330
|
+
: operator === '$not' ? 'NON logique'
|
|
331
|
+
: 'OU NON logique',
|
|
332
|
+
description: operator === '$and'
|
|
333
|
+
? 'Combine plusieurs expressions avec ET logique'
|
|
334
|
+
: operator === '$or'
|
|
335
|
+
? 'Combine plusieurs expressions avec OU logique'
|
|
336
|
+
: operator === '$not'
|
|
337
|
+
? 'Inverse la condition (ex: non égal à)'
|
|
338
|
+
: 'Renvoie vrai si aucune des conditions n\'est vérifiée',
|
|
339
|
+
multi: true
|
|
340
|
+
};
|
|
341
|
+
} else if (operator) {
|
|
342
|
+
opConfig = MONGO_CALC_OPERATORS[operator];
|
|
343
|
+
} else {
|
|
344
|
+
return <></>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if( !opConfig )
|
|
348
|
+
return <></>
|
|
349
|
+
const isMulti = opConfig.multi && !opConfig.args; // vrai seulement si multi sans args fixe
|
|
350
|
+
const isFixedArgs = opConfig.args; // vrai si nombre d'args fixé
|
|
351
|
+
|
|
352
|
+
// Déterminer si le parent est un opérateur logique ou $find
|
|
353
|
+
const parentIsLogicalOrFind = path.length === 0 ||
|
|
354
|
+
(path[path.length - 1] === '$and' ||
|
|
355
|
+
path[path.length - 1] === '$or' ||
|
|
356
|
+
path[path.length - 1] === '$and' ||
|
|
357
|
+
path[path.length - 1] === '$or' ||
|
|
358
|
+
path[path.length - 1] === '$find');
|
|
359
|
+
|
|
360
|
+
if (!opConfig) {
|
|
361
|
+
console.error(`Opérateur non reconnu: ${operator}`);
|
|
362
|
+
return <div>Erreur: Opérateur non reconnu</div>;
|
|
363
|
+
}
|
|
364
|
+
// Dans ExpressionField, remplacer la partie qui gère opConfig.converter par:
|
|
365
|
+
if (opConfig.converter || (!isMulti && !isFixedArgs)) {
|
|
366
|
+
// Rendu pour les convertisseurs et opérateurs mono-argument
|
|
367
|
+
return (
|
|
368
|
+
<div className="expression-block single-arg-block">
|
|
369
|
+
<div className="expression-header">
|
|
370
|
+
<span className="operator">{operator}</span>
|
|
371
|
+
{editing && (
|
|
372
|
+
<div className="operator-selector-overlay">
|
|
373
|
+
<OperatorSelector onSelect={handleOperatorSelect} />
|
|
374
|
+
<button type="button" onClick={() => setEditing(false)}>Annuler</button>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
<div className="expression-actions">
|
|
378
|
+
<button type="button" onClick={() => setEditing(!editing)}><FaRepeat /></button>
|
|
379
|
+
<button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle /></button>
|
|
380
|
+
{!parentIsLogicalOrFind && (
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
className="switch-to-expr"
|
|
384
|
+
onClick={() => {
|
|
385
|
+
// Convertir en valeur simple
|
|
386
|
+
onChange(args, path);
|
|
387
|
+
}}
|
|
388
|
+
title={i18n.t("cb.enterValue")}
|
|
389
|
+
>
|
|
390
|
+
<FaEdit/>
|
|
391
|
+
</button>
|
|
392
|
+
)}
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => onChange(null, path)}
|
|
396
|
+
className="remove-expr"
|
|
397
|
+
title={i18n.t("cb.deleteBlock")}
|
|
398
|
+
>
|
|
399
|
+
<FaTrash/>
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="single-arg">
|
|
404
|
+
<ExpressionField
|
|
405
|
+
value={args} // args est directement la valeur pour mono-argument
|
|
406
|
+
onChange={(newVal) => {
|
|
407
|
+
const newValue = { [operator]: newVal };
|
|
408
|
+
onChange(newValue, path);
|
|
409
|
+
setCurrentValue(newValue);
|
|
410
|
+
}}
|
|
411
|
+
fieldNames={fieldNames}
|
|
412
|
+
path={[...path, operator]}
|
|
413
|
+
models={models}
|
|
414
|
+
currentModelFields={currentModelFields}
|
|
415
|
+
isInFindContext={isInFindContext}
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
// Rendu pour les opérateurs avec nombre d'arguments fixe
|
|
422
|
+
if (isFixedArgs) {
|
|
423
|
+
return (
|
|
424
|
+
<div className="expression-block fixed-args-block">
|
|
425
|
+
<div className="expression-header">
|
|
426
|
+
<span className="operator">{operator}</span>
|
|
427
|
+
{editing && (
|
|
428
|
+
<div className="operator-selector-overlay">
|
|
429
|
+
<OperatorSelector onSelect={handleOperatorSelect}/>
|
|
430
|
+
<button type="button" onClick={() => setEditing(false)}>Annuler</button>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
<div className="expression-actions">
|
|
434
|
+
<button type="button" onClick={() => setEditing(!editing)}><FaRepeat/></button>
|
|
435
|
+
<button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle /></button>
|
|
436
|
+
{!parentIsLogicalOrFind && (<button
|
|
437
|
+
type="button"
|
|
438
|
+
className="switch-to-expr"
|
|
439
|
+
onClick={() => {
|
|
440
|
+
// Convertir l'expression en valeur simple
|
|
441
|
+
onChange("", path);
|
|
442
|
+
}}
|
|
443
|
+
title={i18n.t("cb.enterValue")}
|
|
444
|
+
>
|
|
445
|
+
<FaEdit/>
|
|
446
|
+
</button>)}
|
|
447
|
+
<button
|
|
448
|
+
type="button"
|
|
449
|
+
onClick={() => onChange(null, path)} // Envoie null pour indiquer la suppression
|
|
450
|
+
className="remove-expr"
|
|
451
|
+
title={i18n.t("cb.deleteBlock")}
|
|
452
|
+
>
|
|
453
|
+
<FaTrash/>
|
|
454
|
+
</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div className="expression-args">
|
|
458
|
+
{Array.isArray(args) && args.map((arg, i) => (
|
|
459
|
+
<div key={`arg-${i}`} className="expression-arg">
|
|
460
|
+
<ExpressionField
|
|
461
|
+
value={arg}
|
|
462
|
+
onChange={(newVal) => {
|
|
463
|
+
const updatedArgs = [...args];
|
|
464
|
+
updatedArgs[i] = newVal;
|
|
465
|
+
onChange({[operator]: updatedArgs}, path);
|
|
466
|
+
}}
|
|
467
|
+
fieldNames={fieldNames}
|
|
468
|
+
path={[...path, operator, i]}
|
|
469
|
+
models={models}
|
|
470
|
+
currentModelFields={currentModelFields}
|
|
471
|
+
isInFindContext={isInFindContext}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return (
|
|
480
|
+
<div className="expression-block">
|
|
481
|
+
<div className="expression-header">
|
|
482
|
+
<span className="operator">{operator}</span>
|
|
483
|
+
{editing && (
|
|
484
|
+
<div className="operator-selector-overlay">
|
|
485
|
+
<OperatorSelector onSelect={handleOperatorSelect}/>
|
|
486
|
+
<button type="button" onClick={() => setEditing(false)}><Trans i18nKey={"cb.cancel"}>Annuler</Trans></button>
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
<div className="expression-actions">
|
|
490
|
+
{!parentIsLogicalOrFind && (
|
|
491
|
+
<button
|
|
492
|
+
type="button"
|
|
493
|
+
className="switch-to-expr"
|
|
494
|
+
onClick={() => onChange("", path)}
|
|
495
|
+
title={i18n.t("cb.enterValue")}
|
|
496
|
+
>
|
|
497
|
+
<FaEdit/>
|
|
498
|
+
</button>
|
|
499
|
+
)}
|
|
500
|
+
<button type="button" onClick={() => setEditing(!editing)}><FaRepeat/></button>
|
|
501
|
+
<button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle/></button>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
<div className="expression-args">
|
|
505
|
+
{Array.isArray(args) ? (
|
|
506
|
+
<>
|
|
507
|
+
{args.map((arg, i) => renderArgument(arg, i, operator, args))}
|
|
508
|
+
{opConfig.multi && (
|
|
509
|
+
<button
|
|
510
|
+
type="button"
|
|
511
|
+
onClick={() => {
|
|
512
|
+
const updatedArgs = [...args, operator === '$and' || operator === '$or' || operator === '$not' || operator === '$nor' ? {$eq: ["", ""]} : ""];
|
|
513
|
+
const newValue = {[operator]: updatedArgs};
|
|
514
|
+
onChange(newValue, path);
|
|
515
|
+
setCurrentValue(newValue);
|
|
516
|
+
}}
|
|
517
|
+
className="add-arg"
|
|
518
|
+
>
|
|
519
|
+
<FaPlus/> <Trans i18nKey={"cb.addArgument"}>Ajouter un argument</Trans>
|
|
520
|
+
</button>
|
|
521
|
+
)}
|
|
522
|
+
</>
|
|
523
|
+
) : (
|
|
524
|
+
<ExpressionField
|
|
525
|
+
value={args}
|
|
526
|
+
onChange={(newVal) => {
|
|
527
|
+
onChange({[operator]: newVal}, path);
|
|
528
|
+
}}
|
|
529
|
+
fieldNames={fieldNames}
|
|
530
|
+
path={[...path, operator]}
|
|
531
|
+
models={models}
|
|
532
|
+
currentModelFields={currentModelFields}
|
|
533
|
+
/>
|
|
534
|
+
)}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const parentOperator = path.length > 1 ? path[path.length - 1] : null;
|
|
540
|
+
const parentOpConfig = parentOperator ? MONGO_CALC_OPERATORS[parentOperator] : null;
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<div className="simple-value">
|
|
545
|
+
<div className="simple-value-container">
|
|
546
|
+
<FieldInput
|
|
547
|
+
value={currentValue}
|
|
548
|
+
onChange={(newValue) => {
|
|
549
|
+
setCurrentValue(newValue);
|
|
550
|
+
onChange(convertInputValue(newValue), path);
|
|
551
|
+
}}
|
|
552
|
+
fieldNames={fieldNames}
|
|
553
|
+
isInFindContext={isInFindContext}
|
|
554
|
+
currentModelFields={currentModelFields} // On propage le contexte
|
|
555
|
+
/>
|
|
556
|
+
|
|
557
|
+
{/* --- MODIFICATION --- */}
|
|
558
|
+
{/* On affiche le bouton de conversion uniquement si l'opérateur parent le permet */}
|
|
559
|
+
{parentOpConfig?.disableAdvancedValue !== true && (
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
className="switch-to-expr"
|
|
563
|
+
onClick={() => {
|
|
564
|
+
gtag('event', '[cb] advanced');
|
|
565
|
+
// On transforme la valeur simple en une expression $eq par défaut
|
|
566
|
+
onChange({ $eq: [currentValue, ""] }, path);
|
|
567
|
+
}}
|
|
568
|
+
title={i18n.t("cb.convertToExpression")}
|
|
569
|
+
>
|
|
570
|
+
<FaProjectDiagram />
|
|
571
|
+
</button>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
);
|
|
576
|
+
};
|
|
577
|
+
const FieldInput = ({
|
|
578
|
+
value,
|
|
579
|
+
onChange,
|
|
580
|
+
fieldNames,
|
|
581
|
+
isFieldName = false,
|
|
582
|
+
isInFindContext = false,
|
|
583
|
+
currentModelFields = [],
|
|
584
|
+
onlyRelations = false
|
|
585
|
+
}) => {
|
|
586
|
+
const [inputValue, setInputValue] = useState(value);
|
|
587
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
588
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
589
|
+
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
setInputValue(value);
|
|
592
|
+
}, [value]);
|
|
593
|
+
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
// Filtrer les suggestions selon le contexte
|
|
596
|
+
let filtered = fieldNames;
|
|
597
|
+
if (onlyRelations && currentModelFields.length > 0) {
|
|
598
|
+
filtered = currentModelFields
|
|
599
|
+
.filter(field => field.type === 'relation')
|
|
600
|
+
.map(field => field.name);
|
|
601
|
+
}
|
|
602
|
+
setSuggestions(filtered);
|
|
603
|
+
}, [fieldNames, currentModelFields, onlyRelations]);
|
|
604
|
+
|
|
605
|
+
const handleInputChange = (e) => {
|
|
606
|
+
const val = e.target.value;
|
|
607
|
+
setInputValue(val);
|
|
608
|
+
|
|
609
|
+
const prefix = isFieldName ? '' : (isInFindContext ? '$$this.' : '$');
|
|
610
|
+
|
|
611
|
+
if (val.startsWith(prefix)) {
|
|
612
|
+
const searchTerm = val.substring(prefix.length).toLowerCase();
|
|
613
|
+
const filtered = suggestions.filter(name =>
|
|
614
|
+
name.toLowerCase().includes(searchTerm));
|
|
615
|
+
setSuggestions(filtered);
|
|
616
|
+
setShowSuggestions(true);
|
|
617
|
+
} else {
|
|
618
|
+
setShowSuggestions(false);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
onChange(val);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const handleSuggestionClick = (suggestion) => {
|
|
625
|
+
const prefix = isFieldName ? '' : (isInFindContext ? '$$this.' : '$');
|
|
626
|
+
const newValue = `${prefix}${suggestion}`;
|
|
627
|
+
setInputValue(newValue);
|
|
628
|
+
onChange(newValue);
|
|
629
|
+
setShowSuggestions(false);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
return (
|
|
633
|
+
<div className="field-input-container">
|
|
634
|
+
<input
|
|
635
|
+
type="text"
|
|
636
|
+
value={inputValue}
|
|
637
|
+
onChange={handleInputChange}
|
|
638
|
+
placeholder={
|
|
639
|
+
onlyRelations ? i18n.t("cb.selectRelationField") :
|
|
640
|
+
isInFindContext ?i18n.t("cb.enterThisField") :
|
|
641
|
+
i18n.t("cb.enterValueOrField")
|
|
642
|
+
}
|
|
643
|
+
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
|
644
|
+
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
|
645
|
+
/>
|
|
646
|
+
{showSuggestions && suggestions.length > 0 && (
|
|
647
|
+
<div className="suggestions-dropdown">
|
|
648
|
+
{suggestions.map((suggestion, index) => {
|
|
649
|
+
// Trouver le champ complet pour afficher des infos supplémentaires
|
|
650
|
+
const fieldInfo = currentModelFields.find(f => f.name === suggestion);
|
|
651
|
+
return (
|
|
652
|
+
<div
|
|
653
|
+
key={index}
|
|
654
|
+
className="suggestion-item"
|
|
655
|
+
onClick={() => handleSuggestionClick(suggestion)}
|
|
656
|
+
data-tooltip-id="field-tooltip"
|
|
657
|
+
data-tooltip-content={fieldInfo?.relation ? i18n.t('cb.relationTooltip', { relation: fieldInfo.relation}) : ''}
|
|
658
|
+
>
|
|
659
|
+
{suggestion}
|
|
660
|
+
{fieldInfo?.relation && (
|
|
661
|
+
<span className="relation-info">
|
|
662
|
+
<FaTags /> {fieldInfo.relation}
|
|
663
|
+
</span>
|
|
664
|
+
)}
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
})}
|
|
668
|
+
</div>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
};
|
|
673
|
+
export const ConditionBuilder2 = ({initialValue = {}, onChange, models, model}) => {
|
|
674
|
+
const { me: user } = useAuthContext();
|
|
675
|
+
const mod = (models||[]).find(f=>f.name===model && f._user === user.username ) || models.find(f => f._user === user.username) || null;
|
|
676
|
+
const fieldNames = mod?.fields?.map(f => f.name) || [];
|
|
677
|
+
const modelFields = mod?.fields || [];
|
|
678
|
+
|
|
679
|
+
// Parse initial value if it's a string
|
|
680
|
+
const parsedInitialValue = typeof initialValue === 'string'
|
|
681
|
+
? JSON.parse(initialValue)
|
|
682
|
+
: initialValue;
|
|
683
|
+
|
|
684
|
+
// L'état reste le même
|
|
685
|
+
const [expression, setExpression] = useState(() => ({ ...parsedInitialValue }));
|
|
686
|
+
|
|
687
|
+
const handleExpressionChange = (newExpr, path = []) => {
|
|
688
|
+
// Cas de suppression (quand newExpr est null)
|
|
689
|
+
if (newExpr === null) {
|
|
690
|
+
// Fonction récursive pour supprimer un élément dans une structure imbriquée
|
|
691
|
+
const removeAtPath = (obj, path) => {
|
|
692
|
+
if (path.length === 0) return null;
|
|
693
|
+
|
|
694
|
+
const [currentKey, ...remainingPath] = path;
|
|
695
|
+
|
|
696
|
+
if (remainingPath.length === 0) {
|
|
697
|
+
// Cas 1: Suppression dans un tableau
|
|
698
|
+
if (Array.isArray(obj)) {
|
|
699
|
+
const newArray = [...obj];
|
|
700
|
+
newArray.splice(currentKey, 1);
|
|
701
|
+
return newArray.length === 0 ? null : newArray;
|
|
702
|
+
}
|
|
703
|
+
// Cas 2: Suppression dans un objet
|
|
704
|
+
else if (obj && typeof obj === 'object') {
|
|
705
|
+
const newObj = {...obj};
|
|
706
|
+
delete newObj[currentKey];
|
|
707
|
+
return Object.keys(newObj).length === 0 ? null : newObj;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Traverser la structure
|
|
712
|
+
if (Array.isArray(obj)) {
|
|
713
|
+
const newArray = [...obj];
|
|
714
|
+
newArray[currentKey] = removeAtPath(obj[currentKey], remainingPath);
|
|
715
|
+
return newArray.filter(x => x !== null); // Retire les nulls
|
|
716
|
+
}
|
|
717
|
+
else if (obj && typeof obj === 'object') {
|
|
718
|
+
const newObj = {...obj};
|
|
719
|
+
newObj[currentKey] = removeAtPath(obj[currentKey], remainingPath);
|
|
720
|
+
return Object.keys(newObj).every(k => newObj[k] === null) ? null : newObj;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return obj;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const newExpression = path.length === 0
|
|
727
|
+
? {}
|
|
728
|
+
: removeAtPath(JSON.parse(JSON.stringify(expression)), path);
|
|
729
|
+
|
|
730
|
+
setExpression(newExpression || {});
|
|
731
|
+
if (onChange) onChange(newExpression || {});
|
|
732
|
+
}
|
|
733
|
+
// Cas normal de mise à jour
|
|
734
|
+
else {
|
|
735
|
+
setExpression(newExpr);
|
|
736
|
+
if (onChange) onChange(newExpr);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// Le rendu est maintenant beaucoup plus propre !
|
|
741
|
+
return (
|
|
742
|
+
<div className="expr-builder">
|
|
743
|
+
<div className="expression-container">
|
|
744
|
+
<ExpressionField
|
|
745
|
+
value={expression}
|
|
746
|
+
onChange={handleExpressionChange}
|
|
747
|
+
fieldNames={fieldNames}
|
|
748
|
+
// On passe des props supplémentaires pour gérer les actions au niveau racine
|
|
749
|
+
isRoot={true}
|
|
750
|
+
models={models}
|
|
751
|
+
currentModelFields={modelFields}
|
|
752
|
+
/>
|
|
753
|
+
</div>
|
|
754
|
+
<Tooltip id="field-tooltip"/>
|
|
755
|
+
</div>
|
|
756
|
+
);
|
|
757
|
+
};
|