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,247 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { useQuery, useQueryClient } from 'react-query';
|
|
3
|
+
import { useModelContext } from './contexts/ModelContext.jsx';
|
|
4
|
+
import { TextField } from './Field.jsx';
|
|
5
|
+
import { useAuthContext } from './contexts/AuthContext.jsx';
|
|
6
|
+
import {conditionToApiSearchFilter, getDataAsString, getUserId} from 'data-primals-engine/data';
|
|
7
|
+
import { FaEdit, FaTrash } from 'react-icons/fa';
|
|
8
|
+
import Button from './Button.jsx';
|
|
9
|
+
import {mainFieldsTypes} from "data-primals-engine/constants";
|
|
10
|
+
import Draggable from "./Draggable.jsx";
|
|
11
|
+
import {useTranslation} from "react-i18next";
|
|
12
|
+
|
|
13
|
+
const RelationField = ({ field, help, onFocus, onBlur, onChange, value=null }) => {
|
|
14
|
+
const { models, dataByModel, setOnSuccessCallbacks, relationIds, setRelationIds } = useModelContext();
|
|
15
|
+
const { name, relation: modelName } = field;
|
|
16
|
+
const queryClient = useQueryClient();
|
|
17
|
+
const [selectedValues, setSelectedValues] = useState([]);
|
|
18
|
+
const [showResults, setResultsVisible] = useState(false);
|
|
19
|
+
const [searchValue, setSearchValue] = useState('');
|
|
20
|
+
const [history, setHistory] = useState([]);
|
|
21
|
+
const { me } = useAuthContext();
|
|
22
|
+
const tr = useTranslation()
|
|
23
|
+
const {t, i18n} = tr
|
|
24
|
+
const model = models?.find(f => f.name === modelName && f._user === me?.username);
|
|
25
|
+
|
|
26
|
+
// Fetch related data based on search value
|
|
27
|
+
const { data: results = [], isError, refetch } = useQuery(
|
|
28
|
+
['api/search', model?.name, field?.name, searchValue],
|
|
29
|
+
async ({ signal }) => {
|
|
30
|
+
if (!model) return [];
|
|
31
|
+
const orFilter = [];
|
|
32
|
+
let filter = {};
|
|
33
|
+
if( field.relationFilter ){
|
|
34
|
+
filter = {"$and": field.relationFilter.filter};
|
|
35
|
+
}else if ( searchValue) {
|
|
36
|
+
model.fields.forEach(f => {
|
|
37
|
+
if (f.asMain) {
|
|
38
|
+
if( f.type !== 'relation' && mainFieldsTypes.includes(f.type))
|
|
39
|
+
orFilter.push({"$regexMatch": { input: '$'+f.name, regex: searchValue}});
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
if (!orFilter.length) {
|
|
43
|
+
model.fields.forEach(f => {
|
|
44
|
+
if (["string", "string_t", "richtext", "url"]?.includes(f.type)) {
|
|
45
|
+
orFilter.push({"$regexMatch": { input: '$'+f.name, regex: searchValue}});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (!orFilter.length) {
|
|
49
|
+
orFilter.push({"$eq": ['_id', searchValue]});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
filter = {'$or':orFilter};
|
|
53
|
+
}
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
params.append('_user', getUserId(me));
|
|
56
|
+
params.append('model', field.relation);
|
|
57
|
+
params.append('limit', '1000');
|
|
58
|
+
params.append('depth', '2'); // Fetch related data with depth 2
|
|
59
|
+
return fetch(`/api/data/search?${params.toString()}`, {
|
|
60
|
+
body: JSON.stringify({ filter }),
|
|
61
|
+
method: 'POST',
|
|
62
|
+
signal: signal,
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
})
|
|
65
|
+
.then(e => e.json())
|
|
66
|
+
.then(e => e.data);
|
|
67
|
+
},
|
|
68
|
+
{ enabled: !!model }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setSearchValue('');
|
|
73
|
+
setHistory([]);
|
|
74
|
+
}, [modelName]);
|
|
75
|
+
|
|
76
|
+
useEffect(()=>{
|
|
77
|
+
if(searchValue==='' && !value && !field.multiple ){
|
|
78
|
+
onChange({ name, value: null });
|
|
79
|
+
}
|
|
80
|
+
},[searchValue])
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (results?.length > 0) {
|
|
84
|
+
setHistory([...new Set([...history, ...results])]);
|
|
85
|
+
}
|
|
86
|
+
}, [results]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
updateValue();
|
|
90
|
+
}, [history]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if( value)
|
|
94
|
+
updateValue();
|
|
95
|
+
}, [value]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if( value === null ){
|
|
98
|
+
onChange({name, value: null});
|
|
99
|
+
setSearchValue('');
|
|
100
|
+
}
|
|
101
|
+
}, [value]);
|
|
102
|
+
|
|
103
|
+
//console.log(field, value);
|
|
104
|
+
const updateValue = () => {
|
|
105
|
+
if (!field.multiple) {
|
|
106
|
+
const v = history.find(d => d._id === value);
|
|
107
|
+
if (v) {
|
|
108
|
+
setSearchValue(getDataAsString(model, v, tr, models) || '');
|
|
109
|
+
onChange({ name, value });
|
|
110
|
+
} else {
|
|
111
|
+
//onChange({ name, value: null });
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
setSelectedValues(value);
|
|
116
|
+
onChange({ name, value });
|
|
117
|
+
} else {
|
|
118
|
+
setSelectedValues([]);
|
|
119
|
+
onChange({ name, value: [] });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleClick = (e, data) => {
|
|
125
|
+
if (!field.multiple) {
|
|
126
|
+
setSearchValue(getDataAsString(model, history.find(d => d._id === data._id), tr, models) || '');
|
|
127
|
+
onChange({ name, value: data._id });
|
|
128
|
+
} else {
|
|
129
|
+
if (!selectedValues.includes(data._id)) {
|
|
130
|
+
setSelectedValues(values => {
|
|
131
|
+
const value = [...values, data._id];
|
|
132
|
+
onChange({ name, value });
|
|
133
|
+
return value;
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
onChange({ name, value });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
setResultsVisible(false);
|
|
140
|
+
ref.current?.focus();
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (showResults) refetch();
|
|
146
|
+
}, [showResults]);
|
|
147
|
+
|
|
148
|
+
const handleRemove = (element) => {
|
|
149
|
+
if (!field.multiple) return;
|
|
150
|
+
setSelectedValues(values => {
|
|
151
|
+
const value = values.filter(f => f !== element);
|
|
152
|
+
onChange({ name, value });
|
|
153
|
+
return value;
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const ref = useRef();
|
|
158
|
+
const inputRef = useRef();
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if( inputRef?.current ){
|
|
162
|
+
inputRef.current.ref.setAttribute('autocomplete', 'off');
|
|
163
|
+
}
|
|
164
|
+
}, [inputRef]);
|
|
165
|
+
return (
|
|
166
|
+
<div onFocus={onFocus} onBlur={onBlur} className="field field-relation flex flex-row flex-start flex-1">
|
|
167
|
+
{help && <div className={"flex help"}>{help}</div>}
|
|
168
|
+
|
|
169
|
+
<div className="inner">
|
|
170
|
+
<TextField
|
|
171
|
+
required={!field.multiple && field.required}
|
|
172
|
+
ref={inputRef}
|
|
173
|
+
multiline={field.multiline}
|
|
174
|
+
autocomplete="none"
|
|
175
|
+
name={field.name}
|
|
176
|
+
id={field.name}
|
|
177
|
+
onFocus={e => {
|
|
178
|
+
setResultsVisible(true);
|
|
179
|
+
queryClient.invalidateQueries(['api/search', field.name, searchValue]);
|
|
180
|
+
}}
|
|
181
|
+
value={searchValue}
|
|
182
|
+
onChange={e => {
|
|
183
|
+
if (!e.target.value) onChange({ name, value: '' });
|
|
184
|
+
else onChange({ name, value: '' });
|
|
185
|
+
setResultsVisible(true);
|
|
186
|
+
setSearchValue(e.target.value);
|
|
187
|
+
}}
|
|
188
|
+
onBlur={(e) => {
|
|
189
|
+
setResultsVisible(false);
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
<Button
|
|
193
|
+
type="button" className="btn-form btn-last" onClick={() => {
|
|
194
|
+
setSearchValue('');
|
|
195
|
+
onChange({ name, value: null });
|
|
196
|
+
setResultsVisible(!showResults);
|
|
197
|
+
}}><FaEdit /></Button>
|
|
198
|
+
|
|
199
|
+
{showResults && (
|
|
200
|
+
<div className="results" onKeyDown={e => {
|
|
201
|
+
if( e.key === 'Escape' )
|
|
202
|
+
setResultsVisible(false);
|
|
203
|
+
}} onBlur={e => {
|
|
204
|
+
const t = e.target.parentNode.children[e.target.parentNode.childElementCount-1];
|
|
205
|
+
if(t === e.target){
|
|
206
|
+
setResultsVisible(false);
|
|
207
|
+
}
|
|
208
|
+
}} >
|
|
209
|
+
{!isError &&
|
|
210
|
+
(results || []).map(r => {
|
|
211
|
+
const v = getDataAsString(models.find(m => m.name === modelName && m._user === me?.username), r, tr, models);
|
|
212
|
+
return (
|
|
213
|
+
<div tabIndex={0} onKeyDown={e => {
|
|
214
|
+
if( e.key === 'Enter')
|
|
215
|
+
handleClick(e, r);
|
|
216
|
+
}} onMouseDown={e => handleClick(e, r)} className="item" key={r._id}>
|
|
217
|
+
{v}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
{field.multiple && selectedValues.length > 0 && (
|
|
225
|
+
<div ref={ref} tabIndex={0} className="selected-values flex flex-border flex-row flex-no-gap flex-start flex-1">
|
|
226
|
+
<Draggable items={selectedValues} renderItem={(id,i) =>{
|
|
227
|
+
const val = history.find(f => f._id === id) || dataByModel[modelName].find(f => f._id === id);
|
|
228
|
+
if (!val) {
|
|
229
|
+
return <div className="flex" key={id}>data non chargée<FaTrash onClick={() => handleRemove(id)} /></div>;
|
|
230
|
+
}
|
|
231
|
+
const v = getDataAsString(models.find(m => m.name === modelName && m._user === me?.username), val, tr, models);
|
|
232
|
+
return <div onClick={() => handleRemove(id)} className="selected-value flex" key={id}>
|
|
233
|
+
<span className="flex-1">{v}</span>
|
|
234
|
+
<FaTrash className="cursor-pointer" />
|
|
235
|
+
</div>;
|
|
236
|
+
}} onChange={(arr) => {
|
|
237
|
+
setSelectedValues(arr);
|
|
238
|
+
onChange({ name, value: arr });
|
|
239
|
+
}} />
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export default RelationField;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useQuery } from 'react-query';
|
|
3
|
+
import { useModelContext } from './contexts/ModelContext.jsx';
|
|
4
|
+
import {getDataAsString, getUserId} from 'data-primals-engine/data';
|
|
5
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
6
|
+
import {Dialog, DialogProvider} from './Dialog.jsx';
|
|
7
|
+
import {FaMagnifyingGlass} from "react-icons/fa6";
|
|
8
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
9
|
+
import {CodeField, ColorField} from "./Field.jsx";
|
|
10
|
+
import { randomColor } from "randomcolor";
|
|
11
|
+
import {getObjectHash, getRand, isLightColor, setSeed} from "data-primals-engine/core";
|
|
12
|
+
|
|
13
|
+
const RelationValue = ({ field, data, align }) => {
|
|
14
|
+
const { models, relations, setRelations, dataByModel, relationIds, setRelationIds, setDatasToLoad, datasToLoad } = useModelContext();
|
|
15
|
+
const [popupVisible, setPopupVisible] = useState(false);
|
|
16
|
+
const tr = useTranslation();
|
|
17
|
+
const { t, i18n } = tr;
|
|
18
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
19
|
+
const { me } = useAuthContext();
|
|
20
|
+
const value = data[field.name];
|
|
21
|
+
const relatedModelName = field.relation;
|
|
22
|
+
const model = models.find(m => m.name === relatedModelName && m._user === me?.username);
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
params.append('_user', getUserId(me));
|
|
25
|
+
params.append('model', relatedModelName);
|
|
26
|
+
params.append('depth', '1');
|
|
27
|
+
params.append('lang', lang);
|
|
28
|
+
let ids = [];// Fetch related data with depth 2
|
|
29
|
+
if( value ){
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
ids = value.map(v => v._id || v).join(',');
|
|
32
|
+
} else {
|
|
33
|
+
ids = [value._id || value];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
params.append('ids', ids);
|
|
37
|
+
const { data: relatedData, isLoading } = useQuery(
|
|
38
|
+
['api/data/relation', field, value],
|
|
39
|
+
async () => {
|
|
40
|
+
if (!model || (typeof(ids) === 'object' && !ids.length) || !value) return [];
|
|
41
|
+
const rel = relations['api/data'+model.name+getObjectHash({ids})];
|
|
42
|
+
if( rel )
|
|
43
|
+
return Promise.resolve(rel.data);
|
|
44
|
+
|
|
45
|
+
return fetch(`/api/data/search?${params.toString()}`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
})
|
|
49
|
+
.then(res => res.json())
|
|
50
|
+
.then(res => {
|
|
51
|
+
return res.data
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
{ enabled: !!model && !!value, onSettled: (data) => {
|
|
55
|
+
|
|
56
|
+
const dt = {...relations};
|
|
57
|
+
dt['api/data'+model.name+getObjectHash({ids})] = { count: data.length, data };
|
|
58
|
+
setRelations(dt);
|
|
59
|
+
} }
|
|
60
|
+
);
|
|
61
|
+
const [intVal, setIntVal] = useState({});
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (relatedData?.length > 0) {
|
|
65
|
+
updateIds();
|
|
66
|
+
}
|
|
67
|
+
}, [relatedData]);
|
|
68
|
+
|
|
69
|
+
const updateIds = () => {
|
|
70
|
+
let dt = [];
|
|
71
|
+
if (!model) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setRelationIds(rels => {
|
|
75
|
+
const relationIds = { ...rels };
|
|
76
|
+
let hasChanged = false;
|
|
77
|
+
model.fields.forEach(f => {
|
|
78
|
+
if (f.type === 'relation') {
|
|
79
|
+
|
|
80
|
+
if (!datasToLoad.includes(f.relation) && !dt.includes(f.relation))
|
|
81
|
+
dt.push(f.relation);
|
|
82
|
+
const vals = Array.isArray(value) || value === null ? value : [value];
|
|
83
|
+
|
|
84
|
+
if (!vals)
|
|
85
|
+
return;
|
|
86
|
+
vals.forEach((val) => {
|
|
87
|
+
const v = typeof (val) === 'string' ? val : (val?._id || val);
|
|
88
|
+
if( !v )
|
|
89
|
+
return;
|
|
90
|
+
if (!relationIds[f.relation])
|
|
91
|
+
relationIds[f.relation] = [];
|
|
92
|
+
if (!relationIds[f.relation].includes(v)) {
|
|
93
|
+
relationIds[f.relation].push(v);
|
|
94
|
+
hasChanged = true;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
setDatasToLoad(datasToLoad => [...datasToLoad, ...dt]);
|
|
100
|
+
if (!hasChanged)
|
|
101
|
+
return rels;
|
|
102
|
+
return relationIds;
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
const handleModal = (rel) => {
|
|
108
|
+
setIntVal(rel);
|
|
109
|
+
setPopupVisible(true);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (isLoading) return <>...</>;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<DialogProvider>
|
|
116
|
+
<div className="flex">
|
|
117
|
+
{(Array.isArray(value) ? value : [value]).map((v,vi) => {
|
|
118
|
+
const rel = relatedData?.find(f => {
|
|
119
|
+
if (typeof (v) === 'string') {
|
|
120
|
+
return f._id === v;
|
|
121
|
+
} else if (Array.isArray(v)) {
|
|
122
|
+
return f._id === v[0]._id;
|
|
123
|
+
} else {
|
|
124
|
+
return f._id === v._id;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (!rel) {
|
|
128
|
+
return <></>;
|
|
129
|
+
}
|
|
130
|
+
let displayValue ;
|
|
131
|
+
try {
|
|
132
|
+
displayValue = getDataAsString(model, rel, tr, models);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.log(e);
|
|
135
|
+
}
|
|
136
|
+
//console.log(model, rel, displayValue)
|
|
137
|
+
const bgColor = // Returns a bright color in RGB
|
|
138
|
+
randomColor({
|
|
139
|
+
seed: rel._hash+rel._id,
|
|
140
|
+
luminosity: isLightColor(field.color) ? 'light' : 'dark',
|
|
141
|
+
alpha: 0.42,
|
|
142
|
+
format: 'rgba' // e.g. 'rgb(225,200,20)'
|
|
143
|
+
});
|
|
144
|
+
return (
|
|
145
|
+
<div style={{backgroundColor: bgColor, color: isLightColor(field.color) ? 'black' : '#eeeded'}} className={`relation-value flex flex-border ${align === 'left' ? '' : 'flex-centered'}`} key={rel._id}>
|
|
146
|
+
{popupVisible && (
|
|
147
|
+
<Dialog onClose={() => setPopupVisible(false)} isClosable={true} isModal={true} className="dialog-relation">
|
|
148
|
+
<dl>
|
|
149
|
+
{model.fields.map(f => {
|
|
150
|
+
if (intVal[f.name] === undefined || intVal[f.name] === null || intVal[f.name] === '')
|
|
151
|
+
return <></>;
|
|
152
|
+
let columnName = t(`field_${model.name}_${f.name}`, f.name);
|
|
153
|
+
let span = columnName !== f.name ? <span style={{ fontWeight: 'lighter' }}>({f.name})</span> : "";
|
|
154
|
+
if (f.type === 'richtext') {
|
|
155
|
+
return <><dt>{columnName} {span}</dt><dd><div dangerouslySetInnerHTML={{ __html: intVal[f.name] }}></div></dd></>;
|
|
156
|
+
}
|
|
157
|
+
if(f.type === 'datetime'){
|
|
158
|
+
return <><dt>{columnName} {span}</dt><dd><>{new Date(intVal[f.name]).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'})}</></dd></>
|
|
159
|
+
}
|
|
160
|
+
if(f.type === 'date'){
|
|
161
|
+
return <><dt>{columnName} {span}</dt><dd><>{new Date(intVal[f.name]).toLocaleDateString(lang, {year: 'numeric', month: 'numeric', day: 'numeric'})}</></dd></>
|
|
162
|
+
}
|
|
163
|
+
if (f.type === 'boolean') {
|
|
164
|
+
return <><dt>{columnName} {span}</dt><dd>{intVal[f.name] === true ? t('yes') : t('no')}</dd></>;
|
|
165
|
+
}
|
|
166
|
+
if (f.type === 'relation') {
|
|
167
|
+
return <><dt>{columnName} {span}</dt><dd><><RelationValue align='left' field={f} data={intVal || {}} /></></dd></>;
|
|
168
|
+
}
|
|
169
|
+
if (f.type === 'file') {
|
|
170
|
+
const value = intVal[f.name];
|
|
171
|
+
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))
|
|
172
|
+
return <><dt>{columnName} {span}</dt><dd><a
|
|
173
|
+
href={`/resources/${value.guid}`} target="_blank"
|
|
174
|
+
rel="noopener noreferrer"><img
|
|
175
|
+
src={`/resources/${value.guid}`}
|
|
176
|
+
alt={`${value.name} (${value.guid})`}/></a></dd></>;
|
|
177
|
+
return <><dt>{columnName} {span}</dt><dd><a style={{color: field.color}}
|
|
178
|
+
href={`/resources/${value.guid}`}
|
|
179
|
+
target="_blank"
|
|
180
|
+
rel="noopener noreferrer">{value.name} ({value.guid})</a>;
|
|
181
|
+
</dd></>;
|
|
182
|
+
}
|
|
183
|
+
if (f.type === 'color') {
|
|
184
|
+
return <><dt>{columnName} {span}</dt><dd>
|
|
185
|
+
<ColorField disabled={true} value={intVal} name={f.name} />
|
|
186
|
+
</dd></>;
|
|
187
|
+
}
|
|
188
|
+
if (f.type === 'code') {
|
|
189
|
+
let t;
|
|
190
|
+
try {
|
|
191
|
+
t = f.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : '';
|
|
192
|
+
|
|
193
|
+
} catch (e) {
|
|
194
|
+
t = intVal[f.name].toString();
|
|
195
|
+
}
|
|
196
|
+
return <>
|
|
197
|
+
<dt>{columnName} {span}</dt>
|
|
198
|
+
<dd>
|
|
199
|
+
<CodeField onChange={() => {
|
|
200
|
+
}} language={field.language || 'json'}
|
|
201
|
+
name={f.name} value={field.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : t} />
|
|
202
|
+
</dd>
|
|
203
|
+
</>;
|
|
204
|
+
}
|
|
205
|
+
if (f.type === 'password') {
|
|
206
|
+
return <></>;
|
|
207
|
+
}
|
|
208
|
+
if (f.type === 'string_t') {
|
|
209
|
+
return <>
|
|
210
|
+
<dt>{columnName} {span}</dt>
|
|
211
|
+
<dd>{intVal[f.name]?.value}</dd>
|
|
212
|
+
</>;
|
|
213
|
+
}
|
|
214
|
+
return <>
|
|
215
|
+
<dt>{columnName} {span}</dt>
|
|
216
|
+
<dd>{intVal[f.name]}</dd>
|
|
217
|
+
</>;
|
|
218
|
+
})}
|
|
219
|
+
</dl>
|
|
220
|
+
</Dialog>
|
|
221
|
+
)}
|
|
222
|
+
{rel && <div className="flex flex-no-wrap flex-mini-gap" onClick={() => handleModal(rel)}>{displayValue}<button><FaMagnifyingGlass /></button></div>}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
})}
|
|
226
|
+
</div></DialogProvider>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export default RelationValue;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// client/src/RestoreConfirmationModal.jsx
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
4
|
+
import { Dialog } from "./Dialog.jsx";
|
|
5
|
+
import Button from "./Button.jsx";
|
|
6
|
+
import { TextField } from "./Field.jsx"; // Importer TextField
|
|
7
|
+
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
8
|
+
import { useNotificationContext } from "./NotificationProvider.jsx";
|
|
9
|
+
import {FaAws, FaInfo} from "react-icons/fa";
|
|
10
|
+
import {NavLink} from "react-router"; // Pour une icône S3
|
|
11
|
+
|
|
12
|
+
// Ajouter des props pour la configuration S3 et sa gestion
|
|
13
|
+
const RestoreConfirmationModal = ({ isOpen, onClose, onConfirm, showS3Config = false }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { me, fetchMe } = useAuthContext(); // fetchMe pour recharger les données utilisateur après sauvegarde
|
|
16
|
+
const { addNotification } = useNotificationContext();
|
|
17
|
+
|
|
18
|
+
// États pour les champs de configuration S3
|
|
19
|
+
const [s3Config, setS3Config] = useState({
|
|
20
|
+
bucketName: '',
|
|
21
|
+
accessKeyId: '',
|
|
22
|
+
secretAccessKey: '', // Ne sera pas affiché directement mais envoyé
|
|
23
|
+
region: ''
|
|
24
|
+
});
|
|
25
|
+
const [isSavingS3Config, setIsSavingS3Config] = useState(false);
|
|
26
|
+
const [showConfig, setConfigVisible] = useState(false);
|
|
27
|
+
|
|
28
|
+
// Charger la configuration S3 existante de l'utilisateur au montage si me.s3Config existe
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (me?.s3Config && showS3Config) {
|
|
31
|
+
setS3Config({
|
|
32
|
+
bucketName: me.s3Config.bucketName || '',
|
|
33
|
+
accessKeyId: me.s3Config.accessKeyId || '',
|
|
34
|
+
secretAccessKey: '', // Ne pas pré-remplir la clé secrète pour la sécurité
|
|
35
|
+
region: me.s3Config.region || '',
|
|
36
|
+
pathPrefix: me.s3Config.pathPrefix || ''
|
|
37
|
+
});
|
|
38
|
+
} else if (showS3Config) {
|
|
39
|
+
// Réinitialiser si pas de config ou si on quitte la section S3
|
|
40
|
+
setS3Config({ bucketName: '', accessKeyId: '', secretAccessKey: '', region: '', pathPrefix: '' });
|
|
41
|
+
}
|
|
42
|
+
}, [me, showS3Config, isOpen]); // Ajouter isOpen pour recharger si la modale est rouverte
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const handleS3ConfigChange = (e) => {
|
|
46
|
+
const { name, value } = e.target;
|
|
47
|
+
setS3Config(prev => ({ ...prev, [name]: value }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleSaveS3Config = async () => {
|
|
51
|
+
setIsSavingS3Config(true);
|
|
52
|
+
// Validation basique côté client
|
|
53
|
+
if (!s3Config.bucketName || !s3Config.accessKeyId || !s3Config.region) {
|
|
54
|
+
addNotification({ type: 'error', message: t('backup.s3config.validationError', 'Le nom du bucket, l\'Access Key ID et la Région sont requis.') });
|
|
55
|
+
setIsSavingS3Config(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const payload = { ...s3Config };
|
|
61
|
+
// N'envoyer la clé secrète que si elle a été modifiée
|
|
62
|
+
if (!payload.secretAccessKey) {
|
|
63
|
+
delete payload.secretAccessKey;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetch('/api/user/s3-config', { // Endpoint pour sauvegarder la config S3
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
// Les en-têtes d'authentification (_user, Authorization) sont gérés globalement par le fetch wrapper si tu en as un, sinon ajoute-les ici
|
|
71
|
+
// Exemple:
|
|
72
|
+
// '_user': me?.username,
|
|
73
|
+
// 'Authorization': `Bearer ${me?.token}`,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
});
|
|
77
|
+
const result = await response.json();
|
|
78
|
+
if (response.ok && result.success) {
|
|
79
|
+
addNotification({ status: 'completed', title: t('backup.s3config.saveSuccess', 'Configuration S3 enregistrée avec succès.') });
|
|
80
|
+
} else {
|
|
81
|
+
addNotification({ status: 'error', title: result.error || t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
addNotification({ status: 'error', title: t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
85
|
+
console.error("Error saving S3 config:", error);
|
|
86
|
+
} finally {
|
|
87
|
+
setIsSavingS3Config(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if (!isOpen) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Dialog isModal={true} isClosable={true} onClose={onClose}>
|
|
98
|
+
<div className="restore-confirmation-modal p-4"> {/* Ajout de padding pour l'esthétique */}
|
|
99
|
+
<h2>{t('backup.restore.title', "Envoi de votre lien de restauration")}</h2>
|
|
100
|
+
|
|
101
|
+
<>
|
|
102
|
+
<p><Trans i18nKey="backup.restore.confirm">Un lien pour restaurer votre sauvegarde vous sera envoyé
|
|
103
|
+
à votre adresse e-mail. Ce lien expirera dans 30 minutes. Voulez-vous vraiment
|
|
104
|
+
continuer?</Trans></p>
|
|
105
|
+
<div className="modal-actions flex justify-end space-x-2 mt-4">
|
|
106
|
+
<Button onClick={onConfirm} className="btn-primary">
|
|
107
|
+
<Trans i18nKey="btns.confirm">Confirmer</Trans>
|
|
108
|
+
</Button>
|
|
109
|
+
<Button onClick={onClose} className="btn-secondary">
|
|
110
|
+
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
<div className={"flex flex-centered actions"}>
|
|
114
|
+
{!showConfig && (<NavLink onClick={() => setConfigVisible(true)}><Trans i18nKey={"backup.s3config.title"}></Trans></NavLink>)}
|
|
115
|
+
</div>
|
|
116
|
+
</>
|
|
117
|
+
|
|
118
|
+
{showConfig && (<>
|
|
119
|
+
<h2>{t('backup.s3config.title', 'Configuration du stockage S3')}</h2>
|
|
120
|
+
<p><Trans i18nKey="backup.prez"></Trans></p>
|
|
121
|
+
<form className="s3-config-form space-y-4"> {/* Ajout de space-y pour l'espacement vertical */}
|
|
122
|
+
<TextField
|
|
123
|
+
name="bucketName"
|
|
124
|
+
className={"flex flex-1"}
|
|
125
|
+
label={t('backup.s3config.bucketName', 'Nom du Bucket S3')}
|
|
126
|
+
value={s3Config.bucketName}
|
|
127
|
+
onChange={handleS3ConfigChange}
|
|
128
|
+
placeholder="my-bucket-name"
|
|
129
|
+
required
|
|
130
|
+
/>
|
|
131
|
+
<TextField
|
|
132
|
+
name="accessKeyId"
|
|
133
|
+
className={"flex flex-1"}
|
|
134
|
+
label={t('backup.s3config.accessKeyId', 'Access Key ID AWS')}
|
|
135
|
+
value={s3Config.accessKeyId}
|
|
136
|
+
onChange={handleS3ConfigChange}
|
|
137
|
+
placeholder="AKIAIOSFODNN7EXAMPLE"
|
|
138
|
+
required
|
|
139
|
+
/>
|
|
140
|
+
<TextField
|
|
141
|
+
name="secretAccessKey"
|
|
142
|
+
className={"flex flex-1"}
|
|
143
|
+
label={t('backup.s3config.secretAccessKey', 'Secret Access Key AWS')}
|
|
144
|
+
type="password" // Important pour masquer la clé
|
|
145
|
+
value={s3Config.secretAccessKey}
|
|
146
|
+
onChange={handleS3ConfigChange}
|
|
147
|
+
placeholder={t('backup.s3config.secretPlaceholder', 'Laisser vide pour ne pas modifier')} />
|
|
148
|
+
<TextField
|
|
149
|
+
name="region"
|
|
150
|
+
className={"flex flex-1"}
|
|
151
|
+
label={t('backup.s3config.region', 'Région AWS')}
|
|
152
|
+
value={s3Config.region}
|
|
153
|
+
onChange={handleS3ConfigChange}
|
|
154
|
+
placeholder="eu-west-3"
|
|
155
|
+
required
|
|
156
|
+
/>
|
|
157
|
+
<TextField
|
|
158
|
+
name="pathPrefix"
|
|
159
|
+
label={t('backup.s3config.pathPrefix', 'Préfixe de chemin (Optionnel)')}
|
|
160
|
+
value={s3Config.pathPrefix}
|
|
161
|
+
onChange={handleS3ConfigChange}
|
|
162
|
+
placeholder="saves/my-app/"
|
|
163
|
+
help={t('backup.s3config.pathPrefixHelp', 'Ex: "mon-dossier/". Laissez vide pour la racine du bucket.')}
|
|
164
|
+
/>
|
|
165
|
+
<div
|
|
166
|
+
className="modal-actions flex justify-end space-x-2 mt-4"> {/* Flex pour aligner les boutons */}
|
|
167
|
+
<Button onClick={handleSaveS3Config} disabled={isSavingS3Config}
|
|
168
|
+
className="btn-primary"> {/* Classe btn-primary pour le bouton principal */}
|
|
169
|
+
{isSavingS3Config ? t('btns.saving', 'Enregistrement...') : t('btns.save', 'Enregistrer la configuration S3')}
|
|
170
|
+
</Button>
|
|
171
|
+
<Button onClick={onClose} className="btn-secondary">
|
|
172
|
+
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</form>
|
|
176
|
+
</>)}
|
|
177
|
+
|
|
178
|
+
</div>
|
|
179
|
+
</Dialog>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default RestoreConfirmationModal;
|