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,75 @@
|
|
|
1
|
+
// src/components/FlexBuilderModal.jsx
|
|
2
|
+
import React, { useState, useEffect, useCallback } from 'react'; // Ajout de useCallback
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import FlexBuilder from './FlexBuilder';
|
|
6
|
+
import './FlexBuilderModal.scss';
|
|
7
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
8
|
+
|
|
9
|
+
const FlexBuilderModal = ({ isOpen, onClose, onSave, models, data, initialConfig = null }) => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const [currentFlexConfig, setCurrentFlexConfig] = useState(initialConfig);
|
|
12
|
+
const {me} = useAuthContext();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Met à jour currentFlexConfig si initialConfig (venant du parent DashboardView) change
|
|
16
|
+
// ou si la modale est (ré)ouverte.
|
|
17
|
+
// Cela est utile si l'utilisateur ferme la modale puis la rouvre pour éditer un autre item
|
|
18
|
+
// ou le même item avec des données potentiellement mises à jour de l'extérieur.
|
|
19
|
+
setCurrentFlexConfig(initialConfig);
|
|
20
|
+
}, [isOpen, initialConfig]); // S'assurer de réagir aux changements de initialConfig
|
|
21
|
+
|
|
22
|
+
// Stabiliser handleFlexConfigChange avec useCallback
|
|
23
|
+
const handleFlexConfigChange = useCallback((newConfig) => {
|
|
24
|
+
setCurrentFlexConfig(newConfig);
|
|
25
|
+
}, []); // Les dépendances sont vides car setCurrentFlexConfig est stable
|
|
26
|
+
|
|
27
|
+
const handleSave = () => {
|
|
28
|
+
if (currentFlexConfig) {
|
|
29
|
+
onSave(currentFlexConfig);
|
|
30
|
+
}
|
|
31
|
+
// onClose(); // La fermeture est gérée par le parent (DashboardView) après la sauvegarde.
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!isOpen) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex-builder-config-modal-overlay" onClick={onClose}>
|
|
40
|
+
<div className="flex-builder-config-modal-content" onClick={(e) => e.stopPropagation()}>
|
|
41
|
+
<div className="flex-builder-config-modal-header">
|
|
42
|
+
<h3>{t('dashboard.configureFlexView', 'Configurer la Vue Flex')}</h3>
|
|
43
|
+
<button onClick={onClose} className="btn-close-x">×</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="flex-builder-config-modal-body">
|
|
46
|
+
<FlexBuilder
|
|
47
|
+
initialConfig={currentFlexConfig}
|
|
48
|
+
models={(models || []).filter(f => f._user === me?.username)}
|
|
49
|
+
data={data}
|
|
50
|
+
onChange={handleFlexConfigChange} // Maintenant, cette prop est stable
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex-builder-config-modal-footer">
|
|
54
|
+
<button onClick={onClose} className="btn-secondary">
|
|
55
|
+
{t('btns.cancel', 'Annuler')}
|
|
56
|
+
</button>
|
|
57
|
+
<button onClick={handleSave} className="btn-primary">
|
|
58
|
+
{t('btns.save', 'Enregistrer la Vue')}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
FlexBuilderModal.propTypes = {
|
|
67
|
+
isOpen: PropTypes.bool.isRequired,
|
|
68
|
+
onClose: PropTypes.func.isRequired,
|
|
69
|
+
onSave: PropTypes.func.isRequired,
|
|
70
|
+
models: PropTypes.array.isRequired,
|
|
71
|
+
data: PropTypes.array,
|
|
72
|
+
initialConfig: PropTypes.object,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default FlexBuilderModal;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/components/FlexBuilderModal.scss
|
|
2
|
+
|
|
3
|
+
.flex-builder-config-modal-overlay {
|
|
4
|
+
position: fixed;
|
|
5
|
+
top: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
right: 0;
|
|
8
|
+
bottom: 0;
|
|
9
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
z-index: 1040; // Un peu moins que la modale de type de widget si elle reste ouverte
|
|
14
|
+
padding: 10px; // Pour petits écrans
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.flex-builder-config-modal-content {
|
|
18
|
+
background-color: #fff;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
width: 95vw; // Très large pour accommoder FlexBuilder
|
|
24
|
+
height: 90vh; // Très haut
|
|
25
|
+
max-width: 1400px; // Limite maximale
|
|
26
|
+
overflow: hidden; // Le scroll sera géré par les enfants
|
|
27
|
+
|
|
28
|
+
.flex-builder-config-modal-header {
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
align-items: center;
|
|
32
|
+
padding: 15px 20px;
|
|
33
|
+
border-bottom: 1px solid #e0e0e0;
|
|
34
|
+
background-color: #f7f7f7;
|
|
35
|
+
|
|
36
|
+
h3 {
|
|
37
|
+
margin: 0;
|
|
38
|
+
font-size: 1.4em;
|
|
39
|
+
color: #333;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.btn-close-x {
|
|
43
|
+
background: none;
|
|
44
|
+
border: none;
|
|
45
|
+
font-size: 1.8em;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
color: #777;
|
|
48
|
+
padding: 0 5px;
|
|
49
|
+
line-height: 1;
|
|
50
|
+
&:hover {
|
|
51
|
+
color: #333;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.flex-builder-config-modal-body {
|
|
57
|
+
flex-grow: 1;
|
|
58
|
+
overflow-y: auto; // Permettre le scroll si FlexBuilder est plus grand
|
|
59
|
+
padding: 0; // FlexBuilder a son propre padding
|
|
60
|
+
|
|
61
|
+
// Le composant FlexBuilder prendra toute la place ici
|
|
62
|
+
.flex-builder {
|
|
63
|
+
height: 100%; // Essayer de faire en sorte que FlexBuilder remplisse la modale
|
|
64
|
+
border: none; // Enlever la bordure par défaut de FlexBuilder s'il est dans une modale
|
|
65
|
+
border-radius: 0;
|
|
66
|
+
box-shadow: none;
|
|
67
|
+
|
|
68
|
+
.flex-builder-controls {
|
|
69
|
+
// Ajustements potentiels pour les contrôles dans la modale
|
|
70
|
+
height: 100%; // S'assurer qu'il prend toute la hauteur
|
|
71
|
+
}
|
|
72
|
+
.flex-builder-preview-area {
|
|
73
|
+
// Ajustements potentiels pour l'aperçu dans la modale
|
|
74
|
+
height: 100%;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.flex-builder-config-modal-footer {
|
|
80
|
+
padding: 15px 20px;
|
|
81
|
+
border-top: 1px solid #e0e0e0;
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: flex-end;
|
|
84
|
+
gap: 10px;
|
|
85
|
+
background-color: #f7f7f7;
|
|
86
|
+
|
|
87
|
+
button {
|
|
88
|
+
padding: 10px 20px;
|
|
89
|
+
font-size: 1em;
|
|
90
|
+
border-radius: 5px;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
border: none;
|
|
93
|
+
}
|
|
94
|
+
.btn-secondary {
|
|
95
|
+
background-color: #6c757d;
|
|
96
|
+
color: white;
|
|
97
|
+
&:hover {
|
|
98
|
+
background-color: #5a6268;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
.btn-primary {
|
|
102
|
+
background-color: #007bff;
|
|
103
|
+
color: white;
|
|
104
|
+
&:hover {
|
|
105
|
+
background-color: #0056b3;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { FlexNodeRenderer } from "./FlexNode.jsx";
|
|
5
|
+
|
|
6
|
+
const FlexBuilderPreview = ({
|
|
7
|
+
flexStructure,
|
|
8
|
+
onSelectNode,
|
|
9
|
+
selectedNodeId,
|
|
10
|
+
filteredData,
|
|
11
|
+
dataIndexRef,
|
|
12
|
+
modelFields,
|
|
13
|
+
dnd,
|
|
14
|
+
onMoveNode
|
|
15
|
+
}) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex-builder-preview-area">
|
|
20
|
+
<h3>{t('preview', 'Aperçu')}</h3>
|
|
21
|
+
<FlexNodeRenderer
|
|
22
|
+
node={flexStructure}
|
|
23
|
+
path={[]}
|
|
24
|
+
t={t}
|
|
25
|
+
onSelectNode={onSelectNode}
|
|
26
|
+
selectedNodeId={selectedNodeId}
|
|
27
|
+
data={filteredData}
|
|
28
|
+
dataIndexRef={dataIndexRef}
|
|
29
|
+
modelFields={modelFields}
|
|
30
|
+
dnd={dnd}
|
|
31
|
+
onMoveNode={onMoveNode}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
FlexBuilderPreview.propTypes = {
|
|
38
|
+
flexStructure: PropTypes.object.isRequired,
|
|
39
|
+
onSelectNode: PropTypes.func.isRequired,
|
|
40
|
+
selectedNodeId: PropTypes.string,
|
|
41
|
+
filteredData: PropTypes.array.isRequired,
|
|
42
|
+
dataIndexRef: PropTypes.shape({ current: PropTypes.number }).isRequired,
|
|
43
|
+
modelFields: PropTypes.array.isRequired,
|
|
44
|
+
dnd: PropTypes.object.isRequired,
|
|
45
|
+
onMoveNode: PropTypes.func.isRequired,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default FlexBuilderPreview;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// Helper to recursively remove a node. Returns { newStructure: object, removedNode: object | null }
|
|
5
|
+
export const deepRemoveNode = (current, nodeIdToRemove) => {
|
|
6
|
+
let removedNodeInstance = null;
|
|
7
|
+
|
|
8
|
+
if (!current.children) {
|
|
9
|
+
return { newStructure: current, removedNode: null };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const newChildren = [];
|
|
13
|
+
let modifiedChildren = false;
|
|
14
|
+
|
|
15
|
+
for (const child of current.children) {
|
|
16
|
+
if (child.id === nodeIdToRemove) {
|
|
17
|
+
removedNodeInstance = child;
|
|
18
|
+
modifiedChildren = true;
|
|
19
|
+
} else {
|
|
20
|
+
if (child.type === 'container' || (child.type === 'item' && child.content?.type === 'nestedContainer')) {
|
|
21
|
+
const containerNode = child.type === 'container' ? child : child.content.nestedContainer;
|
|
22
|
+
const result = deepRemoveNode(containerNode, nodeIdToRemove);
|
|
23
|
+
|
|
24
|
+
if (result.removedNode) {
|
|
25
|
+
removedNodeInstance = result.removedNode;
|
|
26
|
+
modifiedChildren = true;
|
|
27
|
+
if (child.type === 'container') {
|
|
28
|
+
newChildren.push(result.newStructure);
|
|
29
|
+
} else {
|
|
30
|
+
newChildren.push({ ...child, content: { ...child.content, nestedContainer: result.newStructure } });
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
newChildren.push(child);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
newChildren.push(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (modifiedChildren) {
|
|
42
|
+
return { newStructure: { ...current, children: newChildren }, removedNode: removedNodeInstance };
|
|
43
|
+
}
|
|
44
|
+
return { newStructure: current, removedNode: null };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Helper to recursively insert a node. Returns { newStructure: object, inserted: boolean }
|
|
48
|
+
export const deepInsertNode = (current, targetParentId, nodeToInsert, index) => {
|
|
49
|
+
if (current.id === targetParentId) {
|
|
50
|
+
const currentChildren = current.children || [];
|
|
51
|
+
const newTargetChildren = [...currentChildren];
|
|
52
|
+
newTargetChildren.splice(index, 0, nodeToInsert);
|
|
53
|
+
return { newStructure: { ...current, children: newTargetChildren }, inserted: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!current.children) {
|
|
57
|
+
return { newStructure: current, inserted: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let opCompleted = false;
|
|
61
|
+
const newChildren = current.children.map(child => {
|
|
62
|
+
if (opCompleted) return child;
|
|
63
|
+
|
|
64
|
+
if (child.type === 'container' || (child.type === 'item' && child.content?.type === 'nestedContainer')) {
|
|
65
|
+
const containerNode = child.type === 'container' ? child : child.content.nestedContainer;
|
|
66
|
+
const result = deepInsertNode(containerNode, targetParentId, nodeToInsert, index);
|
|
67
|
+
|
|
68
|
+
if (result.inserted) {
|
|
69
|
+
opCompleted = true;
|
|
70
|
+
if (child.type === 'container') {
|
|
71
|
+
return result.newStructure;
|
|
72
|
+
} else {
|
|
73
|
+
return { ...child, content: { ...child.content, nestedContainer: result.newStructure } };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return child;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (opCompleted) {
|
|
81
|
+
return { newStructure: { ...current, children: newChildren }, inserted: true };
|
|
82
|
+
}
|
|
83
|
+
return { newStructure: current, inserted: false };
|
|
84
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/components/FlexDataRenderer.jsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import {FaCheckSquare, FaCopy, FaExternalLinkAlt, FaImage, FaRegSquare} from 'react-icons/fa';
|
|
6
|
+
import './FlexDataRenderer.scss';
|
|
7
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
8
|
+
import RelationValue from "./RelationValue.jsx";
|
|
9
|
+
import {CodeField, ColorField} from "./Field.jsx";
|
|
10
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
11
|
+
import {DialogProvider} from "./Dialog.jsx";
|
|
12
|
+
|
|
13
|
+
// Helper for date formatting
|
|
14
|
+
const formatDate = (value, type, t) => {
|
|
15
|
+
if (!value) return '';
|
|
16
|
+
try {
|
|
17
|
+
const date = new Date(value);
|
|
18
|
+
if (isNaN(date.getTime())) return t('flexDataRenderer.invalidDate', 'Date invalide');
|
|
19
|
+
|
|
20
|
+
// Adjust formatting as needed, or use a library like date-fns or moment
|
|
21
|
+
if (type === 'datetime-local' || type === 'datetime') {
|
|
22
|
+
return date.toLocaleString();
|
|
23
|
+
}
|
|
24
|
+
if (type === 'date') {
|
|
25
|
+
return date.toLocaleDateString();
|
|
26
|
+
}
|
|
27
|
+
return date.toISOString(); // Fallback
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error("Error formatting date:", e);
|
|
30
|
+
return t('flexDataRenderer.invalidDate', 'Date invalide');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const FlexDataRenderer = ({ value, fieldDefinition,data }) => {
|
|
35
|
+
const { t, i18n } = useTranslation();
|
|
36
|
+
const {models} = useModelContext()
|
|
37
|
+
const {me} = useAuthContext()
|
|
38
|
+
// 'translatedLabel' is added by getFieldDefinitionByPath
|
|
39
|
+
const field = fieldDefinition;
|
|
40
|
+
const translatedLabel = t('model_'+field.name, field.name);
|
|
41
|
+
|
|
42
|
+
const renderContent = () => {
|
|
43
|
+
|
|
44
|
+
if (value === undefined || value === null) {
|
|
45
|
+
return <span className="data-value-empty">({t('flexDataRenderer.noValue', 'N/A')})</span>;
|
|
46
|
+
}
|
|
47
|
+
if( field.type === 'relation' && !models.find(f => f.name === field.relation && f._user === me?.username ))
|
|
48
|
+
return '';
|
|
49
|
+
if (field.type === "relation" && typeof field.relation === "string") {
|
|
50
|
+
return <RelationValue field={field} data={data}/>;
|
|
51
|
+
}
|
|
52
|
+
if ((field.type === "date" || field.type === "datetime") && value) {
|
|
53
|
+
return new Date(value).toLocaleDateString(i18n.resolvedLanguage || i18n.language, {
|
|
54
|
+
day: "numeric",
|
|
55
|
+
month: "numeric",
|
|
56
|
+
year: "numeric",
|
|
57
|
+
hour: "numeric",
|
|
58
|
+
minute: "numeric"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (field.type === "enum") {
|
|
62
|
+
return field.items.includes(value) ? `${t(value, value)}` : `${value || ''}`;
|
|
63
|
+
}
|
|
64
|
+
if (field.type === "code") {
|
|
65
|
+
const v = typeof(value) === "string" ? value : (value ? JSON.stringify(value) : '');
|
|
66
|
+
return <CodeField language={field.language} name={field.name} value={v} disabled={true} />
|
|
67
|
+
}
|
|
68
|
+
if (field.type === "object") {
|
|
69
|
+
return <CodeField language={'json'} name={field.name} value={value ? JSON.stringify(value) : ''} disabled={true} />
|
|
70
|
+
}
|
|
71
|
+
if (field.type === 'email') {
|
|
72
|
+
return <a href={"mailto:"+value}>{value}</a>;
|
|
73
|
+
}
|
|
74
|
+
if (field.type === 'phone') {
|
|
75
|
+
return <a href={"tel:"+value}>{value}</a>;
|
|
76
|
+
}
|
|
77
|
+
if (field.type === 'model') {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
if (field.type === 'password') {
|
|
81
|
+
return <></>;
|
|
82
|
+
}
|
|
83
|
+
if (field.type === 'array') {
|
|
84
|
+
let t;
|
|
85
|
+
if (field.itemsType === 'file') {
|
|
86
|
+
t = (value || []).map(it =>
|
|
87
|
+
it &&
|
|
88
|
+
<a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
|
|
89
|
+
rel="noopener noreferrer"><img src={`/resources/${it.guid}`}
|
|
90
|
+
alt={`${it.name} (${it.guid})`}/></a>
|
|
91
|
+
);
|
|
92
|
+
return {t};
|
|
93
|
+
}
|
|
94
|
+
return value.join(', ');
|
|
95
|
+
}
|
|
96
|
+
if (field.type === 'url') {
|
|
97
|
+
return value && (<><a href={value}
|
|
98
|
+
title={value} style={{color: field.color}}
|
|
99
|
+
className={"link-value"}
|
|
100
|
+
target="_blank">{value}</a>
|
|
101
|
+
<button title={"Copy URL"} onClick={(e) => {
|
|
102
|
+
navigator.clipboard.writeText(value).then(function () {
|
|
103
|
+
console.log('Async: Copying to clipboard was successful!');
|
|
104
|
+
}, function (err) {
|
|
105
|
+
console.error('Async: Could not copy text: ', err);
|
|
106
|
+
});
|
|
107
|
+
}}><FaCopy/></button>
|
|
108
|
+
</>)
|
|
109
|
+
}
|
|
110
|
+
if (field.type === "file" && value) {
|
|
111
|
+
if (['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml', 'image/webp', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/x-windows-bmp'].includes(value.type))
|
|
112
|
+
return <a
|
|
113
|
+
href={`/resources/${value.guid}`} target="_blank"
|
|
114
|
+
rel="noopener noreferrer"><img
|
|
115
|
+
src={`/resources/${value.guid}`}
|
|
116
|
+
alt={`${value.name} (${value.guid})`}/></a>;
|
|
117
|
+
return <a style={{color: field.color}}
|
|
118
|
+
href={`/resources/${value.guid}`}
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="noopener noreferrer">{value.name} ({value.guid})</a>;
|
|
121
|
+
}
|
|
122
|
+
if (field.type === 'number') {
|
|
123
|
+
let val = value;
|
|
124
|
+
if (val && field.unit) {
|
|
125
|
+
let formatter = new Intl.NumberFormat(lang);
|
|
126
|
+
val = formatter.format(value);
|
|
127
|
+
}
|
|
128
|
+
return val ? `${val} ${field.unit || ''}` : '';
|
|
129
|
+
}
|
|
130
|
+
if (field.type === "boolean") {
|
|
131
|
+
return value ? t('yes') : t('no');
|
|
132
|
+
}
|
|
133
|
+
if (field.type === 'string_t') {
|
|
134
|
+
return t(value.value, '') || value.key || (typeof(value) === 'string' ? value : '');
|
|
135
|
+
}
|
|
136
|
+
if (field.type === 'richtext') {
|
|
137
|
+
return <div className="rte-value"
|
|
138
|
+
dangerouslySetInnerHTML={{__html: value}}></div>
|
|
139
|
+
}
|
|
140
|
+
if (field.type === 'color') {
|
|
141
|
+
return <ColorField name={field.name} disabled={true}
|
|
142
|
+
value={value}/>;
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
renderContent()
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
FlexDataRenderer.propTypes = {
|
|
153
|
+
value: PropTypes.any,
|
|
154
|
+
fieldDefinition: PropTypes.shape({
|
|
155
|
+
name: PropTypes.string.isRequired,
|
|
156
|
+
type: PropTypes.string.isRequired,
|
|
157
|
+
labelKey: PropTypes.string,
|
|
158
|
+
label: PropTypes.string,
|
|
159
|
+
translatedLabel: PropTypes.string,
|
|
160
|
+
options: PropTypes.shape({
|
|
161
|
+
items: PropTypes.arrayOf(PropTypes.shape({
|
|
162
|
+
value: PropTypes.any.isRequired,
|
|
163
|
+
label: PropTypes.string.isRequired,
|
|
164
|
+
labelKey: PropTypes.string,
|
|
165
|
+
}))
|
|
166
|
+
}),
|
|
167
|
+
}).isRequired,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default FlexDataRenderer;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/components/FlexDataRenderer.scss
|
|
2
|
+
|
|
3
|
+
// Using the same overall structure as .data-badge for consistency.
|
|
4
|
+
// Specific styles for the values rendered by FlexDataRenderer:
|
|
5
|
+
|
|
6
|
+
.flex-data-renderer { // This class is on the same element as .data-badge
|
|
7
|
+
.data-badge-value {
|
|
8
|
+
// General container for the rendered value, if specific styling is needed here
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.data-value-empty {
|
|
12
|
+
color: #9ca3af; // Tailwind gray-400
|
|
13
|
+
font-style: italic;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.data-value-boolean {
|
|
17
|
+
font-size: 1.1em; // Make icons slightly larger
|
|
18
|
+
&.true {
|
|
19
|
+
color: #10b981; // Tailwind green-500
|
|
20
|
+
}
|
|
21
|
+
&.false {
|
|
22
|
+
color: #6b7280; // Tailwind gray-500
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.data-value-date {
|
|
27
|
+
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
28
|
+
font-size: 0.95em;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.data-value-email,
|
|
32
|
+
.data-value-url {
|
|
33
|
+
color: #3b82f6; // Tailwind blue-500
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
&:hover {
|
|
36
|
+
text-decoration: underline;
|
|
37
|
+
}
|
|
38
|
+
svg { // For the external link icon
|
|
39
|
+
margin-left: 0.25em;
|
|
40
|
+
font-size: 0.8em;
|
|
41
|
+
display: inline-block;
|
|
42
|
+
vertical-align: middle;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.data-value-image {
|
|
47
|
+
max-width: 120px; // Adjust as needed
|
|
48
|
+
max-height: 80px; // Adjust as needed
|
|
49
|
+
height: auto;
|
|
50
|
+
display: block;
|
|
51
|
+
border: 1px solid #e5e7eb; // Tailwind gray-200
|
|
52
|
+
border-radius: 3px;
|
|
53
|
+
margin-top: 2px; // Small space if label is above
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.data-value-color {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
.color-swatch {
|
|
60
|
+
width: 1.2em;
|
|
61
|
+
height: 1.2em;
|
|
62
|
+
border: 1px solid #d1d5db; // Tailwind gray-300
|
|
63
|
+
margin-right: 0.5em;
|
|
64
|
+
display: inline-block;
|
|
65
|
+
border-radius: 2px;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.data-value-array {
|
|
70
|
+
// Custom styling for arrays if needed, e.g., tags
|
|
71
|
+
font-style: italic;
|
|
72
|
+
color: #4b5563; // Tailwind gray-600
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.data-value-select,
|
|
76
|
+
.data-value-text,
|
|
77
|
+
.data-value-default {
|
|
78
|
+
// General text styling, if different from default .data-badge-value
|
|
79
|
+
}
|
|
80
|
+
}
|