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,622 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/ConditionBuilder.jsx
|
|
2
|
+
|
|
3
|
+
import React, {useState, useEffect, useRef} from 'react';
|
|
4
|
+
import {
|
|
5
|
+
FaPlus,
|
|
6
|
+
FaTrashAlt,
|
|
7
|
+
FaProjectDiagram,
|
|
8
|
+
FaChevronRight,
|
|
9
|
+
FaCode,
|
|
10
|
+
FaListAlt,
|
|
11
|
+
FaMinusCircle,
|
|
12
|
+
FaExchangeAlt
|
|
13
|
+
} from 'react-icons/fa';
|
|
14
|
+
import './ConditionBuilder.scss';
|
|
15
|
+
import { useTranslation, Trans } from "react-i18next";
|
|
16
|
+
import { CodeField } from "./Field.jsx";
|
|
17
|
+
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
18
|
+
import { MONGO_CALC_OPERATORS} from "./filter.js";
|
|
19
|
+
import {ConditionBuilder2} from "./ConditionBuilder2.jsx";
|
|
20
|
+
import {mongoOperators} from "./constants.js";
|
|
21
|
+
|
|
22
|
+
const getFieldDefinition = (fields, fieldName) => fields?.find(f => f.name === fieldName);
|
|
23
|
+
const getModelFields = (modelName, allModels, user) => {
|
|
24
|
+
const model = allModels?.find(m => m.name === modelName && (m._user === user?.username || !m._user));
|
|
25
|
+
return model?.fields || [];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ConditionRow = ({ condition, onChange, onRemove, allModels, currentModel, user }) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const [showCalcEditor, setShowCalcEditor] = useState(!!condition.transform);
|
|
31
|
+
|
|
32
|
+
const [fieldPath, setFieldPath] = useState(condition.path || []);
|
|
33
|
+
const [operator, setOperator] = useState(condition.op || '$eq');
|
|
34
|
+
const [value, setValue] = useState(condition.value ?? '');
|
|
35
|
+
const [transform, setTransform] = useState(condition.transform || null);
|
|
36
|
+
|
|
37
|
+
const modelFields = getModelFields(currentModel, allModels, user);
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const updatePath = (levelIndex, selectedField) => {
|
|
41
|
+
const newPath = [...fieldPath.slice(0, levelIndex), selectedField];
|
|
42
|
+
setFieldPath(newPath);
|
|
43
|
+
setValue('');
|
|
44
|
+
setTransform(null);
|
|
45
|
+
onChange({ path: newPath, op: operator, value: '', transform: null });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getFinalFieldDef = () => {
|
|
49
|
+
let fields = modelFields;
|
|
50
|
+
let def = null;
|
|
51
|
+
for (let i = 0; i < fieldPath.length; i++) {
|
|
52
|
+
def = getFieldDefinition(fields, fieldPath[i]);
|
|
53
|
+
if (!def) return null;
|
|
54
|
+
fields = def.type === 'relation' ? getModelFields(def.relation, allModels, user) : [];
|
|
55
|
+
}
|
|
56
|
+
return def;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const def = getFinalFieldDef();
|
|
61
|
+
const newCondition = {
|
|
62
|
+
path: fieldPath,
|
|
63
|
+
op: operator,
|
|
64
|
+
value,
|
|
65
|
+
transform
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Si c'est une transformation pure sans opérateur, on nettoie la valeur
|
|
69
|
+
if (transform && !operator) {
|
|
70
|
+
newCondition.value = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Comparaison profonde pour éviter les mises à jour inutiles
|
|
74
|
+
const oldCondition = { path: condition.path || [], op: condition.op, value: condition.value, transform: condition.transform };
|
|
75
|
+
if (JSON.stringify(newCondition) !== JSON.stringify(oldCondition)) {
|
|
76
|
+
onChange(newCondition);
|
|
77
|
+
}
|
|
78
|
+
}, [operator, value, transform, fieldPath, condition.path, condition.op, condition.value, condition.transform, onChange]);
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
const renderValueInput = () => {
|
|
82
|
+
if (fieldPath.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
const def = getFinalFieldDef();
|
|
85
|
+
const isDate = def?.type === 'date';
|
|
86
|
+
const isDatetime = def?.type === 'datetime';
|
|
87
|
+
const isBoolean = def?.type === 'boolean';
|
|
88
|
+
const isEnum = def?.type === 'enum';
|
|
89
|
+
const isNumber = def?.type === 'number';
|
|
90
|
+
const isString = def?.type === 'string';
|
|
91
|
+
|
|
92
|
+
// Si l'opérateur existe et n'est pas '$exists'
|
|
93
|
+
if (operator && operator !== '$exists') {
|
|
94
|
+
return (
|
|
95
|
+
<div className="value-input-container">
|
|
96
|
+
{showCalcEditor ? (
|
|
97
|
+
<CalculationEditor
|
|
98
|
+
expr={transform || { op: '$add', args: [`$${fieldPath.join('.')}`] }}
|
|
99
|
+
onChange={(newTransform) => {
|
|
100
|
+
setTransform(newTransform);
|
|
101
|
+
if (!operator) setValue(undefined);
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<input
|
|
106
|
+
placeholder={t('value', 'Valeur')}
|
|
107
|
+
value={value || ''}
|
|
108
|
+
onChange={(e) => setValue(e.target.value)}
|
|
109
|
+
className="condition-value-input"
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
{/* Bouton pour basculer l'éditeur de calcul */}
|
|
113
|
+
<button type="button" onClick={() => setShowCalcEditor(!showCalcEditor)} title="Utiliser une expression calculée">
|
|
114
|
+
fx
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (operator === '$exists') {
|
|
120
|
+
return (
|
|
121
|
+
<select
|
|
122
|
+
value={String(value === true)}
|
|
123
|
+
onChange={(e) => setValue(e.target.value === 'true')}
|
|
124
|
+
className="condition-value-input"
|
|
125
|
+
>
|
|
126
|
+
<option value="true">{t('exists', 'Existe')}</option>
|
|
127
|
+
<option value="false">{t('doesNotExist', 'N\'existe pas')}</option>
|
|
128
|
+
</select>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isEnum && def.items) {
|
|
133
|
+
return (
|
|
134
|
+
<select
|
|
135
|
+
value={value || ''}
|
|
136
|
+
onChange={(e) => setValue(e.target.value)}
|
|
137
|
+
className="condition-value-input"
|
|
138
|
+
>
|
|
139
|
+
<option value="">{t('selectValue', 'Choisir une valeur...')}</option>
|
|
140
|
+
{def.items.map(item => (
|
|
141
|
+
<option key={item} value={item}>{t(item, item)}</option>
|
|
142
|
+
))}
|
|
143
|
+
</select>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (isBoolean) {
|
|
148
|
+
return (
|
|
149
|
+
<select
|
|
150
|
+
value={String(value === true)}
|
|
151
|
+
onChange={(e) => setValue(e.target.value === 'true')}
|
|
152
|
+
className="condition-value-input"
|
|
153
|
+
>
|
|
154
|
+
<option value="true">{t('true', 'Vrai')}</option>
|
|
155
|
+
<option value="false">{t('false', 'Faux')}</option>
|
|
156
|
+
</select>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (isDate || isDatetime || isNumber || isString) {
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<CalculationEditor
|
|
164
|
+
expr={transform || { op: '$add', args: [`$${fieldPath.join('.')}`] }}
|
|
165
|
+
onChange={(newTransform) => {
|
|
166
|
+
setTransform(newTransform);
|
|
167
|
+
// Effacer la valeur si c'est une transformation pure
|
|
168
|
+
if (!operator) {
|
|
169
|
+
setValue(undefined);
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
{operator && operator !== '$exists' && (
|
|
174
|
+
<input
|
|
175
|
+
placeholder={t('value', 'Valeur')}
|
|
176
|
+
value={value || ''}
|
|
177
|
+
onChange={(e) => setValue(e.target.value)}
|
|
178
|
+
className="condition-value-input"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<input
|
|
187
|
+
placeholder={t('value', 'Valeur')}
|
|
188
|
+
value={value}
|
|
189
|
+
onChange={(e) => setValue(e.target.value)}
|
|
190
|
+
className="condition-value-input"
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
const renderFieldSelectors = () => {
|
|
195
|
+
const selectors = [];
|
|
196
|
+
let currentFieldsForLevel = modelFields;
|
|
197
|
+
|
|
198
|
+
for (let i = 0; ; i++) {
|
|
199
|
+
const currentPathSegment = fieldPath[i] || '';
|
|
200
|
+
if (!currentFieldsForLevel || currentFieldsForLevel.length === 0) break;
|
|
201
|
+
|
|
202
|
+
selectors.push(
|
|
203
|
+
<span key={i} className="field-selector">
|
|
204
|
+
{i > 0 && <FaChevronRight className="path-separator" />}
|
|
205
|
+
<select
|
|
206
|
+
value={currentPathSegment}
|
|
207
|
+
onChange={(e) => updatePath(i, e.target.value)}
|
|
208
|
+
className="condition-field-select"
|
|
209
|
+
>
|
|
210
|
+
<option value="">{t('selectField', 'Champ...')}</option>
|
|
211
|
+
<option value="_id">_id</option>
|
|
212
|
+
{currentFieldsForLevel.map(f => (
|
|
213
|
+
<option key={f.name} value={f.name}>
|
|
214
|
+
{t(`field_${currentModel}_${f.name}`, f.name)}
|
|
215
|
+
{f.type === 'relation' ? ' (...)' : ''}
|
|
216
|
+
</option>
|
|
217
|
+
))}
|
|
218
|
+
</select>
|
|
219
|
+
</span>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (!currentPathSegment) break;
|
|
223
|
+
const fieldDef = getFieldDefinition(currentFieldsForLevel, currentPathSegment);
|
|
224
|
+
if (fieldDef?.type === 'relation') {
|
|
225
|
+
currentFieldsForLevel = getModelFields(fieldDef.relation, allModels, user);
|
|
226
|
+
} else {
|
|
227
|
+
currentFieldsForLevel = [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return selectors;
|
|
231
|
+
};
|
|
232
|
+
return (
|
|
233
|
+
<div className="condition-node condition-simple">
|
|
234
|
+
{renderFieldSelectors()}
|
|
235
|
+
{fieldPath.length > 0 && (
|
|
236
|
+
<>
|
|
237
|
+
<select
|
|
238
|
+
value={operator || ''}
|
|
239
|
+
onChange={(e) => {
|
|
240
|
+
setOperator(e.target.value || undefined);
|
|
241
|
+
if (!e.target.value) {
|
|
242
|
+
setValue(undefined);
|
|
243
|
+
}
|
|
244
|
+
}}
|
|
245
|
+
className="condition-operator-select"
|
|
246
|
+
>
|
|
247
|
+
<option value="">{t('noComparison', 'Transformation seule')}</option>
|
|
248
|
+
{mongoOperators.map(o => (
|
|
249
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
250
|
+
))}
|
|
251
|
+
</select>
|
|
252
|
+
{renderValueInput()}
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
<button onClick={onRemove} className="delete-button" title={t('conditionBuilder.deleteCondition')}>
|
|
256
|
+
<FaTrashAlt />
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
const ConditionGroup = ({ node, onChange, allModels, currentModel, user }) => {
|
|
262
|
+
const { t } = useTranslation();
|
|
263
|
+
const [type, setType] = useState(node.$and ? '$and' : '$or');
|
|
264
|
+
const [children, setChildren] = useState(node[type] || []);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
onChange({ [type]: children });
|
|
268
|
+
}, [type, children]);
|
|
269
|
+
|
|
270
|
+
const updateChild = (index, newChild) => {
|
|
271
|
+
const newChildren = [...children];
|
|
272
|
+
newChildren[index] = newChild;
|
|
273
|
+
setChildren(newChildren);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const removeChild = (index) => {
|
|
277
|
+
setChildren(children.filter((_, i) => i !== index));
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const addCondition = () => {
|
|
281
|
+
gtag('event', '[cb] add condition');
|
|
282
|
+
setChildren([...children, { path: [], op: '$eq', value: '' }]);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const addGroup = (groupType) => {
|
|
286
|
+
gtag('event', '[cb] add group');
|
|
287
|
+
setChildren([...children, { [groupType]: [] }]);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div className={`condition-node condition-group ${type === '$and' ? 'condition-and' : 'condition-or'}`}>
|
|
292
|
+
<div className="group-header">
|
|
293
|
+
<select value={type} onChange={(e) => setType(e.target.value)}>
|
|
294
|
+
<option value="$and">{t('conditionBuilder.logical.and', 'ET')}</option>
|
|
295
|
+
<option value="$or">{t('conditionBuilder.logical.or', 'OU')}</option>
|
|
296
|
+
</select>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="group-content">
|
|
299
|
+
{children.map((child, index) => (
|
|
300
|
+
child.path ? (
|
|
301
|
+
<ConditionRow
|
|
302
|
+
key={index}
|
|
303
|
+
condition={child}
|
|
304
|
+
onChange={(updated) => updateChild(index, updated)}
|
|
305
|
+
onRemove={() => removeChild(index)}
|
|
306
|
+
allModels={allModels}
|
|
307
|
+
currentModel={currentModel}
|
|
308
|
+
user={user}
|
|
309
|
+
/>
|
|
310
|
+
) : (
|
|
311
|
+
<ConditionGroup
|
|
312
|
+
key={index}
|
|
313
|
+
node={child}
|
|
314
|
+
onChange={(updated) => updateChild(index, updated)}
|
|
315
|
+
allModels={allModels}
|
|
316
|
+
currentModel={currentModel}
|
|
317
|
+
user={user}
|
|
318
|
+
/>
|
|
319
|
+
)
|
|
320
|
+
))}
|
|
321
|
+
<div className="add-buttons">
|
|
322
|
+
<button type={"button"} onClick={addCondition}><FaPlus /> <Trans i18nKey="addCondition">Condition</Trans></button>
|
|
323
|
+
<button type={"button"} onClick={() => addGroup('$and')}><FaProjectDiagram /> <Trans i18nKey="conditionBuilder.andGroup">Groupe ET</Trans></button>
|
|
324
|
+
<button type={"button"} onClick={() => addGroup('$or')}><FaProjectDiagram /> <Trans i18nKey="conditionBuilder.orGroup">Groupe OU</Trans></button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const CalculationEditor = ({ expr, onChange }) => {
|
|
332
|
+
const { t } = useTranslation();
|
|
333
|
+
const [currentOp, setCurrentOp] = useState(expr?.op || '$add');
|
|
334
|
+
const [args, setArgs] = useState(expr?.args || []);
|
|
335
|
+
const isConstant = currentOp === '$const';
|
|
336
|
+
const isConverter = MONGO_CALC_OPERATORS[currentOp]?.converter;
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (isConstant && args.length === 0) {
|
|
340
|
+
setArgs(['']); // Initialiser avec une valeur vide pour les constantes
|
|
341
|
+
}
|
|
342
|
+
onChange?.({ op: currentOp, args });
|
|
343
|
+
}, [currentOp, args]);
|
|
344
|
+
|
|
345
|
+
const updateArg = (index, value) => {
|
|
346
|
+
const updated = [...args];
|
|
347
|
+
updated[index] = value;
|
|
348
|
+
setArgs(updated);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const addArg = () => {
|
|
352
|
+
if (isConstant) return; // Pas d'ajout d'argument pour les constantes
|
|
353
|
+
setArgs([...args, '']);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const removeArg = (index) => {
|
|
357
|
+
if (isConstant) return; // Pas de suppression pour les constantes
|
|
358
|
+
setArgs(args.filter((_, i) => i !== index));
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const toggleArgType = (index) => {
|
|
362
|
+
const arg = args[index];
|
|
363
|
+
const isObject = typeof arg === 'object' && arg !== null;
|
|
364
|
+
|
|
365
|
+
if (isObject) {
|
|
366
|
+
// Convertir en valeur simple
|
|
367
|
+
updateArg(index, '');
|
|
368
|
+
} else {
|
|
369
|
+
// Convertir en expression (par défaut $add)
|
|
370
|
+
updateArg(index, { op: '$add', args: [arg || ''] });
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const renderConstantInput = () => {
|
|
375
|
+
const value = args[0] ?? '';
|
|
376
|
+
return (
|
|
377
|
+
<div className="constant-input">
|
|
378
|
+
<select
|
|
379
|
+
value={typeof value}
|
|
380
|
+
onChange={(e) => {
|
|
381
|
+
// Convertir le type de la constante
|
|
382
|
+
const newValue = e.target.value === 'boolean' ? true :
|
|
383
|
+
e.target.value === 'number' ? 0 : '';
|
|
384
|
+
updateArg(0, newValue);
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<option value="string">String</option>
|
|
388
|
+
<option value="number">Nombre</option>
|
|
389
|
+
<option value="boolean">Booléen</option>
|
|
390
|
+
</select>
|
|
391
|
+
|
|
392
|
+
{typeof value === 'boolean' ? (
|
|
393
|
+
<select
|
|
394
|
+
value={String(value)}
|
|
395
|
+
onChange={(e) => updateArg(0, e.target.value === 'true')}
|
|
396
|
+
>
|
|
397
|
+
<option value="true">Vrai</option>
|
|
398
|
+
<option value="false">Faux</option>
|
|
399
|
+
</select>
|
|
400
|
+
) : (
|
|
401
|
+
<input
|
|
402
|
+
type={typeof value === 'number' ? 'number' : 'text'}
|
|
403
|
+
value={value}
|
|
404
|
+
onChange={(e) => {
|
|
405
|
+
const val = typeof value === 'number' ?
|
|
406
|
+
Number(e.target.value) : e.target.value;
|
|
407
|
+
updateArg(0, val);
|
|
408
|
+
}}
|
|
409
|
+
placeholder="Valeur constante"
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const renderArgInput = (arg, index) => {
|
|
417
|
+
const isObject = typeof arg === 'object' && arg !== null;
|
|
418
|
+
|
|
419
|
+
return isObject ? (
|
|
420
|
+
<div className="nested-calc-editor">
|
|
421
|
+
<CalculationEditor
|
|
422
|
+
expr={arg}
|
|
423
|
+
onChange={(val) => updateArg(index, val)}
|
|
424
|
+
/>
|
|
425
|
+
<div className="arg-actions">
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={() => toggleArgType(index)}
|
|
429
|
+
className="icon-btn"
|
|
430
|
+
title={t('convertToValue', 'Convertir en valeur simple')}
|
|
431
|
+
>
|
|
432
|
+
<FaCode />
|
|
433
|
+
</button>
|
|
434
|
+
<FaMinusCircle
|
|
435
|
+
onClick={() => removeArg(index)}
|
|
436
|
+
className="icon-btn"
|
|
437
|
+
title={t('removeArg', 'Supprimer cet argument')}
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
) : (
|
|
442
|
+
<div className="simple-arg">
|
|
443
|
+
<input
|
|
444
|
+
placeholder={t('value', 'Valeur')}
|
|
445
|
+
value={arg}
|
|
446
|
+
onChange={(e) => updateArg(index, e.target.value)}
|
|
447
|
+
/>
|
|
448
|
+
<div className="arg-actions">
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
onClick={() => toggleArgType(index)}
|
|
452
|
+
className="icon-btn"
|
|
453
|
+
title={t('convertToExpression', 'Convertir en expression')}
|
|
454
|
+
>
|
|
455
|
+
<FaProjectDiagram />
|
|
456
|
+
</button>
|
|
457
|
+
<FaMinusCircle
|
|
458
|
+
onClick={() => removeArg(index)}
|
|
459
|
+
className="icon-btn"
|
|
460
|
+
title={t('removeArg', 'Supprimer cet argument')}
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
if ((isConverter || isConstant) && args.length === 0) {
|
|
469
|
+
setArgs(['']); // Initialiser avec une valeur vide
|
|
470
|
+
}
|
|
471
|
+
onChange?.({ op: currentOp, args });
|
|
472
|
+
}, [currentOp, args]);
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
const renderConverterInput = () => {
|
|
476
|
+
return (
|
|
477
|
+
<div className="converter-input">
|
|
478
|
+
{args[0] && typeof args[0] === 'object' && args[0].op ? (
|
|
479
|
+
<CalculationEditor
|
|
480
|
+
expr={args[0]}
|
|
481
|
+
onChange={(val) => updateArg(0, val)}
|
|
482
|
+
/>
|
|
483
|
+
) : (
|
|
484
|
+
<input
|
|
485
|
+
placeholder="Expression ou valeur"
|
|
486
|
+
value={args[0] || ''}
|
|
487
|
+
onChange={(e) => updateArg(0, e.target.value)}
|
|
488
|
+
/>
|
|
489
|
+
)}
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
onClick={() => toggleArgType(0)}
|
|
493
|
+
className="icon-btn"
|
|
494
|
+
title={t('toggleArgType', 'Basculer entre valeur et expression')}
|
|
495
|
+
>
|
|
496
|
+
<FaExchangeAlt />
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
return (
|
|
502
|
+
<div className="calc-editor">
|
|
503
|
+
<select
|
|
504
|
+
value={currentOp}
|
|
505
|
+
onChange={(e) => {
|
|
506
|
+
setCurrentOp(e.target.value);
|
|
507
|
+
const isNewConverter = MONGO_CALC_OPERATORS[e.target.value]?.converter;
|
|
508
|
+
if (isNewConverter || e.target.value === '$const') {
|
|
509
|
+
setArgs(['']);
|
|
510
|
+
}
|
|
511
|
+
}}
|
|
512
|
+
className="calc-operator-select"
|
|
513
|
+
>
|
|
514
|
+
<option value="$const">Constante</option>
|
|
515
|
+
<optgroup label="Conversions">
|
|
516
|
+
<option value="$toBool">toBool</option>
|
|
517
|
+
<option value="$toString">toString</option>
|
|
518
|
+
<option value="$toInt">toInt</option>
|
|
519
|
+
<option value="$toDouble">toDouble</option>
|
|
520
|
+
</optgroup>
|
|
521
|
+
<optgroup label="Opérations">
|
|
522
|
+
{Object.entries(MONGO_CALC_OPERATORS)
|
|
523
|
+
.filter(([_, def]) => !def.converter)
|
|
524
|
+
.map(([key, def]) => (
|
|
525
|
+
<option key={key} value={key}>{def.label}</option>
|
|
526
|
+
))}
|
|
527
|
+
</optgroup>
|
|
528
|
+
</select>
|
|
529
|
+
|
|
530
|
+
{isConstant ? (
|
|
531
|
+
renderConstantInput()
|
|
532
|
+
) : isConverter ? (
|
|
533
|
+
renderConverterInput()
|
|
534
|
+
) : (
|
|
535
|
+
<>
|
|
536
|
+
<div className="calc-args">
|
|
537
|
+
{args.map((arg, index) => (
|
|
538
|
+
<div key={index} className="calc-arg-wrapper">
|
|
539
|
+
{renderArgInput(arg, index)}
|
|
540
|
+
</div>
|
|
541
|
+
))}
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
onClick={addArg}
|
|
547
|
+
className="add-arg-button"
|
|
548
|
+
disabled={isConstant || isConverter}
|
|
549
|
+
>
|
|
550
|
+
<FaPlus /> {t('addArg', 'Ajouter un argument')}
|
|
551
|
+
</button>
|
|
552
|
+
</>
|
|
553
|
+
)}
|
|
554
|
+
</div>
|
|
555
|
+
);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const ConditionBuilder = ({ initialValue = null, onChange, models, model = null, label = null}) => {
|
|
559
|
+
const { t } = useTranslation();
|
|
560
|
+
const [root, setRoot] = useState(initialValue);
|
|
561
|
+
const [showJsonEditor, setShowJsonEditor] = useState(false);
|
|
562
|
+
const [jsonError, setJsonError] = useState('');
|
|
563
|
+
const { me:user } = useAuthContext()
|
|
564
|
+
|
|
565
|
+
const handleJsonChange = (e) => {
|
|
566
|
+
const p = typeof(e.value) === 'string' ? JSON.parse(e.value) : e.value;
|
|
567
|
+
setRoot(p);
|
|
568
|
+
onChange(p);
|
|
569
|
+
};
|
|
570
|
+
// C:/Dev/hackersonline-engine/client/src/ConditionBuilder.jsx
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
let parsedValue = {}; // Valeur par défaut si tout échoue
|
|
573
|
+
|
|
574
|
+
if (typeof initialValue === 'string' && initialValue.trim().startsWith('{')) {
|
|
575
|
+
try {
|
|
576
|
+
// Tenter de parser la chaîne en objet JSON
|
|
577
|
+
parsedValue = JSON.parse(initialValue);
|
|
578
|
+
} catch (e) {
|
|
579
|
+
console.error("ConditionBuilder: Impossible de parser la chaîne JSON initialValue.", initialValue, e);
|
|
580
|
+
parsedValue = {}; // Revenir à un état sûr en cas d'erreur
|
|
581
|
+
}
|
|
582
|
+
} else if (typeof initialValue === 'object' && initialValue !== null) {
|
|
583
|
+
// Si c'est déjà un objet, on le prend tel quel
|
|
584
|
+
parsedValue = initialValue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
setRoot(parsedValue);
|
|
588
|
+
}, [initialValue]); // <-- Très important : réagir aux changements de la prop
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div className="condition-builder">
|
|
593
|
+
{label && <label>{label}</label>}
|
|
594
|
+
<button type="button" onClick={() => {
|
|
595
|
+
setShowJsonEditor(!showJsonEditor);
|
|
596
|
+
gtag('event', '[cb] show ' + (showJsonEditor ? 'CB' : 'JSON'));
|
|
597
|
+
}} className="condition-builder-toggle-view">
|
|
598
|
+
{showJsonEditor ? <FaListAlt title={t('conditionBuilder.showBuilderTooltip', 'Afficher le constructeur')} /> : <FaCode title={t('conditionBuilder.showJsonTooltip', 'Afficher le JSON')} />}
|
|
599
|
+
</button>
|
|
600
|
+
{showJsonEditor ? (
|
|
601
|
+
<div className="condition-builder-json">
|
|
602
|
+
<CodeField
|
|
603
|
+
name="conditionJsonEditor"
|
|
604
|
+
language="json"
|
|
605
|
+
value={JSON.stringify(root, null, 2)}
|
|
606
|
+
onChange={handleJsonChange}
|
|
607
|
+
/>
|
|
608
|
+
{jsonError && <p className="condition-builder-json-error">{jsonError}</p>}
|
|
609
|
+
</div>
|
|
610
|
+
) : (
|
|
611
|
+
<ConditionBuilder2
|
|
612
|
+
initialValue={root}
|
|
613
|
+
onChange={onChange}
|
|
614
|
+
models={models}
|
|
615
|
+
model={model}
|
|
616
|
+
/>
|
|
617
|
+
)}
|
|
618
|
+
</div>
|
|
619
|
+
);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
export default ConditionBuilder;
|