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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +5390 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. 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;