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,197 @@
|
|
|
1
|
+
@import "_variables.scss";
|
|
2
|
+
|
|
3
|
+
.btn {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
gap: 6px;
|
|
6
|
+
align-items: center;
|
|
7
|
+
border-radius: 24px;
|
|
8
|
+
padding: 4px 8px;
|
|
9
|
+
@media only screen and (min-width: 480px){
|
|
10
|
+
padding: 0.6em 1.2em;
|
|
11
|
+
}
|
|
12
|
+
font-size: 1em;
|
|
13
|
+
font-weight: 500;
|
|
14
|
+
font-family: inherit;
|
|
15
|
+
background-color: white;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
transition: border-color 0.25s;
|
|
18
|
+
color: #979797;
|
|
19
|
+
border: 1px solid #B2BBC2;
|
|
20
|
+
span {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
&.active {
|
|
26
|
+
background-color: #3498db;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
.btn {
|
|
32
|
+
&.btn-ellipsis {
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
display: block;
|
|
37
|
+
}
|
|
38
|
+
&.btn-nav {
|
|
39
|
+
@media only screen and (max-width: 479px){
|
|
40
|
+
padding: 10px 22px;
|
|
41
|
+
margin: 7px;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// --- Styles Généraux pour les Boutons ---
|
|
48
|
+
.btn, button { // Cible les .btn et les <button> au cas où
|
|
49
|
+
box-shadow: 0 3px 6px $shadow-color; // Ombre subtile pour la profondeur
|
|
50
|
+
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, background-color 0.2s ease-out, filter 0.2s ease-out; // Transitions douces
|
|
51
|
+
will-change: transform, box-shadow; // Optimisation pour les animations
|
|
52
|
+
|
|
53
|
+
&:hover {
|
|
54
|
+
transform: translateY(-3px) scale(1.03); // Effet de soulèvement et léger zoom
|
|
55
|
+
box-shadow: 0 6px 12px $shadow-color-hover; // Ombre plus prononcée
|
|
56
|
+
//background-color: $primary-color-darker; // Assombrir légèrement
|
|
57
|
+
// filter: brightness(110%); // Alternative: éclaircir légèrement
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:active {
|
|
61
|
+
transform: translateY(-1px) scale(1.01); // Effet d'appui léger
|
|
62
|
+
box-shadow: 0 2px 4px $shadow-color; // Ombre réduite
|
|
63
|
+
filter: brightness(95%); // Assombrir un peu plus
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:disabled {
|
|
67
|
+
opacity: 0.6;
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
transform: none;
|
|
70
|
+
box-shadow: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Style spécifique pour les boutons primaires (Ex: Démo, Connexion)
|
|
74
|
+
&.btn-primary {
|
|
75
|
+
background-color: #3498db;
|
|
76
|
+
color: #f8f8f8;
|
|
77
|
+
&:hover {
|
|
78
|
+
color: white;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&.btn-big {
|
|
83
|
+
padding: 4px 24px;
|
|
84
|
+
font-size: 130%;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Style pour les boutons "danger" (Ex: Supprimer)
|
|
88
|
+
&.btn-danger {
|
|
89
|
+
background-color: #e74c3c; // Rouge
|
|
90
|
+
&:hover {
|
|
91
|
+
background-color: darken(#e74c3c, 10%);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Style pour les boutons secondaires/discrets (si nécessaire)
|
|
96
|
+
&.btn-secondary {
|
|
97
|
+
background-color: $light-gray;
|
|
98
|
+
color: $dark-gray;
|
|
99
|
+
border: 1px solid $medium-gray;
|
|
100
|
+
box-shadow: none;
|
|
101
|
+
&:hover {
|
|
102
|
+
background-color: $medium-gray;
|
|
103
|
+
border-color: darken($medium-gray, 10%);
|
|
104
|
+
transform: translateY(-2px); // Soullus discret
|
|
105
|
+
box-shadow: 0 4px 8px $shadow-color;
|
|
106
|
+
}
|
|
107
|
+
&:active {
|
|
108
|
+
background-color: darken($medium-gray, 5%);
|
|
109
|
+
transform: translateY(0);
|
|
110
|
+
box-shadow: none;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
.dark .btn {
|
|
117
|
+
color: #f1f1f1;
|
|
118
|
+
background-color: black;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.btn:hover {
|
|
122
|
+
border-color: $primary-color;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.btn:focus,
|
|
126
|
+
.btn:focus-visible {
|
|
127
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.btn-disabled:hover {
|
|
131
|
+
border-color: transparent;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.btn-disabled:focus,
|
|
135
|
+
.btn-disabled:focus-visible {
|
|
136
|
+
outline: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.btn-disabled {
|
|
140
|
+
color: #575757;
|
|
141
|
+
background-color: #f9f9f9;
|
|
142
|
+
cursor: not-allowed;
|
|
143
|
+
}
|
|
144
|
+
.dark .btn-disabled {
|
|
145
|
+
background-color:black;
|
|
146
|
+
}
|
|
147
|
+
.btn-group {
|
|
148
|
+
display: flex;
|
|
149
|
+
width: fit-content;
|
|
150
|
+
gap: 8px;
|
|
151
|
+
padding: 4px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.btn {
|
|
155
|
+
&:has(.alignment-left) {
|
|
156
|
+
flex-direction: row;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&:has(.alignment-right) {
|
|
160
|
+
flex-direction: row-reverse;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
&:has(.alignment-top) {
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&:has(.alignment-bottom) {
|
|
168
|
+
flex-direction: column-reverse;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
svg.alignment-left {
|
|
172
|
+
margin-right: 8px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
svg.alignment-right {
|
|
176
|
+
margin-left: 8px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
svg.alignment-top {
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
svg.alignment-bottom {
|
|
183
|
+
margin-top: 8px;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
.btn-form {
|
|
189
|
+
padding: 4px 8px;
|
|
190
|
+
height: auto;
|
|
191
|
+
|
|
192
|
+
&.btn-last {
|
|
193
|
+
border-radius: 0;
|
|
194
|
+
border-top-right-radius: 8px;
|
|
195
|
+
border-bottom-right-radius: 8px;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import React, {useState, useEffect, useCallback, useRef} from 'react';
|
|
2
|
+
import { FaPlus, FaTrash, FaEquals } from 'react-icons/fa';
|
|
3
|
+
import {object_equals} from "data-primals-engine/core"; // Assuming this is a deep equality check
|
|
4
|
+
import CalculationStepRow from "./CalculationStepRow.jsx";
|
|
5
|
+
import {getFieldDefinitionFromPath, isAtomicDateOperator} from "./core/data.js";
|
|
6
|
+
import {MONGO_OPERATORS, OPERAND_TYPES} from "../src/constants.js";
|
|
7
|
+
|
|
8
|
+
// --- Helper Functions & Constants ---
|
|
9
|
+
const generateId = () => `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// --- Main Calculation Builder Component ---
|
|
13
|
+
const CalculationBuilder = ({
|
|
14
|
+
currentModelName, // Name of the model these calculations are for
|
|
15
|
+
availableFields: rootModelFields = [], // Fields of the currentModelName
|
|
16
|
+
models = [], // All model definitions
|
|
17
|
+
initialSteps = [],
|
|
18
|
+
onCalculationChange,
|
|
19
|
+
}) => {
|
|
20
|
+
|
|
21
|
+
const generateFlattenedFieldsRecursive = useCallback((modelName, allModels, maxDepth = 3, currentDepth = 0, pathPrefix = '', labelPrefix = '', visitedModelsInPath = new Set()) => {
|
|
22
|
+
if (currentDepth > maxDepth) return [];
|
|
23
|
+
|
|
24
|
+
const model = allModels.find(m => m.name === modelName);
|
|
25
|
+
if (!model || !model.fields) return [];
|
|
26
|
+
|
|
27
|
+
// Prevent infinite recursion for circular dependencies within the current path
|
|
28
|
+
if (visitedModelsInPath.has(modelName)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const newVisitedInPath = new Set(visitedModelsInPath).add(modelName);
|
|
32
|
+
|
|
33
|
+
let options = [];
|
|
34
|
+
const fieldsToProcess = currentDepth === 0 ? rootModelFields : model.fields;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
fieldsToProcess.forEach(field => {
|
|
38
|
+
if (!field.name) return;
|
|
39
|
+
const value = `${pathPrefix}${field.name}`;
|
|
40
|
+
const displayLabel = `${labelPrefix}${field.label || field.name}`;
|
|
41
|
+
const modelLabel = currentDepth === 0 ? (model.label || model.name) : labelPrefix.substring(0, labelPrefix.length - 3); // Get the parent model label
|
|
42
|
+
|
|
43
|
+
options.push({
|
|
44
|
+
value,
|
|
45
|
+
label: displayLabel,
|
|
46
|
+
type: field.type, // Original type of this specific field
|
|
47
|
+
modelName: model.name,
|
|
48
|
+
modelLabel: modelLabel,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (field.type === 'relation' && field.relation) {
|
|
52
|
+
const newPathPrefix = `${value}.`;
|
|
53
|
+
const newLabelPrefix = `${displayLabel} > `;
|
|
54
|
+
options = options.concat(
|
|
55
|
+
generateFlattenedFieldsRecursive(field.relation, allModels, maxDepth, currentDepth + 1, newPathPrefix, newLabelPrefix, newVisitedInPath)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return options;
|
|
60
|
+
}, [rootModelFields]);
|
|
61
|
+
|
|
62
|
+
const [allFlattenedFields, setAllFlattenedFields] = useState([]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (currentModelName && models.length > 0) {
|
|
66
|
+
setAllFlattenedFields(generateFlattenedFieldsRecursive(currentModelName, models));
|
|
67
|
+
}
|
|
68
|
+
}, [currentModelName, models, generateFlattenedFieldsRecursive]);
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const createDefaultStepArray = useCallback(() => {
|
|
72
|
+
const numericFields = allFlattenedFields.filter(f => {
|
|
73
|
+
const fieldDef = getFieldDefinitionFromPath(f.value, currentModelName, models);
|
|
74
|
+
return fieldDef && fieldDef.type === 'number';
|
|
75
|
+
});
|
|
76
|
+
const defaultOperand1Value = numericFields.length > 0 ? numericFields[0].value : '';
|
|
77
|
+
|
|
78
|
+
return [{
|
|
79
|
+
id: generateId(),
|
|
80
|
+
outputAlias: 'result',
|
|
81
|
+
operand1: { type: OPERAND_TYPES.FIELD, value: defaultOperand1Value },
|
|
82
|
+
operator: MONGO_OPERATORS.ADD.mongo,
|
|
83
|
+
operand2: { type: OPERAND_TYPES.CONSTANT, value: 0 },
|
|
84
|
+
}];
|
|
85
|
+
}, [allFlattenedFields, currentModelName, models]);
|
|
86
|
+
|
|
87
|
+
const [steps, setSteps] = useState(
|
|
88
|
+
initialSteps && initialSteps.length > 0 ? initialSteps : createDefaultStepArray()
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
//Re-initialize steps if initialSteps change or if allFlattenedFields are populated for the first time
|
|
93
|
+
if (initialSteps && initialSteps.length > 0) {
|
|
94
|
+
setSteps([...initialSteps]);
|
|
95
|
+
} else if (allFlattenedFields.length > 0) { // Ensure fields are ready before creating default
|
|
96
|
+
setSteps(createDefaultStepArray());
|
|
97
|
+
}
|
|
98
|
+
}, [initialSteps, createDefaultStepArray, allFlattenedFields.length]);
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
const getPreviousStepAliases = useCallback((currentIndex) => {
|
|
102
|
+
return steps
|
|
103
|
+
.slice(0, currentIndex)
|
|
104
|
+
.map((s) => s.outputAlias)
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
}, [steps]);
|
|
107
|
+
const handleAddStep = () => {
|
|
108
|
+
const newStep = {
|
|
109
|
+
id: generateId(),
|
|
110
|
+
outputAlias: `calc_step_${steps.length + 1}`,
|
|
111
|
+
operand1: { type: OPERAND_TYPES.FIELD, value: '' }, // Changer ici
|
|
112
|
+
operator: MONGO_OPERATORS.ADD.mongo,
|
|
113
|
+
operand2: { type: OPERAND_TYPES.CONSTANT, value: 0 },
|
|
114
|
+
};
|
|
115
|
+
setSteps([...steps, newStep]);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleRemoveStep = (id) => {
|
|
119
|
+
if (steps.length === 1 && steps[0].id === id) {
|
|
120
|
+
setSteps(createDefaultStepArray());
|
|
121
|
+
} else {
|
|
122
|
+
setSteps(steps.filter((step) => step.id !== id));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleUpdateStep = (id, updatedStepData) => {
|
|
127
|
+
setSteps(
|
|
128
|
+
steps.map((step) => (step.id === id ? { ...step, ...updatedStepData } : step))
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const lastEmittedCalculation = useRef(null);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!onCalculationChange || !currentModelName || models.length === 0) return;
|
|
136
|
+
|
|
137
|
+
const abstractPipeline = { lookups: [], addFields: {} };
|
|
138
|
+
const definedLookupAsFields = new Set(); // Tracks 'as' names of lookups
|
|
139
|
+
let isValid = true;
|
|
140
|
+
let finalOutputFieldName = null;
|
|
141
|
+
|
|
142
|
+
if (steps.length === 0 || (steps.length === 1 && !steps[0].outputAlias && !steps[0].operand1?.value && steps[0].operand2?.value === 0)) {
|
|
143
|
+
const emptyCalc = { final: null, pipeline: null, steps: JSON.parse(JSON.stringify(steps)) };
|
|
144
|
+
if (!object_equals(emptyCalc, lastEmittedCalculation.current)) {
|
|
145
|
+
lastEmittedCalculation.current = emptyCalc;
|
|
146
|
+
onCalculationChange(emptyCalc);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
for (const [idx, step] of steps.entries()) {
|
|
153
|
+
if (!step.outputAlias || !step.operator) {
|
|
154
|
+
isValid = false; break;
|
|
155
|
+
}
|
|
156
|
+
if (step.outputAlias.startsWith("__calc_lookup_")) { // Prevent conflicts
|
|
157
|
+
console.warn(`Step alias "${step.outputAlias}" is reserved. Please choose another name.`);
|
|
158
|
+
isValid = false; break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
const parseOperandToMongoPath = (operand, currentStepOperator, idx) => {
|
|
163
|
+
if (operand.type === OPERAND_TYPES.CONSTANT) {
|
|
164
|
+
if (currentStepOperator === MONGO_OPERATORS.CONCAT.mongo) {
|
|
165
|
+
if (operand.value === undefined || operand.value === null) { isValid = false; return null; }
|
|
166
|
+
return String(operand.value);
|
|
167
|
+
} else if (isAtomicDateOperator(currentStepOperator) && typeof operand.value === 'string') {
|
|
168
|
+
// Conversion de la chaîne constante en date pour les opérateurs de date
|
|
169
|
+
return { $toDate: operand.value };
|
|
170
|
+
} else {
|
|
171
|
+
if (operand.value === '' || operand.value === null || operand.value === undefined) { isValid = false; return null; }
|
|
172
|
+
const num = parseFloat(operand.value);
|
|
173
|
+
if (isNaN(num)) { isValid = false; return null; }
|
|
174
|
+
return num;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (operand.type === OPERAND_TYPES.PREVIOUS_STEP) {
|
|
179
|
+
if (!operand.value || !getPreviousStepAliases(idx).includes(operand.value)) {
|
|
180
|
+
isValid = false; return null;
|
|
181
|
+
}
|
|
182
|
+
const prevStepPath = `$${operand.value}`;
|
|
183
|
+
if (currentStepOperator === MONGO_OPERATORS.CONCAT.mongo) {
|
|
184
|
+
return { $toString: prevStepPath }; // Conversion en string
|
|
185
|
+
} else if (isAtomicDateOperator(currentStepOperator)) {
|
|
186
|
+
// Conversion en date si on attend une date
|
|
187
|
+
return { $toDate: prevStepPath }; // À adapter en fonction du type réel de prevStepPath
|
|
188
|
+
}
|
|
189
|
+
return prevStepPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (operand.type === OPERAND_TYPES.FIELD) {
|
|
193
|
+
if (!operand.value) { isValid = false; return null; }
|
|
194
|
+
|
|
195
|
+
const pathParts = operand.value.split('.');
|
|
196
|
+
let currentModelForPath = models.find(m => m.name === currentModelName);
|
|
197
|
+
let mongoPath = '$'; // Starts from the root document fields
|
|
198
|
+
let lastFieldDef = null;
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
201
|
+
const partName = pathParts[i];
|
|
202
|
+
if (!currentModelForPath || !currentModelForPath.fields) { isValid = false; return null; }
|
|
203
|
+
const fieldDef = currentModelForPath.fields.find(f => f.name === partName);
|
|
204
|
+
if (!fieldDef) { isValid = false; return null; }
|
|
205
|
+
lastFieldDef = fieldDef;
|
|
206
|
+
|
|
207
|
+
if (i < pathParts.length - 1) {
|
|
208
|
+
if (fieldDef.type !== 'relation' || !fieldDef.relation) { isValid = false; return null; }
|
|
209
|
+
|
|
210
|
+
const lookupAs = `__calc_lookup_${pathParts.slice(0, i + 1).join('_')}`;
|
|
211
|
+
mongoPath = `$${lookupAs}`;
|
|
212
|
+
|
|
213
|
+
if (!definedLookupAsFields.has(lookupAs)) {
|
|
214
|
+
abstractPipeline.lookups.push({
|
|
215
|
+
fromModel: currentModelForPath.name,
|
|
216
|
+
localField: (i === 0) ? fieldDef.name : `${pathParts.slice(0, i).map(p => `${p}`).join('.')}.${fieldDef.name}`,
|
|
217
|
+
foreignModel: fieldDef.relation,
|
|
218
|
+
as: lookupAs,
|
|
219
|
+
isMultiple: fieldDef.multiple || false
|
|
220
|
+
});
|
|
221
|
+
definedLookupAsFields.add(lookupAs);
|
|
222
|
+
}
|
|
223
|
+
currentModelForPath = models.find(m => m.name === fieldDef.relation);
|
|
224
|
+
} else {
|
|
225
|
+
mongoPath += (mongoPath === '$' ? '' : '.') + partName;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if( 'date' === lastFieldDef.type) {
|
|
230
|
+
return {$toDate: mongoPath };
|
|
231
|
+
}else if('datetime' === lastFieldDef.type){
|
|
232
|
+
return {$toDate: mongoPath };
|
|
233
|
+
}
|
|
234
|
+
if (currentStepOperator !== MONGO_OPERATORS.CONCAT.mongo && lastFieldDef && lastFieldDef.type === 'string') {
|
|
235
|
+
return { $toDate: mongoPath };
|
|
236
|
+
}
|
|
237
|
+
return mongoPath;
|
|
238
|
+
}
|
|
239
|
+
isValid = false; return null;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const op1Value = parseOperandToMongoPath(step.operand1, step.operator);
|
|
243
|
+
const op2Value = parseOperandToMongoPath(step.operand2, step.operator);
|
|
244
|
+
|
|
245
|
+
if (!isValid) break;
|
|
246
|
+
|
|
247
|
+
if (isAtomicDateOperator(step.operator)) {
|
|
248
|
+
// Pour les opérateurs atomiques de date, passer op1Value directement
|
|
249
|
+
abstractPipeline.addFields[step.outputAlias] = { [step.operator]: op1Value };
|
|
250
|
+
} else {
|
|
251
|
+
// Pour les autres opérateurs, conserver la logique actuelle
|
|
252
|
+
abstractPipeline.addFields[step.outputAlias] = { [step.operator]: [op1Value, op2Value] };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (isValid && steps.length > 0) {
|
|
257
|
+
finalOutputFieldName = steps[steps.length - 1].outputAlias;
|
|
258
|
+
if (!abstractPipeline.addFields[finalOutputFieldName] && steps.some(s => s.outputAlias)) {
|
|
259
|
+
// If the last step's output isn't in addFields, but there were valid steps, it's an issue
|
|
260
|
+
// unless all steps were invalid.
|
|
261
|
+
if (Object.keys(abstractPipeline.addFields).length > 0) isValid = false;
|
|
262
|
+
}
|
|
263
|
+
} else if (steps.length === 0) {
|
|
264
|
+
isValid = true; // No steps is valid, calc will be null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
let currentCalculationData;
|
|
269
|
+
if (isValid && finalOutputFieldName && (abstractPipeline.lookups.length > 0 || Object.keys(abstractPipeline.addFields).length > 0)) {
|
|
270
|
+
currentCalculationData = {
|
|
271
|
+
final: finalOutputFieldName,
|
|
272
|
+
pipeline: abstractPipeline, // Send the abstract pipeline
|
|
273
|
+
steps: JSON.parse(JSON.stringify(steps))
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
currentCalculationData = {
|
|
277
|
+
final: null,
|
|
278
|
+
pipeline: null,
|
|
279
|
+
steps: JSON.parse(JSON.stringify(steps))
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!object_equals(currentCalculationData, lastEmittedCalculation.current)) {
|
|
284
|
+
lastEmittedCalculation.current = currentCalculationData;
|
|
285
|
+
onCalculationChange(currentCalculationData);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
}, [steps, onCalculationChange, currentModelName, models, getPreviousStepAliases]);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div style={{ fontFamily: 'Arial, sans-serif', padding: '15px', border: '1px solid #ddd', borderRadius: '8px', backgroundColor: '#f9f9f9' }}>
|
|
293
|
+
<h3 style={{ marginTop: 0, marginBottom: '15px', color: '#333' }}>Calculation Builder</h3>
|
|
294
|
+
{steps.map((step, index) => (
|
|
295
|
+
<CalculationStepRow
|
|
296
|
+
key={step.id}
|
|
297
|
+
step={step}
|
|
298
|
+
onUpdateStep={handleUpdateStep}
|
|
299
|
+
onRemoveStep={handleRemoveStep}
|
|
300
|
+
allFlattenedFields={allFlattenedFields}
|
|
301
|
+
previousStepAliases={getPreviousStepAliases(index)}
|
|
302
|
+
isLastStep={index === steps.length - 1}
|
|
303
|
+
currentModelName={currentModelName}
|
|
304
|
+
models={models}
|
|
305
|
+
/>
|
|
306
|
+
))}
|
|
307
|
+
<button
|
|
308
|
+
type={"button"}
|
|
309
|
+
onClick={handleAddStep}
|
|
310
|
+
style={{
|
|
311
|
+
padding: '10px 15px',
|
|
312
|
+
backgroundColor: '#28a745',
|
|
313
|
+
color: 'white',
|
|
314
|
+
border: 'none',
|
|
315
|
+
borderRadius: '4px',
|
|
316
|
+
cursor: 'pointer',
|
|
317
|
+
display: 'flex',
|
|
318
|
+
alignItems: 'center',
|
|
319
|
+
gap: '5px'
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
<FaPlus /> Add Calculation Step
|
|
323
|
+
</button>
|
|
324
|
+
{steps.length > 0 && steps[steps.length - 1].outputAlias && (
|
|
325
|
+
<p style={{marginTop: '15px', fontSize: '0.9em', color: '#555'}}>
|
|
326
|
+
Final result will be available in the field: <strong>{steps[steps.length - 1].outputAlias}</strong>
|
|
327
|
+
</p>
|
|
328
|
+
)}
|
|
329
|
+
{steps.length > 0 && !steps[steps.length - 1].outputAlias && steps.some(s => s.operand1?.value || s.operand2?.value) && (
|
|
330
|
+
<p style={{marginTop: '15px', fontSize: '0.9em', color: 'red'}}>
|
|
331
|
+
<strong>Warning:</strong> Define an output alias for the last calculation step.
|
|
332
|
+
</p>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export default CalculationBuilder;
|