data-primals-engine 1.0.11 → 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 +212 -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,290 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/FlexNode.jsx
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { FaPlus, FaTrash, FaEdit, FaRegSquare, FaRegCheckSquare, FaBox, FaExpandArrowsAlt, FaObjectGroup } from 'react-icons/fa';
|
|
5
|
+
// Remove Droppable, Draggable from '@hello-pangea/dnd' if no longer used
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
// Supprimer l'import de useDragAndDrop s'il n'est plus utilisé localement
|
|
8
|
+
// import useDragAndDrop from './hooks/useDragAndDrop.js';
|
|
9
|
+
|
|
10
|
+
export const FieldSelectorModal = ({ isOpen, onClose, modelFields, allModels, onSelectField }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
if (!isOpen) return null;
|
|
13
|
+
const getFieldDisplayName = (field) => {
|
|
14
|
+
let displayName = field.displayName || field.name;
|
|
15
|
+
if (field.modelName && field.modelName !== field.originalModelName) {
|
|
16
|
+
displayName = `${t(`model_${field.modelName}`, field.modelName)}.${displayName}`;
|
|
17
|
+
}
|
|
18
|
+
return displayName;
|
|
19
|
+
};
|
|
20
|
+
const renderFieldItem = (field, index) => (
|
|
21
|
+
<li key={`${field.path}-${index}`} onClick={() => onSelectField(field)} title={field.path}>
|
|
22
|
+
{getFieldDisplayName(field)}
|
|
23
|
+
<span className="field-path">({field.type}{field.relation ? ` -> ${field.relation}` : ''})</span>
|
|
24
|
+
</li>
|
|
25
|
+
);
|
|
26
|
+
const groupedFields = modelFields.reduce((acc, field) => {
|
|
27
|
+
const modelName = field.modelName || t('flexBuilder.currentModel', 'Modle actuel');
|
|
28
|
+
if (!acc[modelName]) { acc[modelName] = []; }
|
|
29
|
+
acc[modelName].push(field);
|
|
30
|
+
return acc;
|
|
31
|
+
}, {});
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex-builder-modal-overlay" onClick={onClose}>
|
|
34
|
+
<div className="flex-builder-modal-content" onClick={e => e.stopPropagation()}>
|
|
35
|
+
<h4>{t('flexBuilder.selectDataFieldTitle', 'Sélectionner un champ de données')}</h4>
|
|
36
|
+
{Object.keys(groupedFields).length > 0 ? (
|
|
37
|
+
Object.entries(groupedFields).map(([modelName, fields]) => (
|
|
38
|
+
<div key={modelName}>
|
|
39
|
+
<h5>{modelName === t(`model_${modelName}`, modelName)}</h5>
|
|
40
|
+
<ul className="field-selector-list">{fields.map(renderFieldItem)}</ul>
|
|
41
|
+
</div>
|
|
42
|
+
))
|
|
43
|
+
) : (<p>{t('flexBuilder.noFieldsAvailable', 'Aucun champ disponible pour ce modèle.')}</p>)}
|
|
44
|
+
<button onClick={onClose} className="btn-cancel">{t('cancel', 'Annuler')}</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
FieldSelectorModal.propTypes = {
|
|
50
|
+
isOpen: PropTypes.bool.isRequired,
|
|
51
|
+
onClose: PropTypes.func.isRequired,
|
|
52
|
+
modelFields: PropTypes.array.isRequired,
|
|
53
|
+
allModels: PropTypes.array.isRequired,
|
|
54
|
+
onSelectField: PropTypes.func.isRequired,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const FlexNodeRenderer = React.forwardRef((props, ref) => {
|
|
58
|
+
const {
|
|
59
|
+
node, path, onSelectNode, selectedNodeId, data, dataIndexRef, modelFields, t, onMoveNode,
|
|
60
|
+
dnd, // Recevoir l'objet dnd en tant que prop
|
|
61
|
+
} = props;
|
|
62
|
+
|
|
63
|
+
// Déstructurer les valeurs et gestionnaires du prop dnd
|
|
64
|
+
const {
|
|
65
|
+
isDragging, draggedItemRef, dragOverContainerId,
|
|
66
|
+
handleNodeDragStart, handleContainerDragEnter, handleContainerDragLeave,
|
|
67
|
+
handleItemDragEnd,
|
|
68
|
+
finalizeDropAndGetData
|
|
69
|
+
} = dnd;
|
|
70
|
+
|
|
71
|
+
const isSelected = selectedNodeId === node.id;
|
|
72
|
+
|
|
73
|
+
const handleNodeClick = (e) => {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
onSelectNode(node.id);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getNestedValue = (obj, fieldPath) => {
|
|
79
|
+
if (!obj || !fieldPath) return 'N/A';
|
|
80
|
+
return fieldPath.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : 'N/A', obj);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isCurrentlyDraggingThisNode = isDragging && draggedItemRef.current === node.id;
|
|
84
|
+
// Correction: dragOverContainerId vient directement du hook/prop, pas besoin de le recalculer ici
|
|
85
|
+
// const currentDragOverContainerId = node.type === 'container' ? node.id : (node.content?.nestedContainer?.id);
|
|
86
|
+
// const isContainerBeingDraggedOver = dragOverContainerId === currentDragOverContainerId;
|
|
87
|
+
// Utiliser directement dragOverContainerId pour vérifier si CE conteneur est survolé
|
|
88
|
+
const isThisContainerBeingDraggedOver = (node.type === 'container' && dragOverContainerId === node.id) ||
|
|
89
|
+
(node.type === 'item' && node.content?.type === 'nestedContainer' && dragOverContainerId === node.content.nestedContainer.id);
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
const onItemDragStartInternal = (e, nodeId) => {
|
|
93
|
+
console.log("FlexNode: Item drag start", nodeId);
|
|
94
|
+
e.dataTransfer.setData("text/plain", nodeId);
|
|
95
|
+
e.dataTransfer.effectAllowed = "move";
|
|
96
|
+
handleNodeDragStart(nodeId); // Utiliser la fonction du prop dnd
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onItemDragEndInternal = () => {
|
|
100
|
+
console.log("FlexNode: Item drag end. Current draggedItemRef before hook.handleItemDragEnd:", draggedItemRef.current);
|
|
101
|
+
handleItemDragEnd(); // Utiliser la fonction du prop dnd
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onContainerDragEnterInternal = (e, containerId) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
console.log("FlexNode: Container drag enter", containerId);
|
|
107
|
+
handleContainerDragEnter(containerId); // Utiliser la fonction du prop dnd
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const onContainerDragOverInternal = (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.dataTransfer.dropEffect = "move";
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const onContainerDragLeaveInternal = () => {
|
|
116
|
+
console.log("FlexNode: Container drag leave");
|
|
117
|
+
handleContainerDragLeave(); // Utiliser la fonction du prop dnd
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const onContainerDropInternal = (e) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
|
|
124
|
+
const droppedItemId = finalizeDropAndGetData(); // Utiliser la fonction du prop dnd
|
|
125
|
+
const targetContainerNode = node.type === 'container' ? node : node.content?.nestedContainer;
|
|
126
|
+
const targetContainerId = targetContainerNode?.id;
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
console.log("FlexNode: Drop on container. Dropped Item ID:", droppedItemId, "Target Container ID:", targetContainerId);
|
|
130
|
+
|
|
131
|
+
if (typeof onMoveNode === 'function' && droppedItemId && targetContainerId) {
|
|
132
|
+
// TODO: Déterminer le bon newIndex
|
|
133
|
+
const newIndex = 0;
|
|
134
|
+
onMoveNode(droppedItemId, targetContainerId, newIndex);
|
|
135
|
+
} else {
|
|
136
|
+
console.warn("FlexNode: onMoveNode not called. Check props or IDs.", { onMoveNodeExists: typeof onMoveNode === 'function', droppedItemId, targetContainerId });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// useEffect(() => {
|
|
141
|
+
// console.log(`FlexNode (${node.id} - ${node.type}): onMoveNode type:`, typeof onMoveNode);
|
|
142
|
+
// }, [onMoveNode, node.id, node.type]);
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if (node.type === 'container' || (node.type === 'item' && node.content?.type === 'nestedContainer')) {
|
|
146
|
+
const containerNode = node.type === 'container' ? node : node.content.nestedContainer;
|
|
147
|
+
const children = containerNode.children || [];
|
|
148
|
+
const containerStyle = node.type === 'item' ?
|
|
149
|
+
{ ...containerNode.containerStyle, padding: '0', border: 'none', minHeight: '40px' }
|
|
150
|
+
: containerNode.containerStyle;
|
|
151
|
+
|
|
152
|
+
const isThisNodeSelected = isSelected && (node.id === selectedNodeId || (node.type === 'item' && containerNode.id === selectedNodeId));
|
|
153
|
+
// Correction pour isThisContainerBeingDraggedOver
|
|
154
|
+
const isThisSpecificContainerDraggedOver = dragOverContainerId === containerNode.id;
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
ref={ref}
|
|
160
|
+
style={node.type === 'item' ? node.itemStyle : {}}
|
|
161
|
+
className={`flex-node-wrapper ${node.type === 'item' ? 'is-nested-container-wrapper' : ''} ${isCurrentlyDraggingThisNode && node.type === 'item' ? 'is-dragging-dnd' : ''}`}
|
|
162
|
+
draggable={node.type === 'item'}
|
|
163
|
+
onDragStart={node.type === 'item' ? (e) => onItemDragStartInternal(e, node.id) : undefined}
|
|
164
|
+
onDragEnd={node.type === 'item' ? onItemDragEndInternal : undefined}
|
|
165
|
+
onClick={node.type === 'item' ? handleNodeClick : undefined} // Permet de s'item qui est un conteneur imbriqué
|
|
166
|
+
>
|
|
167
|
+
<div
|
|
168
|
+
style={containerStyle}
|
|
169
|
+
className={`flex-node preview-container ${isThisNodeSelected ? 'is-selected' : ''} ${isThisSpecificContainerDraggedOver ? 'drag-over-active' : ''}`}
|
|
170
|
+
onClick={node.type === 'container' ? handleNodeClick : (e) => e.stopPropagation()} // Le clic sur le conteneur racine le sélectionne
|
|
171
|
+
onDragEnter={(e) => onContainerDragEnterInternal(e, containerNode.id)}
|
|
172
|
+
onDragLeave={onContainerDragLeaveInternal}
|
|
173
|
+
onDragOver={onContainerDragOverInternal}
|
|
174
|
+
onDrop={onContainerDropInternal}
|
|
175
|
+
>
|
|
176
|
+
{children.length === 0 && !isThisSpecificContainerDraggedOver && (
|
|
177
|
+
<span className="empty-container-placeholder">{t('flexBuilder.emptyContainerHint', 'Glissez des i ou ajoutez-en via les contrôles.')}</span>
|
|
178
|
+
)}
|
|
179
|
+
{children.map((childNode, index) => (
|
|
180
|
+
<FlexNodeRenderer
|
|
181
|
+
key={childNode.id}
|
|
182
|
+
node={childNode}
|
|
183
|
+
path={[...path, index]}
|
|
184
|
+
onSelectNode={onSelectNode}
|
|
185
|
+
selectedNodeId={selectedNodeId}
|
|
186
|
+
data={data}
|
|
187
|
+
dataIndexRef={dataIndexRef}
|
|
188
|
+
modelFields={modelFields}
|
|
189
|
+
t={t}
|
|
190
|
+
dnd={dnd} // Passer le prop dnd récursivement
|
|
191
|
+
onMoveNode={onMoveNode}
|
|
192
|
+
/>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
} else if (node.type === 'item') {
|
|
198
|
+
const itemStyleWithDefaults = {
|
|
199
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'grab', ...node.itemStyle,
|
|
200
|
+
};
|
|
201
|
+
if (isCurrentlyDraggingThisNode) {
|
|
202
|
+
itemStyleWithDefaults.opacity = 0.7;
|
|
203
|
+
itemStyleWithDefaults.boxShadow = '0 5px 15px rgba(0,0,0,0.3)';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let contentElement;
|
|
207
|
+
if (node.content.type === 'placeholder') {
|
|
208
|
+
contentElement = <span className="preview-item-content">{t('flexBuilder.emptyItemHint', 'Case vide')}</span>;
|
|
209
|
+
} else if (node.content.type === 'dataField' && node.content.mapping) {
|
|
210
|
+
// Logs de débogage pour dataField (vous pouvez les garder ou les commenter)
|
|
211
|
+
console.log(`[FlexNodeRenderer - ${node.id}] Rendu DataField:`, {
|
|
212
|
+
nodeContent: JSON.parse(JSON.stringify(node.content)),
|
|
213
|
+
nodeMapping: JSON.parse(JSON.stringify(node.content.mapping)),
|
|
214
|
+
fieldPathToUse: node.content.mapping?.fieldPath,
|
|
215
|
+
dataPropLength: data?.length,
|
|
216
|
+
dataIndexAtStartOfItem: dataIndexRef.current,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
let displayValue;
|
|
220
|
+
const fieldPathForDisplay = node.content.mapping?.displayName || node.content.mapping?.fieldPath || t('flexBuilder.noFieldSelected', 'Champ non lié');
|
|
221
|
+
|
|
222
|
+
if (data && data.length > 0) {
|
|
223
|
+
let currentDataRecord = {};
|
|
224
|
+
let recordIndex = -1;
|
|
225
|
+
recordIndex = dataIndexRef.current % data.length;
|
|
226
|
+
currentDataRecord = data[recordIndex];
|
|
227
|
+
|
|
228
|
+
console.log(`[FlexNodeRenderer - ${node.id}] currentDataRecord (index ${recordIndex}):`, JSON.parse(JSON.stringify(currentDataRecord)));
|
|
229
|
+
|
|
230
|
+
const value = node.content.mapping?.fieldPath
|
|
231
|
+
? getNestedValue(currentDataRecord, node.content.mapping.fieldPath)
|
|
232
|
+
: t('flexBuilder.noDataMapped', 'N/A');
|
|
233
|
+
|
|
234
|
+
console.log(`[FlexNodeRenderer - ${node.id}] Valeur récupérée pour ${node.content.mapping?.fieldPath}:`, value);
|
|
235
|
+
|
|
236
|
+
displayValue = `${fieldPathForDisplay}`;
|
|
237
|
+
|
|
238
|
+
if (data && data.length > 0) {
|
|
239
|
+
dataIndexRef.current = (dataIndexRef.current + 1);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// Si data est vide, afficher le nom du champ
|
|
243
|
+
console.warn(`[FlexNodeRenderer - ${node.id}] data prop est vide ou null. Affichage du nom du champ.`);
|
|
244
|
+
displayValue = `${fieldPathForDisplay}`; // Affiche le nom du champ entre crochets
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
contentElement = <span className="preview-item-content" title={node.content.mapping?.fieldPath || 'Champ non lié'}>{displayValue}</span>;
|
|
248
|
+
|
|
249
|
+
} else {
|
|
250
|
+
contentElement = <span className="preview-item-content">{node.content.type}</span>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
ref={ref}
|
|
256
|
+
style={itemStyleWithDefaults}
|
|
257
|
+
className={`flex-node preview-item ${isSelected ? 'is-selected' : ''} ${isCurrentlyDraggingThisNode ? 'is-dragging-dnd' : ''}`}
|
|
258
|
+
onClick={handleNodeClick}
|
|
259
|
+
draggable // Les items sont toujours déplaçables
|
|
260
|
+
onDragStart={(e) => onItemDragStartInternal(e, node.id)}
|
|
261
|
+
onDragEnd={onItemDragEndInternal}
|
|
262
|
+
>
|
|
263
|
+
{contentElement}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div ref={ref} onClick={handleNodeClick} style={{ padding: '10px', border: '1px dashed red' }}>
|
|
270
|
+
{t('flexBuilder.unknownNodeType', 'Type de nœud inconnu')}: {node.type}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
FlexNodeRenderer.displayName = 'FlexNodeRenderer';
|
|
276
|
+
FlexNodeRenderer.propTypes = {
|
|
277
|
+
node: PropTypes.object.isRequired,
|
|
278
|
+
path: PropTypes.array.isRequired,
|
|
279
|
+
onSelectNode: PropTypes.func.isRequired,
|
|
280
|
+
selectedNodeId: PropTypes.string,
|
|
281
|
+
data: PropTypes.array,
|
|
282
|
+
dataIndexRef: PropTypes.object,
|
|
283
|
+
modelFields: PropTypes.array,
|
|
284
|
+
t: PropTypes.func.isRequired,
|
|
285
|
+
onMoveNode: PropTypes.func,
|
|
286
|
+
dnd: PropTypes.object.isRequired, // dnd est maintenant requis
|
|
287
|
+
};
|
|
288
|
+
FlexNodeRenderer.defaultProps = {
|
|
289
|
+
data: [], modelFields: [], onMoveNode: null,
|
|
290
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Crée un nouveau nœud de type 'container' ou 'item'.
|
|
5
|
+
*/
|
|
6
|
+
export const createNewNode = (type) => {
|
|
7
|
+
const baseStyle = { border: '1px solid #ddd', padding: '5px' };
|
|
8
|
+
if (type === 'container') {
|
|
9
|
+
return {
|
|
10
|
+
id: uuidv4(), type: 'container',
|
|
11
|
+
containerStyle: { ...baseStyle, display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '5px', minHeight: '50px'},
|
|
12
|
+
children: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
id: uuidv4(), type: 'item',
|
|
17
|
+
itemStyle: { ...baseStyle, width: '', height: '', display: 'flex', alignItems: 'center', justifyContent: 'center' },
|
|
18
|
+
content: { type: 'placeholder' },
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Nettoie un nœud pour la sauvegarde.
|
|
24
|
+
*/
|
|
25
|
+
export const cleanNodeForOutput = (node) => {
|
|
26
|
+
const { id, type, children, content, containerStyle, itemStyle } = node;
|
|
27
|
+
const outputNode = { id, type };
|
|
28
|
+
|
|
29
|
+
if (type === 'container') {
|
|
30
|
+
outputNode.containerStyle = containerStyle || {};
|
|
31
|
+
outputNode.children = children ? children.map(cleanNodeForOutput) : [];
|
|
32
|
+
} else if (type === 'item') {
|
|
33
|
+
outputNode.itemStyle = itemStyle || {};
|
|
34
|
+
if (content) {
|
|
35
|
+
outputNode.content = { type: content.type };
|
|
36
|
+
if (content.type === 'dataField' && content.mapping) {
|
|
37
|
+
outputNode.content.mapping = content.mapping;
|
|
38
|
+
} else if (content.type === 'nestedContainer' && content.nestedContainer) {
|
|
39
|
+
outputNode.content.nestedContainer = cleanNodeForOutput(content.nestedContainer);
|
|
40
|
+
} else if (content.type === 'richtext' && content.html) {
|
|
41
|
+
outputNode.content.html = content.html;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
outputNode.content = { type: 'placeholder' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return outputNode;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Trouve un nœud par son ID de manière récursive.
|
|
52
|
+
*/
|
|
53
|
+
export const findNodeRecursive = (nodes, id) => {
|
|
54
|
+
for (const node of nodes) {
|
|
55
|
+
if (node.id === id) return node;
|
|
56
|
+
if (node.type === 'container' && node.children) {
|
|
57
|
+
const found = findNodeRecursive(node.children, id);
|
|
58
|
+
if (found) return found;
|
|
59
|
+
} else if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
|
|
60
|
+
if (node.content.nestedContainer.id === id) return node.content.nestedContainer;
|
|
61
|
+
const found = findNodeRecursive(node.content.nestedContainer.children, id);
|
|
62
|
+
if (found) return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Met de mise à jour.
|
|
70
|
+
*/
|
|
71
|
+
export const updateNodeRecursive = (nodes, nodeId, updateFn) => {
|
|
72
|
+
return nodes.map(node => {
|
|
73
|
+
if (node.id === nodeId) return updateFn(node);
|
|
74
|
+
if (node.type === 'container' && node.children) {
|
|
75
|
+
return { ...node, children: updateNodeRecursive(node.children, nodeId, updateFn) };
|
|
76
|
+
}
|
|
77
|
+
if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
|
|
78
|
+
const nested = node.content.nestedContainer;
|
|
79
|
+
if (nested.id === nodeId) {
|
|
80
|
+
return { ...node, content: { ...node.content, nestedContainer: updateFn(nested) }};
|
|
81
|
+
}
|
|
82
|
+
const updatedChildren = updateNodeRecursive(nested.children, nodeId, updateFn);
|
|
83
|
+
if (updatedChildren !== nested.children) {
|
|
84
|
+
return { ...node, content: { ...node.content, nestedContainer: { ...nested, children: updatedChildren }}};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return node;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Supprime un nœud de manière récursive.
|
|
93
|
+
*/
|
|
94
|
+
export const deleteNodeRecursive = (nodes, nodeId) => {
|
|
95
|
+
const filteredNodes = nodes.filter(node => node.id !== nodeId);
|
|
96
|
+
if (filteredNodes.length < nodes.length) return filteredNodes;
|
|
97
|
+
|
|
98
|
+
return nodes.map(node => {
|
|
99
|
+
if (node.type === 'container' && node.children) {
|
|
100
|
+
const updatedChildren = deleteNodeRecursive(node.children, nodeId);
|
|
101
|
+
if (updatedChildren !== node.children) return { ...node, children: updatedChildren };
|
|
102
|
+
} else if (node.type === 'item' && node.content.type === 'nestedContainer' && node.content.nestedContainer) {
|
|
103
|
+
const nested = node.content.nestedContainer;
|
|
104
|
+
if (nested.id === nodeId) return { ...node, content: { type: 'placeholder' } }; // Reset item
|
|
105
|
+
const updatedNestedChildren = deleteNodeRecursive(nested.children, nodeId);
|
|
106
|
+
if (updatedNestedChildren !== nested.children) {
|
|
107
|
+
return { ...node, content: { ...node.content, nestedContainer: { ...nested, children: updatedNestedChildren }}};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return node;
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Efface les mappings de données de manière récursive.
|
|
116
|
+
*/
|
|
117
|
+
export const clearMappingsRecursive = (nodes) => nodes.map(node => {
|
|
118
|
+
if (node.type === 'item') {
|
|
119
|
+
if (node.content.type === 'dataField') {
|
|
120
|
+
return { ...node, content: { type: 'placeholder' } };
|
|
121
|
+
}
|
|
122
|
+
if (node.content.type === 'nestedContainer' && node.content.nestedContainer) {
|
|
123
|
+
return { ...node, content: { ...node.content, nestedContainer: { ...node.content.nestedContainer, children: clearMappingsRecursive(node.content.nestedContainer.children) } } };
|
|
124
|
+
}
|
|
125
|
+
} else if (node.type === 'container') {
|
|
126
|
+
return { ...node, children: clearMappingsRecursive(node.children) };
|
|
127
|
+
}
|
|
128
|
+
return node;
|
|
129
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// client/src/HiddenableCell.jsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { FaEye, FaEyeSlash } from 'react-icons/fa';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import './HiddenableCell.scss';
|
|
6
|
+
|
|
7
|
+
export const HiddenableCell = ({ value }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const [isRevealed, setIsRevealed] = useState(false);
|
|
10
|
+
|
|
11
|
+
const toggleVisibility = () => {
|
|
12
|
+
setIsRevealed(!isRevealed);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="hiddenable-cell">
|
|
17
|
+
{isRevealed ? (
|
|
18
|
+
// --- État visible ---
|
|
19
|
+
<>
|
|
20
|
+
<span className="revealed-content">{value}</span>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={toggleVisibility}
|
|
24
|
+
className="hide-toggle-btn"
|
|
25
|
+
title={t('tooltip.hideContent', 'Cacher le contenu')}
|
|
26
|
+
>
|
|
27
|
+
<FaEyeSlash />
|
|
28
|
+
</button>
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
// --- État masqué (par défaut) ---
|
|
32
|
+
<>
|
|
33
|
+
<span className="hidden-placeholder">••••••••••</span>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={toggleVisibility}
|
|
37
|
+
className="hide-toggle-btn"
|
|
38
|
+
title={t('tooltip.showHiddenContent', 'Afficher le contenu')}
|
|
39
|
+
>
|
|
40
|
+
<FaEye />
|
|
41
|
+
</button>
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// client/src/HiddenableCell.scss
|
|
2
|
+
|
|
3
|
+
.hiddenable-cell {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: space-between; // Aligne le bouton n gap: 8px; // Espace entre le contenu et le bouton
|
|
7
|
+
width: 100%;
|
|
8
|
+
|
|
9
|
+
.hidden-placeholder, .revealed-content {
|
|
10
|
+
flex-grow: 1; // Le contenu prend l'espace disponible
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
text-overflow: ellipsis; // Ajoute "..." si le contenu est trop long
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.hidden-placeholder {
|
|
17
|
+
color: #999;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.hide-toggle-btn {
|
|
21
|
+
background: none;
|
|
22
|
+
border: none;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
color: #6c757d;
|
|
25
|
+
padding: 2px 4px;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
transition: background-color 0.2s, color 0.2s;
|
|
30
|
+
|
|
31
|
+
&:hover {
|
|
32
|
+
background-color: #f0f0f0;
|
|
33
|
+
color: #333;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Dialog } from './Dialog.jsx'; // Assure-toi que Dialog est importé
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
8
|
+
|
|
9
|
+
const filteredKpis = (availableKpis || []).filter(kpi =>
|
|
10
|
+
kpi.name.value.toLowerCase().includes(searchTerm.toLowerCase())
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Dialog isModal={true} isClosable={true} onClose={onClose} className="add-kpi-dialog">
|
|
15
|
+
<h2>{t('dashboards.add_kpi_title', 'Ajouter un indicateur (kpi)')}</h2>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
placeholder={t('dashboards.search_kpi', 'Rechercher un KPI...')}
|
|
19
|
+
value={searchTerm}
|
|
20
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
21
|
+
style={{ width: '100%', marginBottom: '15px' }}
|
|
22
|
+
/>
|
|
23
|
+
<ul className="kpi-selection-list">
|
|
24
|
+
{filteredKpis.length > 0 ? (
|
|
25
|
+
filteredKpis.map(kpi => (
|
|
26
|
+
<li key={kpi._id} onClick={() => onAddKpi(kpi)}>
|
|
27
|
+
{kpi.name.value || (typeof(kpi.name) === 'string' ? kpi.name : '')}
|
|
28
|
+
{/* Tu peux ajouter l'icône ou l'unité ici pour aider oisir */}
|
|
29
|
+
</li>
|
|
30
|
+
))
|
|
31
|
+
) : (
|
|
32
|
+
<li>{t('dashboards.no_kpi_found', 'Aucun KPI disponible ou correspondant.')}</li>
|
|
33
|
+
)}
|
|
34
|
+
</ul>
|
|
35
|
+
</Dialog>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default KPIDialog;
|