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,269 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import './FlexBuilder.scss';
|
|
4
|
+
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import { applyFilter } from "data-primals-engine/data";
|
|
7
|
+
import { FieldSelectorModal } from "./FlexNode.jsx";
|
|
8
|
+
import { deepInsertNode, deepRemoveNode } from "./FlexBuilderUtils.js";
|
|
9
|
+
import useDragAndDrop from './hooks/useDragAndDrop.js';
|
|
10
|
+
import RichTextEditorModal from "./RichTextEditorModal.jsx";
|
|
11
|
+
import FlexBuilderControls from './FlexBuilderControls.jsx';
|
|
12
|
+
import FlexBuilderPreview from './FlexBuilderPreview.jsx';
|
|
13
|
+
import {
|
|
14
|
+
createNewNode,
|
|
15
|
+
cleanNodeForOutput,
|
|
16
|
+
findNodeRecursive,
|
|
17
|
+
updateNodeRecursive,
|
|
18
|
+
deleteNodeRecursive,
|
|
19
|
+
clearMappingsRecursive
|
|
20
|
+
} from './FlexTreeUtils.js';
|
|
21
|
+
import {DialogProvider} from "./Dialog.jsx";
|
|
22
|
+
|
|
23
|
+
const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
|
|
24
|
+
const { me: user } = useAuthContext();
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const dnd = useDragAndDrop();
|
|
27
|
+
|
|
28
|
+
const [editingHtmlInfo, setEditingHtmlInfo] = useState(null);
|
|
29
|
+
|
|
30
|
+
const defaultConfig = useMemo(() => ({
|
|
31
|
+
id: 'root', type: 'container',
|
|
32
|
+
containerStyle: { customCss: '', display: 'flex', flexDirection: 'column', flexWrap: 'nowrap', gap: '10px', padding: '10px', border: '1px dashed #999', width: '100%' },
|
|
33
|
+
children: [createNewNode('container')],
|
|
34
|
+
selectedModelName: null, dataFilter: null,
|
|
35
|
+
dataLimit: 3, refreshInterval: 60000,
|
|
36
|
+
}), [createNewNode]);
|
|
37
|
+
|
|
38
|
+
// --- CORRECTION ---
|
|
39
|
+
// Fonction utilitaire pour construire l'état à partir de la configuration chargée.
|
|
40
|
+
// Elle fusionne correctement les données par défaut, les métadonnées et la structure de la vue.
|
|
41
|
+
const buildStateFromConfig = useCallback((config) => {
|
|
42
|
+
if (config && config.flexStructure) {
|
|
43
|
+
const { flexStructure: structureData, ...metaData } = config;
|
|
44
|
+
return { ...defaultConfig, ...metaData, ...structureData };
|
|
45
|
+
}
|
|
46
|
+
return defaultConfig;
|
|
47
|
+
}, [defaultConfig]);
|
|
48
|
+
|
|
49
|
+
const [flexStructure, setFlexStructure] = useState(() => buildStateFromConfig(initialConfig));
|
|
50
|
+
|
|
51
|
+
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
|
52
|
+
const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false);
|
|
53
|
+
const currentlyEditingViewIdRef = useRef(initialConfig?.id || null);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const newViewId = initialConfig?.id || null;
|
|
57
|
+
if (newViewId !== currentlyEditingViewIdRef.current) {
|
|
58
|
+
// Utilise la même logique de construction d'état lors du changement de vue
|
|
59
|
+
setFlexStructure(buildStateFromConfig(initialConfig));
|
|
60
|
+
setSelectedNodeId(null);
|
|
61
|
+
currentlyEditingViewIdRef.current = newViewId;
|
|
62
|
+
}
|
|
63
|
+
}, [initialConfig, buildStateFromConfig]);
|
|
64
|
+
// --- FIN DE LA CORRECTION ---
|
|
65
|
+
|
|
66
|
+
const selectedNode = useMemo(() => {
|
|
67
|
+
if (!selectedNodeId) return null;
|
|
68
|
+
if (flexStructure.id === selectedNodeId) return flexStructure;
|
|
69
|
+
return findNodeRecursive(flexStructure.children, selectedNodeId);
|
|
70
|
+
}, [flexStructure, selectedNodeId]);
|
|
71
|
+
|
|
72
|
+
const currentModelFields = useMemo(() => {
|
|
73
|
+
if (!flexStructure.selectedModelName || !Array.isArray(models)) return [];
|
|
74
|
+
const model = models.find(m => m.name === flexStructure.selectedModelName);
|
|
75
|
+
return model?.fields || [];
|
|
76
|
+
}, [flexStructure.selectedModelName, models]);
|
|
77
|
+
|
|
78
|
+
const filteredData = useMemo(() => {
|
|
79
|
+
if (!flexStructure.selectedModelName) return [];
|
|
80
|
+
return applyFilter(data || [], flexStructure.dataFilter);
|
|
81
|
+
}, [data, flexStructure.dataFilter, flexStructure.selectedModelName]);
|
|
82
|
+
|
|
83
|
+
const dataIndexRef = useRef(0);
|
|
84
|
+
useEffect(() => { dataIndexRef.current = 0; }, [filteredData, flexStructure]);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (onChange) {
|
|
88
|
+
const { children, containerStyle, id, type, ...rootProps } = flexStructure;
|
|
89
|
+
const output = {
|
|
90
|
+
...rootProps,
|
|
91
|
+
id: currentlyEditingViewIdRef.current,
|
|
92
|
+
flexStructure: cleanNodeForOutput({ children, containerStyle, id, type }),
|
|
93
|
+
};
|
|
94
|
+
onChange(output);
|
|
95
|
+
}
|
|
96
|
+
}, [flexStructure, onChange]);
|
|
97
|
+
|
|
98
|
+
const handleUpdateNode = (nodeId, propertyPath, value) => {
|
|
99
|
+
setFlexStructure(prev => {
|
|
100
|
+
const updateFn = (nodeToUpdate) => {
|
|
101
|
+
const newNode = JSON.parse(JSON.stringify(nodeToUpdate)); // Deep copy for safety
|
|
102
|
+
let current = newNode;
|
|
103
|
+
const pathArray = propertyPath.split('.');
|
|
104
|
+
for (let i = 0; i < pathArray.length - 1; i++) {
|
|
105
|
+
current[pathArray[i]] = current[pathArray[i]] || {};
|
|
106
|
+
current = current[pathArray[i]];
|
|
107
|
+
}
|
|
108
|
+
current[pathArray[pathArray.length - 1]] = value;
|
|
109
|
+
return newNode;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (nodeId === prev.id) {
|
|
113
|
+
if (['selectedModelName', 'dataFilter', 'dataLimit', 'refreshInterval'].includes(propertyPath)) {
|
|
114
|
+
return { ...prev, [propertyPath]: value };
|
|
115
|
+
}
|
|
116
|
+
return updateFn(prev);
|
|
117
|
+
}
|
|
118
|
+
return { ...prev, children: updateNodeRecursive(prev.children, nodeId, updateFn) };
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleAddChildNode = (parentId, type) => {
|
|
123
|
+
const newNode = createNewNode(type);
|
|
124
|
+
const updateFn = (node) => ({ ...node, children: [...(node.children || []), newNode] });
|
|
125
|
+
setFlexStructure(prev => {
|
|
126
|
+
if (prev.id === parentId) return updateFn(prev);
|
|
127
|
+
return { ...prev, children: updateNodeRecursive(prev.children, parentId, updateFn) };
|
|
128
|
+
});
|
|
129
|
+
setSelectedNodeId(newNode.id);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleMakeItemNestedContainer = (itemId) => {
|
|
133
|
+
const updateFn = (node) => {
|
|
134
|
+
if (node.type === 'item' && node.content.type !== 'nestedContainer') {
|
|
135
|
+
const newNestedContainer = createNewNode('container');
|
|
136
|
+
newNestedContainer.children = [createNewNode('item')];
|
|
137
|
+
return { ...node, content: { type: 'nestedContainer', nestedContainer: newNestedContainer } };
|
|
138
|
+
}
|
|
139
|
+
return node;
|
|
140
|
+
};
|
|
141
|
+
setFlexStructure(prev => ({ ...prev, children: updateNodeRecursive(prev.children, itemId, updateFn) }));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleDeleteNode = () => {
|
|
145
|
+
if (!selectedNodeId || selectedNodeId === flexStructure.id) {
|
|
146
|
+
alert(t('flexBuilder.cannotDeleteRoot'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (window.confirm(t('flexBuilder.confirmDeleteNode'))) {
|
|
150
|
+
setFlexStructure(prev => ({ ...prev, children: deleteNodeRecursive(prev.children, selectedNodeId) }));
|
|
151
|
+
setSelectedNodeId(null);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleModelChange = (modelName) => {
|
|
156
|
+
setFlexStructure(prev => ({
|
|
157
|
+
...prev,
|
|
158
|
+
selectedModelName: modelName,
|
|
159
|
+
dataFilter: null,
|
|
160
|
+
children: clearMappingsRecursive(prev.children)
|
|
161
|
+
}));
|
|
162
|
+
setSelectedNodeId(null);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleFieldSelectedForMapping = (field) => {
|
|
166
|
+
if (selectedNode && selectedNode.type === 'item') {
|
|
167
|
+
const fieldPath = field.path || field.name;
|
|
168
|
+
const displayName = field.displayName || field.name;
|
|
169
|
+
if (!fieldPath) return;
|
|
170
|
+
const newContent = { type: 'dataField', mapping: { fieldPath, displayName } };
|
|
171
|
+
handleUpdateNode(selectedNode.id, 'content', newContent);
|
|
172
|
+
}
|
|
173
|
+
setIsFieldSelectorOpen(false);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleMoveNode = useCallback((nodeId, newParentId, newIndex) => {
|
|
177
|
+
if (!nodeId || !newParentId) {
|
|
178
|
+
dnd.handleItemDragEnd();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setFlexStructure(prev => {
|
|
182
|
+
const removal = deepRemoveNode(prev, nodeId);
|
|
183
|
+
if (!removal.removedNode) return prev;
|
|
184
|
+
const insertion = deepInsertNode(removal.newStructure, newParentId, removal.removedNode, newIndex);
|
|
185
|
+
if (!insertion.inserted) return prev;
|
|
186
|
+
if (selectedNodeId === nodeId) setSelectedNodeId(null);
|
|
187
|
+
return insertion.newStructure;
|
|
188
|
+
});
|
|
189
|
+
}, [dnd, selectedNodeId]);
|
|
190
|
+
|
|
191
|
+
const handleSaveHtmlContent = (newHtml) => {
|
|
192
|
+
if (editingHtmlInfo) {
|
|
193
|
+
const newContent = { type: 'richtext', html: newHtml };
|
|
194
|
+
handleUpdateNode(editingHtmlInfo.nodeId, 'content', newContent);
|
|
195
|
+
setEditingHtmlInfo(null);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const modelOptions = useMemo(() => models.map(m => ({ value: m.name, label: t(`model_${m.name}`, m.name) })), [models, t]);
|
|
200
|
+
const flexOptions = useMemo(() => ({
|
|
201
|
+
flexDirection: ['row', 'row-reverse', 'column', 'column-reverse'],
|
|
202
|
+
flexWrap: ['nowrap', 'wrap', 'wrap-reverse'],
|
|
203
|
+
justifyContent: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'],
|
|
204
|
+
alignItems: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline'],
|
|
205
|
+
}), []);
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="flex-builder">
|
|
209
|
+
<FlexBuilderControls
|
|
210
|
+
flexStructure={flexStructure}
|
|
211
|
+
selectedNode={selectedNode}
|
|
212
|
+
models={models}
|
|
213
|
+
user={user}
|
|
214
|
+
lang={lang}
|
|
215
|
+
modelOptions={modelOptions}
|
|
216
|
+
flexOptions={flexOptions}
|
|
217
|
+
currentModelFields={currentModelFields}
|
|
218
|
+
onModelChange={handleModelChange}
|
|
219
|
+
onUpdateNode={handleUpdateNode}
|
|
220
|
+
onDeleteNode={handleDeleteNode}
|
|
221
|
+
onAddChildNode={handleAddChildNode}
|
|
222
|
+
onMakeItemNestedContainer={handleMakeItemNestedContainer}
|
|
223
|
+
onSetIsFieldSelectorOpen={setIsFieldSelectorOpen}
|
|
224
|
+
onSetEditingHtmlInfo={setEditingHtmlInfo}
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<FlexBuilderPreview
|
|
228
|
+
flexStructure={flexStructure}
|
|
229
|
+
onSelectNode={setSelectedNodeId}
|
|
230
|
+
selectedNodeId={selectedNodeId}
|
|
231
|
+
filteredData={filteredData}
|
|
232
|
+
dataIndexRef={dataIndexRef}
|
|
233
|
+
modelFields={currentModelFields}
|
|
234
|
+
dnd={dnd}
|
|
235
|
+
onMoveNode={handleMoveNode}
|
|
236
|
+
/>
|
|
237
|
+
|
|
238
|
+
<DialogProvider>
|
|
239
|
+
{selectedNode?.type === 'item' && (
|
|
240
|
+
<FieldSelectorModal
|
|
241
|
+
isOpen={isFieldSelectorOpen}
|
|
242
|
+
onClose={() => setIsFieldSelectorOpen(false)}
|
|
243
|
+
modelFields={currentModelFields}
|
|
244
|
+
allModels={models}
|
|
245
|
+
onSelectField={handleFieldSelectedForMapping}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{editingHtmlInfo && (
|
|
250
|
+
<RichTextEditorModal
|
|
251
|
+
initialContent={editingHtmlInfo.initialContent}
|
|
252
|
+
onClose={() => setEditingHtmlInfo(null)}
|
|
253
|
+
onSave={handleSaveHtmlContent}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
</DialogProvider>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
FlexBuilder.propTypes = {
|
|
262
|
+
initialConfig: PropTypes.object,
|
|
263
|
+
onChange: PropTypes.func.isRequired,
|
|
264
|
+
data: PropTypes.array,
|
|
265
|
+
models: PropTypes.array.isRequired,
|
|
266
|
+
lang: PropTypes.string,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export default FlexBuilder;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/components/FlexBuilder.scss
|
|
2
|
+
|
|
3
|
+
.flex-builder {
|
|
4
|
+
display: flex;
|
|
5
|
+
font-family: sans-serif;
|
|
6
|
+
border: 1px solid #ccc;
|
|
7
|
+
border-radius: 4px;
|
|
8
|
+
min-height: 600px; // Give it some space
|
|
9
|
+
background-color: #f9f9f9;
|
|
10
|
+
|
|
11
|
+
.flex-builder-controls {
|
|
12
|
+
width: 350px;
|
|
13
|
+
padding: 15px;
|
|
14
|
+
background-color: #fff;
|
|
15
|
+
border-right: 1px solid #ccc;
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
box-shadow: 2px 0 5px rgba(0,0,0,0.05);
|
|
18
|
+
|
|
19
|
+
h3, h4 {
|
|
20
|
+
margin-top: 0;
|
|
21
|
+
margin-bottom: 10px;
|
|
22
|
+
padding-bottom: 5px;
|
|
23
|
+
border-bottom: 1px solid #eee;
|
|
24
|
+
color: #333;
|
|
25
|
+
}
|
|
26
|
+
h4 {
|
|
27
|
+
font-size: 1em;
|
|
28
|
+
margin-top: 15px;
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
align-items: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.control-group {
|
|
35
|
+
margin-bottom: 12px;
|
|
36
|
+
label {
|
|
37
|
+
display: block;
|
|
38
|
+
margin-bottom: 4px;
|
|
39
|
+
font-size: 0.9em;
|
|
40
|
+
color: #555;
|
|
41
|
+
font-weight: 500;
|
|
42
|
+
}
|
|
43
|
+
input[type="text"],
|
|
44
|
+
input[type="number"],
|
|
45
|
+
select,
|
|
46
|
+
.select-field-container .select-field { // Target your SelectField specifically
|
|
47
|
+
width: 100%;
|
|
48
|
+
padding: 8px;
|
|
49
|
+
border: 1px solid #ccc;
|
|
50
|
+
border-radius: 4px;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
font-size: 0.9em;
|
|
53
|
+
}
|
|
54
|
+
button {
|
|
55
|
+
padding: 6px 10px;
|
|
56
|
+
margin-right: 5px;
|
|
57
|
+
margin-top: 5px;
|
|
58
|
+
border: 1px solid #ccc;
|
|
59
|
+
background-color: #f0f0f0;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
font-size: 0.85em;
|
|
63
|
+
color: #575757;
|
|
64
|
+
&:hover {
|
|
65
|
+
background-color: #e0e0e0;
|
|
66
|
+
}
|
|
67
|
+
&.btn-delete-node {
|
|
68
|
+
background-color: transparent;
|
|
69
|
+
border: none;
|
|
70
|
+
color: #dc3545;
|
|
71
|
+
padding: 2px 5px;
|
|
72
|
+
&:hover {
|
|
73
|
+
color: #c82333;
|
|
74
|
+
background-color: rgba(220, 53, 69, 0.1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
&.actions {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
gap: 5px;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
.selected-node-controls {
|
|
85
|
+
border-top: 2px solid #007bff;
|
|
86
|
+
padding-top: 10px;
|
|
87
|
+
margin-top: 15px;
|
|
88
|
+
}
|
|
89
|
+
.no-node-selected-hint {
|
|
90
|
+
color: #777;
|
|
91
|
+
font-style: italic;
|
|
92
|
+
text-align: center;
|
|
93
|
+
margin-top: 20px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.flex-builder-preview-area {
|
|
98
|
+
flex-grow: 1;
|
|
99
|
+
padding: 15px;
|
|
100
|
+
overflow: auto; // Important for deeply nested structures
|
|
101
|
+
|
|
102
|
+
h3 {
|
|
103
|
+
margin-top: 0;
|
|
104
|
+
margin-bottom: 15px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.flex-node { // Common styles for both container and item in preview
|
|
108
|
+
transition: outline 0.2s ease-in-out;
|
|
109
|
+
background-color: #f5f5f5;
|
|
110
|
+
&.is-selected {
|
|
111
|
+
outline: 2px solid #007bff !important; // Force outline for selected
|
|
112
|
+
box-shadow: 0 0 8px rgba(0, 123, 255, 0.5);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.preview-container {
|
|
117
|
+
// Default styles are applied via JS, this is for base and selection
|
|
118
|
+
border: 1px dashed #aaa; // Default border for all containers
|
|
119
|
+
padding: 8px; // Default padding
|
|
120
|
+
min-height: 40px; // Minimum height to be clickable
|
|
121
|
+
position: relative; // For placeholder text
|
|
122
|
+
|
|
123
|
+
&.is-selected {
|
|
124
|
+
border-style: solid;
|
|
125
|
+
border-color: #007bff;
|
|
126
|
+
}
|
|
127
|
+
&:hover:not(.is-selected) {
|
|
128
|
+
border-color: #66afe9;
|
|
129
|
+
}
|
|
130
|
+
.empty-container-placeholder {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 50%;
|
|
133
|
+
left: 50%;
|
|
134
|
+
transform: translate(-50%, -50%);
|
|
135
|
+
color: #aaa;
|
|
136
|
+
font-size: 0.9em;
|
|
137
|
+
pointer-events: none; // Allow clicks to pass through to the container
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.preview-item {
|
|
142
|
+
border: 1px solid #ccc;
|
|
143
|
+
padding: 8px;
|
|
144
|
+
min-height: 30px; // Minimum height
|
|
145
|
+
display: flex; // To center content by default
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
text-align: center;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
word-break: break-word;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
background-color: #fff;
|
|
153
|
+
|
|
154
|
+
&.is-selected {
|
|
155
|
+
border-color: #007bff;
|
|
156
|
+
background-color: #e7f3ff;
|
|
157
|
+
}
|
|
158
|
+
&:hover:not(.is-selected) {
|
|
159
|
+
border-color: #66afe9;
|
|
160
|
+
background-color: #f8f9fa;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.preview-item-content {
|
|
164
|
+
font-size: 0.9em;
|
|
165
|
+
color: #333;
|
|
166
|
+
max-width: 100%;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
white-space: nowrap; // Prevent content from wrapping and breaking layout
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// When an item contains a nested container
|
|
173
|
+
& > .preview-container { // Direct child container
|
|
174
|
+
margin: -8px; // Offset parent item's padding
|
|
175
|
+
width: calc(100% + 16px); // Expand to fill parent item, accounting for offset padding
|
|
176
|
+
height: calc(100% + 16px);
|
|
177
|
+
min-height: calc(100% + 16px); // Ensure it fills
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Styles for FieldSelectorModal (can be reused or adapted from previous version)
|
|
184
|
+
.flex-builder-modal-overlay {
|
|
185
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
186
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
187
|
+
display: flex; align-items: center; justify-content: center;
|
|
188
|
+
z-index: 1050; padding: 15px;
|
|
189
|
+
}
|
|
190
|
+
.flex-builder-modal-content {
|
|
191
|
+
background-color: white; padding: 25px; border-radius: 8px;
|
|
192
|
+
min-width: 320px; max-width: 600px; width: 90%; max-height: 85vh;
|
|
193
|
+
display: flex; flex-direction: column; box-shadow: 0 5px 20px rgba(0,0,0,0.25);
|
|
194
|
+
h4 {
|
|
195
|
+
margin-top: 0; margin-bottom: 20px; border-bottom: 1px solid #e0e0e0;
|
|
196
|
+
padding-bottom: 15px; font-size: 1.2em; color: #333;
|
|
197
|
+
}
|
|
198
|
+
.field-selector-list {
|
|
199
|
+
list-style: none; padding: 0; margin: 0 0 20px 0;
|
|
200
|
+
overflow-y: auto; flex-grow: 1;
|
|
201
|
+
li {
|
|
202
|
+
padding: 10px 15px; cursor: pointer; border-bottom: 1px solid #f0f0f0;
|
|
203
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
204
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
205
|
+
&:last-child { border-bottom: none; }
|
|
206
|
+
&:hover {
|
|
207
|
+
background-color: #007bff; color: white;
|
|
208
|
+
.field-path { color: #e0e0e0; }
|
|
209
|
+
}
|
|
210
|
+
em { font-style: italic; color: #6c757d; }
|
|
211
|
+
.field-path { font-size: 0.85em; color: #6c757d; margin-left: 10px; white-space: nowrap; }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
.btn-cancel {
|
|
215
|
+
margin-top: auto; padding: 10px 18px; background-color: #6c757d;
|
|
216
|
+
color: white; border: none; border-radius: 4px; cursor: pointer;
|
|
217
|
+
align-self: flex-end;
|
|
218
|
+
&:hover { background-color: #5a6268; }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Styles for ConditionBuilder integration
|
|
223
|
+
.flex-builder-controls .control-group .condition-builder {
|
|
224
|
+
border: 1px solid #e0e0e0; padding: 10px; border-radius: 4px;
|
|
225
|
+
background-color: #fdfdfd; margin-top: 5px;
|
|
226
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { FaTrash, FaBox, FaExpandArrowsAlt, FaObjectGroup, FaEdit, FaFileAlt } from 'react-icons/fa';
|
|
5
|
+
import { CodeField, NumberField, SelectField } from './Field.jsx';
|
|
6
|
+
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
7
|
+
|
|
8
|
+
const FlexBuilderControls = ({
|
|
9
|
+
flexStructure,
|
|
10
|
+
selectedNode,
|
|
11
|
+
models,
|
|
12
|
+
user,
|
|
13
|
+
lang,
|
|
14
|
+
modelOptions,
|
|
15
|
+
flexOptions,
|
|
16
|
+
currentModelFields,
|
|
17
|
+
onModelChange,
|
|
18
|
+
onUpdateNode,
|
|
19
|
+
onDeleteNode,
|
|
20
|
+
onAddChildNode,
|
|
21
|
+
onMakeItemNestedContainer,
|
|
22
|
+
onSetIsFieldSelectorOpen,
|
|
23
|
+
onSetEditingHtmlInfo,
|
|
24
|
+
}) => {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
|
|
27
|
+
const handleDataLimitChange = (e) => {
|
|
28
|
+
let value = parseInt(e.target.value, 10);
|
|
29
|
+
if (isNaN(value) || value < 1) value = 1;
|
|
30
|
+
if (value > 8) value = 8;
|
|
31
|
+
onUpdateNode(flexStructure.id, 'dataLimit', value);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleRefreshIntervalChange = (e) => {
|
|
35
|
+
onUpdateNode(flexStructure.id, 'refreshInterval', parseInt(e.target.value, 10) || 0);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex-builder-controls">
|
|
40
|
+
{/* Section des paramètres de données */}
|
|
41
|
+
<h3>{t('dataSettings', 'Paramètres des données')}</h3>
|
|
42
|
+
<div className="control-group">
|
|
43
|
+
<label htmlFor="selectedModel">{t('flexBuilder.selectDataModel', 'Modèle de données')}:</label>
|
|
44
|
+
<SelectField
|
|
45
|
+
id="selectedModel"
|
|
46
|
+
value={flexStructure.selectedModelName}
|
|
47
|
+
onChange={(selected) => onModelChange(selected.value)}
|
|
48
|
+
items={[{ value: null, label: t('flexBuilder.selectAModel', 'Sélectionner un modèle...') }, ...modelOptions]}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
{flexStructure.selectedModelName && (
|
|
52
|
+
<>
|
|
53
|
+
<label>{t('dataFilter', 'Filtre des données')}:</label>
|
|
54
|
+
<div className="control-group">
|
|
55
|
+
<ConditionBuilder
|
|
56
|
+
modelFields={models.find(f => f.name === flexStructure.selectedModelName)?.fields || []}
|
|
57
|
+
model={flexStructure.selectedModelName}
|
|
58
|
+
models={models}
|
|
59
|
+
selectableModels={false}
|
|
60
|
+
initialValue={flexStructure.dataFilter}
|
|
61
|
+
onChange={(newCondition) => onUpdateNode(flexStructure.id, 'dataFilter', newCondition || {})}
|
|
62
|
+
lang={lang}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="form">
|
|
66
|
+
<NumberField
|
|
67
|
+
label={t('flexBuilder.dataLimit', "Nombre d'éléments affichés (1-8)")}
|
|
68
|
+
id="flex-data-limit"
|
|
69
|
+
value={flexStructure.dataLimit || 1}
|
|
70
|
+
onChange={handleDataLimitChange}
|
|
71
|
+
min="1"
|
|
72
|
+
max="8"
|
|
73
|
+
/>
|
|
74
|
+
<NumberField
|
|
75
|
+
label={t('flexBuilder.refreshInterval', "Intervalle de rafraîchissement (ms)")}
|
|
76
|
+
id="flex-refresh-interval"
|
|
77
|
+
value={flexStructure.refreshInterval || 60000}
|
|
78
|
+
onChange={handleRefreshIntervalChange}
|
|
79
|
+
min="0"
|
|
80
|
+
placeholder="0 pour désactiver"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Section des paramètres du nœud sélectionné */}
|
|
87
|
+
{selectedNode && (
|
|
88
|
+
<div className="selected-node-controls">
|
|
89
|
+
<h4>
|
|
90
|
+
{t('flexBuilder.selectedNodeSettings', 'Paramètres')}: {t(selectedNode.type)} ({selectedNode.id.substring(0, 8)})
|
|
91
|
+
{selectedNode.id !== flexStructure.id &&
|
|
92
|
+
<button onClick={onDeleteNode} className="btn-delete-node" title={t('flexBuilder.deleteThisNode', 'Supprimer cet élément')}><FaTrash /></button>
|
|
93
|
+
}
|
|
94
|
+
</h4>
|
|
95
|
+
|
|
96
|
+
{/* Contrôles pour un conteneur */}
|
|
97
|
+
{selectedNode.type === 'container' && (
|
|
98
|
+
<>
|
|
99
|
+
<p>{t('flexBuilder.containerControlsTitle', 'Propriétés du conteneur Flex:')}</p>
|
|
100
|
+
<div className="control-group actions">
|
|
101
|
+
<button onClick={() => onAddChildNode(selectedNode.id, 'item')}><FaBox /> {t('flexBuilder.addItem', 'Ajouter une case')}</button>
|
|
102
|
+
<button onClick={() => onAddChildNode(selectedNode.id, 'container')}><FaExpandArrowsAlt /> {t('flexBuilder.addNestedContainer', 'Ajouter un conteneur')}</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="control-group"><label>{t('flexDirection')}:</label><SelectField items={flexOptions.flexDirection.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexDirection} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexDirection', sel.value)} /></div>
|
|
105
|
+
<div className="control-group"><label>{t('flexWrap')}:</label><SelectField items={flexOptions.flexWrap.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.flexWrap} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.flexWrap', sel.value)} /></div>
|
|
106
|
+
<div className="control-group"><label>{t('justifyContent')}:</label><SelectField items={flexOptions.justifyContent.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.justifyContent} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.justifyContent', sel.value)} /></div>
|
|
107
|
+
<div className="control-group"><label>{t('alignItems')}:</label><SelectField items={flexOptions.alignItems.map(o => ({ label: t(o, o), value: o }))} value={selectedNode.containerStyle.alignItems} onChange={sel => onUpdateNode(selectedNode.id, 'containerStyle.alignItems', sel.value)} /></div>
|
|
108
|
+
<div className="control-group"><label>{t('gap')}:</label><input type="text" value={selectedNode.containerStyle.gap || ''} onChange={e => onUpdateNode(selectedNode.id, 'containerStyle.gap', e.target.value)} placeholder="ex: 10px" /></div>
|
|
109
|
+
<div className="form-group"><CodeField id="customCss" name="customCss" language="css" value={selectedNode.containerStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'containerStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: red;')} rows={4} /></div>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Contrôles pour un item */}
|
|
114
|
+
{selectedNode.type === 'item' && (
|
|
115
|
+
<>
|
|
116
|
+
<p>{t('flexBuilder.itemControlsTitle', 'Propriétés de la case:')}</p>
|
|
117
|
+
<div className="control-group actions">
|
|
118
|
+
{flexStructure.selectedModelName && (
|
|
119
|
+
<button onClick={() => onSetIsFieldSelectorOpen(true)}>
|
|
120
|
+
<FaEdit /> {selectedNode.content.type === 'dataField' ? t('flexBuilder.editDataMapping', 'Modifier le champ lié') : t('flexBuilder.mapDataField', 'Lier un champ de données')}
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
<button onClick={(e) => {
|
|
124
|
+
onSetEditingHtmlInfo({ nodeId: selectedNode.id, initialContent: selectedNode.content.html || '' });
|
|
125
|
+
e.stopPropagation()
|
|
126
|
+
}}>
|
|
127
|
+
<FaFileAlt /> {t('flexBuilder.editHTML', 'Éditer le contenu')}
|
|
128
|
+
</button>
|
|
129
|
+
{selectedNode.content.type !== 'nestedContainer' && (
|
|
130
|
+
<button onClick={() => onMakeItemNestedContainer(selectedNode.id)}>
|
|
131
|
+
<FaObjectGroup /> {t('flexBuilder.makeNestedContainer', 'Transformer en conteneur')}
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
<div className="control-group"><label>{t('itemWidth')}:</label><input type="text" value={selectedNode.itemStyle.width || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.width', e.target.value)} placeholder="ex: 100px, 20%" /></div>
|
|
136
|
+
<div className="control-group"><label>{t('itemHeight')}:</label><input type="text" value={selectedNode.itemStyle.height || ''} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.height', e.target.value)} placeholder="ex: 50px, auto" /></div>
|
|
137
|
+
<div className="control-group"><label>{t('flexBuilder.itemFlexGrow', 'Flex Grow')}:</label><input type="number" value={selectedNode.itemStyle.flexGrow || 0} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexGrow', parseFloat(e.target.value) || 0)} min="0" step="0.1" /></div>
|
|
138
|
+
<div className="control-group"><label>{t('flexBuilder.itemFlexShrink', 'Flex Shrink')}:</label><input type="number" value={selectedNode.itemStyle.flexShrink === undefined ? 1 : selectedNode.itemStyle.flexShrink} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexShrink', parseFloat(e.target.value))} min="0" step="0.1" /></div>
|
|
139
|
+
<div className="control-group"><label>{t('flexBuilder.itemFlexBasis', 'Flex Basis')}:</label><input type="text" value={selectedNode.itemStyle.flexBasis || 'auto'} onChange={e => onUpdateNode(selectedNode.id, 'itemStyle.flexBasis', e.target.value)} placeholder="auto, 100px, 20%" /></div>
|
|
140
|
+
<div className="form-group"><CodeField id="customCssItem" name="customCssItem" language="css" value={selectedNode.itemStyle.customCss || ''} onChange={(e) => onUpdateNode(selectedNode.id, 'itemStyle.customCss', e.value)} placeholder={t('flexBuilder.customCssPlaceholder', 'Ex: background-color: blue;')} rows={4} /></div>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
{!selectedNode &&
|
|
146
|
+
<p className="no-node-selected-hint">{t('flexBuilder.clickToSelectNode', 'Cliquez sur un élément dans l\'aperçu pour le configurer.')}</p>
|
|
147
|
+
}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
FlexBuilderControls.propTypes = {
|
|
153
|
+
flexStructure: PropTypes.object.isRequired,
|
|
154
|
+
selectedNode: PropTypes.object,
|
|
155
|
+
models: PropTypes.array.isRequired,
|
|
156
|
+
user: PropTypes.object,
|
|
157
|
+
lang: PropTypes.string.isRequired,
|
|
158
|
+
modelOptions: PropTypes.array.isRequired,
|
|
159
|
+
flexOptions: PropTypes.object.isRequired,
|
|
160
|
+
currentModelFields: PropTypes.array.isRequired,
|
|
161
|
+
onModelChange: PropTypes.func.isRequired,
|
|
162
|
+
onUpdateNode: PropTypes.func.isRequired,
|
|
163
|
+
onDeleteNode: PropTypes.func.isRequired,
|
|
164
|
+
onAddChildNode: PropTypes.func.isRequired,
|
|
165
|
+
onMakeItemNestedContainer: PropTypes.func.isRequired,
|
|
166
|
+
onSetIsFieldSelectorOpen: PropTypes.func.isRequired,
|
|
167
|
+
onSetEditingHtmlInfo: PropTypes.func.isRequired,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default FlexBuilderControls;
|