data-primals-engine 1.3.1 → 1.3.3-rc.1
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/Dockerfile +19 -9
- package/DockerfileTest +19 -9
- package/README.md +782 -782
- package/client/package-lock.json +190 -19
- package/client/package.json +6 -5
- package/client/src/CalendarConfigModal.jsx +60 -21
- package/client/src/CalendarView.jsx +107 -0
- package/client/src/CalendarView.scss +9 -0
- package/client/src/DataLayout.jsx +52 -25
- package/client/src/Dialog.scss +4 -3
- package/client/src/HistoryDialog.jsx +252 -95
- package/client/src/HistoryDialog.scss +319 -72
- package/client/src/ViewSwitcher.jsx +6 -6
- package/client/src/translations.js +192 -49
- package/package.json +2 -2
- package/src/modules/data/data.history.js +489 -303
- package/src/modules/data/data.js +4632 -4620
- package/src/HistoryDialog.jsx +0 -86
- package/src/HistoryDialog.scss +0 -77
|
@@ -31,16 +31,9 @@ import ViewSwitcher from "./ViewSwitcher.jsx";
|
|
|
31
31
|
import KanbanConfigModal from "./KanbanConfigModal.jsx";
|
|
32
32
|
import CalendarConfigModal from "./CalendarConfigModal.jsx";
|
|
33
33
|
import KanbanView from "./KanbanView.jsx";
|
|
34
|
+
import CalendarView from "./CalendarView.jsx";
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
const CalendarView = ({ settings, model }) => (
|
|
37
|
-
<div className="p-4 border rounded-md mt-4 bg-gray-50">
|
|
38
|
-
<h3 className="font-bold">Vue Calendrier</h3>
|
|
39
|
-
<p>Affichage des données pour le modèle <strong>{model?.name}</strong> en utilisant le champ de date : <strong>{settings.dateField}</strong>.</p>
|
|
40
|
-
{/* L'implémentation réelle du calendrier irait ici */}
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
|
|
44
37
|
const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
|
|
45
38
|
<div className="p-4 border border-dashed rounded-md mt-4 text-center bg-gray-50">
|
|
46
39
|
<h4><Trans i18nKey="dataview.notConfiguredTitle" values={{ type }}>Vue {{type}} non configurée</Trans></h4>
|
|
@@ -52,9 +45,6 @@ const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
|
|
|
52
45
|
);
|
|
53
46
|
|
|
54
47
|
function DataLayout() {
|
|
55
|
-
const [currentView, setCurrentView] = useState('table');
|
|
56
|
-
|
|
57
|
-
// Nouvelle version (persistante)
|
|
58
48
|
const [viewSettings, setViewSettings] = useLocalStorage('viewSettings', {});
|
|
59
49
|
|
|
60
50
|
const [isCalendarModalOpen, setCalendarModalOpen] = useState(false);
|
|
@@ -64,6 +54,9 @@ function DataLayout() {
|
|
|
64
54
|
const { t, i18n } = useTranslation();
|
|
65
55
|
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
66
56
|
|
|
57
|
+
// Stocke la vue sélectionnée pour chaque modèle. Ex: { "contacts": "calendar", "tasks": "kanban" }
|
|
58
|
+
const [viewsByModel, setViewsByModel] = useLocalStorage('dataLayout_viewsByModel', {});
|
|
59
|
+
|
|
67
60
|
const [filterValues, setFilterValues] = useState({});
|
|
68
61
|
const { dataByModel,paginatedDataByModel,
|
|
69
62
|
setRelationFilters, setSelectedModel, selectedModel,
|
|
@@ -94,6 +87,20 @@ function DataLayout() {
|
|
|
94
87
|
const [showDataEditor, setDataEditorVisible] = useState(false);
|
|
95
88
|
const [showAPIInfo, setAPIInfoVisible] = useState(false);
|
|
96
89
|
|
|
90
|
+
// La vue courante est dérivée du modèle sélectionné et des préférences stockées.
|
|
91
|
+
const currentView = useMemo(() => {
|
|
92
|
+
if (!selectedModel) return 'table';
|
|
93
|
+
return viewsByModel[selectedModel.name] || 'table';
|
|
94
|
+
}, [selectedModel, viewsByModel]);
|
|
95
|
+
|
|
96
|
+
// Met à jour la vue pour le modèle actuellement sélectionné.
|
|
97
|
+
const setCurrentView = (viewName) => {
|
|
98
|
+
if (!selectedModel) return;
|
|
99
|
+
setViewsByModel(prev => ({
|
|
100
|
+
...prev,
|
|
101
|
+
[selectedModel.name]: viewName,
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
97
104
|
|
|
98
105
|
// --- MODIFICATION : Logique de changement de vue mise à jour ---
|
|
99
106
|
const handleSwitchView = (viewName) => {
|
|
@@ -110,7 +117,7 @@ function DataLayout() {
|
|
|
110
117
|
const modelSettings = viewSettings[selectedModel.name] || {};
|
|
111
118
|
|
|
112
119
|
if (viewName === 'calendar') {
|
|
113
|
-
if (modelSettings.calendar?.
|
|
120
|
+
if (modelSettings.calendar?.titleField && modelSettings.calendar?.startField && modelSettings.calendar?.endField) {
|
|
114
121
|
setCurrentView('calendar');
|
|
115
122
|
} else {
|
|
116
123
|
setCalendarModalOpen(true);
|
|
@@ -127,15 +134,26 @@ function DataLayout() {
|
|
|
127
134
|
// --- MODIFICATION : Sauvegarde dans localStorage ---
|
|
128
135
|
const handleSaveCalendarConfig = (config) => {
|
|
129
136
|
if (!selectedModel) return;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
const isConfigValid = config && config.titleField && config.startField && config.endField;
|
|
138
|
+
|
|
139
|
+
if (isConfigValid) {
|
|
140
|
+
setViewSettings(prev => ({
|
|
141
|
+
...prev,
|
|
142
|
+
[selectedModel.name]: {
|
|
143
|
+
...(prev[selectedModel.name] || {}),
|
|
144
|
+
calendar: config,
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
setCalendarModalOpen(false);
|
|
148
|
+
setCurrentView('calendar');
|
|
149
|
+
} else {
|
|
150
|
+
// Si la configuration n'est pas valide, afficher une notification et garder la modale ouverte
|
|
151
|
+
addNotification({
|
|
152
|
+
title: t('datalayout.invalidConfigTitle', 'Configuration invalide'),
|
|
153
|
+
message: t('datalayout.invalidConfigMessage', 'Veuillez vous assurer que les champs pour le titre, la date de début et la date de fin sont tous sélectionnés.'),
|
|
154
|
+
status: 'error'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
139
157
|
};
|
|
140
158
|
|
|
141
159
|
const handleSaveKanbanConfig = (config) => {
|
|
@@ -162,7 +180,7 @@ function DataLayout() {
|
|
|
162
180
|
if (!selectedModel) return { calendar: false, kanban: false };
|
|
163
181
|
const modelSettings = viewSettings[selectedModel.name] || {};
|
|
164
182
|
return {
|
|
165
|
-
calendar: !!modelSettings.calendar?.
|
|
183
|
+
calendar: !!modelSettings.calendar?.titleField && !!modelSettings.calendar?.startField && !!modelSettings.calendar?.endField,
|
|
166
184
|
kanban: !!modelSettings.kanban?.groupByField,
|
|
167
185
|
};
|
|
168
186
|
}, [viewSettings, selectedModel]);
|
|
@@ -174,7 +192,7 @@ function DataLayout() {
|
|
|
174
192
|
switch (currentView) {
|
|
175
193
|
case 'calendar':
|
|
176
194
|
return configuredViews.calendar
|
|
177
|
-
? <CalendarView settings={currentModelViewSettings.calendar} model={selectedModel} />
|
|
195
|
+
? <CalendarView settings={currentModelViewSettings.calendar} onEditData={(model, data) => handleAddData(model,data)} model={selectedModel} />
|
|
178
196
|
: <NotConfiguredPlaceholder type="calendar" onConfigure={() => setCalendarModalOpen(true)} />;
|
|
179
197
|
case 'kanban':
|
|
180
198
|
return configuredViews.kanban
|
|
@@ -217,13 +235,20 @@ function DataLayout() {
|
|
|
217
235
|
|
|
218
236
|
const handleModelSelect = (model) => {
|
|
219
237
|
setRelationFilters({});
|
|
220
|
-
setCurrentView('table');
|
|
221
238
|
setCheckedItems([])
|
|
222
239
|
setFilterValues({});
|
|
223
240
|
if (!model) {
|
|
241
|
+
setSelectedModel(null);
|
|
224
242
|
return;
|
|
225
243
|
}
|
|
226
244
|
|
|
245
|
+
// Maintient la vue actuelle si elle est configurée pour le nouveau modèle, sinon revient à la vue "table"
|
|
246
|
+
const modelSettings = viewSettings[model.name] || {};
|
|
247
|
+
if (currentView === 'calendar' && (!modelSettings.calendar?.titleField || !modelSettings.calendar?.startField || !modelSettings.calendar?.endField)) {
|
|
248
|
+
setCurrentView('table');
|
|
249
|
+
} else if (currentView === 'kanban' && !modelSettings.kanban?.groupByField) {
|
|
250
|
+
setCurrentView('table');
|
|
251
|
+
}
|
|
227
252
|
const dt = [];
|
|
228
253
|
const t = [...model.fields].reduce((acc, field, index) => {
|
|
229
254
|
if (field.type === "relation") {
|
|
@@ -464,7 +489,9 @@ function DataLayout() {
|
|
|
464
489
|
|
|
465
490
|
const handleAddData = (model, data)=>{
|
|
466
491
|
|
|
467
|
-
|
|
492
|
+
if (!selectedModel || model.name !== selectedModel.name) {
|
|
493
|
+
handleModelSelect(model);
|
|
494
|
+
}
|
|
468
495
|
|
|
469
496
|
if( data ){
|
|
470
497
|
setFormData(data);
|
package/client/src/Dialog.scss
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
#content .dialog.dialog-relation {
|
|
45
45
|
max-width: 480px;
|
|
46
46
|
}
|
|
47
|
-
.dialog-header {
|
|
47
|
+
#content .dialog-header {
|
|
48
48
|
display: flex;
|
|
49
49
|
gap: 8px;
|
|
50
50
|
justify-content: flex-start;
|
|
@@ -53,8 +53,9 @@
|
|
|
53
53
|
h2 {
|
|
54
54
|
display: flex;
|
|
55
55
|
flex: 1;
|
|
56
|
-
padding:
|
|
57
|
-
|
|
56
|
+
padding: 0;
|
|
57
|
+
align-items: center;
|
|
58
|
+
margin: 8px;
|
|
58
59
|
font-size: 100%;
|
|
59
60
|
line-height: 208%;
|
|
60
61
|
}
|
|
@@ -1,96 +1,253 @@
|
|
|
1
|
-
import { useEffect, useState
|
|
2
|
-
import { Dialog } from "./Dialog.jsx";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import "
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1
|
+
import {useCallback, useEffect, useState} from "react";
|
|
2
|
+
import { Dialog } from "./Dialog.jsx";
|
|
3
|
+
import Button from "./Button.jsx";
|
|
4
|
+
import { FaHistory } from "react-icons/fa";
|
|
5
|
+
import {Trans, useTranslation} from "react-i18next";
|
|
6
|
+
import "./HistoryDialog.scss";
|
|
7
|
+
import {Tooltip} from "react-tooltip";
|
|
8
|
+
import {CodeField} from "./Field.jsx";
|
|
9
|
+
|
|
10
|
+
// Helper to render values in a friendly way for the summary list
|
|
11
|
+
const renderSummaryValue = (value) => {
|
|
12
|
+
if (value === undefined || value === null) {
|
|
13
|
+
return <i className="value-empty">empty</i>;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'boolean') {
|
|
16
|
+
return <code className="value-boolean">{value.toString()}</code>;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return <code className="value-object" title={JSON.stringify(value, null, 2)}>[...] ({value.length} items)</code>
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'object' && value !== null) {
|
|
22
|
+
return <code className="value-object" title={JSON.stringify(value, null, 2)}>{`{...}`}</code>
|
|
23
|
+
}
|
|
24
|
+
const strValue = String(value);
|
|
25
|
+
if (strValue.length > 50) {
|
|
26
|
+
return <span className="value-text" title={strValue}>{strValue.substring(0, 47) + '...'}</span>;
|
|
27
|
+
}
|
|
28
|
+
return <span className="value-text">{strValue}</span>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// New sub-component for rendering a single field change
|
|
32
|
+
const ChangeDetail = ({ field, from, to }) => {
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
|
|
35
|
+
const tt = typeof(to) === 'object' ? JSON.stringify(to,null,2):to;
|
|
36
|
+
const f = typeof(from) === 'object' ? JSON.stringify(from,null,2):from;
|
|
37
|
+
return (
|
|
38
|
+
<div className="change-detail">
|
|
39
|
+
<strong className="change-field-name" title={field}>{field}</strong>
|
|
40
|
+
<div className="change-values">
|
|
41
|
+
<div className="value-from" data-tooltip-id={"changeTooltip"} data-tooltip-content={f}>{renderSummaryValue(from)}</div>
|
|
42
|
+
<div className="change-arrow">→</div>
|
|
43
|
+
<div className="value-to" data-tooltip-id={"changeTooltip"} data-tooltip-content={tt}>{renderSummaryValue(to)}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Sub-component for displaying a single history entry
|
|
50
|
+
const HistoryEntry = ({ entry, onPreview, isSelected }) => {
|
|
51
|
+
const { t } = useTranslation();
|
|
52
|
+
// The backend has transformed the data for us.
|
|
53
|
+
// _id: history doc id, _v: version, _op: operation type (i,u,d), _rid: original record id
|
|
54
|
+
const { _id, _v, _op, _updatedAt, _user, ...data } = entry; // ...data collects the remaining fields
|
|
55
|
+
|
|
56
|
+
const operationDetails = {
|
|
57
|
+
// The keys 'i', 'u', 'd' correspond to what the backend sends now.
|
|
58
|
+
'i': { label: t('history.op.create', 'Creation'), className: 'op-insert' },
|
|
59
|
+
'u': { label: t('history.op.update', 'Update'), className: 'op-update' },
|
|
60
|
+
'd': { label: t('history.op.delete', 'Deletion'), className: 'op-delete' },
|
|
61
|
+
};
|
|
62
|
+
const opInfo = operationDetails[_op] || { label: _op, className: '' };
|
|
63
|
+
|
|
64
|
+
const renderData = () => {
|
|
65
|
+
if (_op === 'u') {
|
|
66
|
+
const changes = Object.entries(data).filter(([, value]) => value && typeof value === 'object' && 'from' in value && 'to' in value);
|
|
67
|
+
if (changes.length === 0) {
|
|
68
|
+
return <div className="no-changes-text">{t('history.noChanges', 'No relevant changes recorded.')}</div>;
|
|
69
|
+
}
|
|
70
|
+
return (
|
|
71
|
+
<div className="changes-list">
|
|
72
|
+
{changes.map(([field, change]) => (
|
|
73
|
+
<ChangeDetail key={field} field={field} from={change.from} to={change.to} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For 'i' (create) and 'd' (delete), show a human-friendly summary of the snapshot.
|
|
80
|
+
const snapshotEntries = Object.entries(data);
|
|
81
|
+
if (snapshotEntries.length === 0) {
|
|
82
|
+
return <div className="no-changes-text">{t('history.noData', 'No data in snapshot.')}</div>;
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<div className="snapshot-list">
|
|
86
|
+
{snapshotEntries.map(([key, value]) => (
|
|
87
|
+
<div className="snapshot-item" key={key}>
|
|
88
|
+
<strong className="snapshot-key" title={key}>{key}:</strong>
|
|
89
|
+
<span className="snapshot-value">{renderSummaryValue(value)}</span>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={`history-entry ${isSelected ? 'selected' : ''}`}>
|
|
98
|
+
<div className="history-entry-header">
|
|
99
|
+
<span className={`op-badge ${opInfo.className}`}>{opInfo.label}</span>
|
|
100
|
+
<span className="history-date">{t('history.datePrefix', 'On')} {new Date(_updatedAt).toLocaleString()}</span>
|
|
101
|
+
{_user && <span className="history-user">{t('history.byUser', 'by')} <strong>{_user}</strong></span>}
|
|
102
|
+
<span className="history-version">{t('history.version', 'Version:')} {_v}</span>
|
|
103
|
+
<Button className="btn-preview" onClick={() => onPreview(_v)}>
|
|
104
|
+
<Trans i18nKey={"history.previewAction"}>Preview</Trans>
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="history-entry-data">
|
|
108
|
+
{renderData()}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
115
|
+
const [history, setHistory] = useState([]);
|
|
116
|
+
const [loading, setLoading] = useState(true);
|
|
117
|
+
const [error, setError] = useState(null);
|
|
118
|
+
const [previewData, setPreviewData] = useState(null);
|
|
119
|
+
const [previewLoading, setPreviewLoading] = useState(false);
|
|
120
|
+
const [selectedVersion, setSelectedVersion] = useState(null);
|
|
121
|
+
const { t, i18n } = useTranslation();
|
|
122
|
+
|
|
123
|
+
const fetchHistory = useCallback(async () => {
|
|
124
|
+
if (!modelName || !recordId) return;
|
|
125
|
+
setLoading(true);
|
|
126
|
+
setError(null);
|
|
127
|
+
try {
|
|
128
|
+
const query = await fetch(`/api/data/history/${modelName}/${recordId}`);
|
|
129
|
+
const response = await query.json();
|
|
130
|
+
if (response.success) {
|
|
131
|
+
setHistory(response.data);
|
|
132
|
+
} else {
|
|
133
|
+
setError(response.error || i18n.t("history.error", "Could not load history."));
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
setError(i18n.t("history.error", "Could not load history."));
|
|
137
|
+
console.error(err);
|
|
138
|
+
} finally {
|
|
139
|
+
setLoading(false);
|
|
140
|
+
}
|
|
141
|
+
}, [modelName, recordId, i18n]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
fetchHistory();
|
|
145
|
+
}, [fetchHistory]);
|
|
146
|
+
|
|
147
|
+
const handlePreview = async (version) => {
|
|
148
|
+
if (selectedVersion === version) return; // Already selected
|
|
149
|
+
|
|
150
|
+
setSelectedVersion(version);
|
|
151
|
+
setPreviewLoading(true);
|
|
152
|
+
setPreviewData(null);
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(`/api/data/history/${modelName}/${recordId}/${version}`);
|
|
155
|
+
const response = await res.json();
|
|
156
|
+
if (response.success) {
|
|
157
|
+
setPreviewData(response.data);
|
|
158
|
+
} else {
|
|
159
|
+
setPreviewData({ error: response.error || i18n.t("history.previewError", "Could not load revision preview.") });
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
setPreviewData({ error: i18n.t("history.previewError", "Could not load revision preview.") });
|
|
163
|
+
console.error(err);
|
|
164
|
+
} finally {
|
|
165
|
+
setPreviewLoading(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleRevert = async (version) => {
|
|
170
|
+
if (!window.confirm(t('history.confirmRevert', `Are you sure you want to revert to version ${version}? This will create a new version with the content of this revision.`))) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setPreviewLoading(true); // Reuse preview loading state to indicate activity
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch(`/api/data/history/${modelName}/${recordId}/revert/${version}`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' }
|
|
179
|
+
});
|
|
180
|
+
const response = await res.json();
|
|
181
|
+
|
|
182
|
+
if (response.success) {
|
|
183
|
+
alert(t('history.revertSuccess', 'Document successfully reverted. The history will now be updated.'));
|
|
184
|
+
// Refresh history list and clear preview
|
|
185
|
+
await fetchHistory();
|
|
186
|
+
setPreviewData(null);
|
|
187
|
+
setSelectedVersion(null);
|
|
188
|
+
} else {
|
|
189
|
+
alert(t('history.revertError', `Error reverting: ${response.error || 'Unknown error'}`));
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
alert(t('history.revertError', 'An error occurred while reverting.'));
|
|
193
|
+
console.error(err);
|
|
194
|
+
} finally {
|
|
195
|
+
setPreviewLoading(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<Dialog
|
|
201
|
+
title={<><FaHistory style={{ marginRight: '8px' }} /> <Trans i18nKey={"history.title"}>Historique de l'enregistrement</Trans></>}
|
|
202
|
+
onClose={onClose}
|
|
203
|
+
isClosable={true}
|
|
204
|
+
className="history-dialog"
|
|
205
|
+
>
|
|
206
|
+
<Tooltip id={"changeTooltip"} clickable={true} />
|
|
207
|
+
<div className="history-dialog-content-split">
|
|
208
|
+
<div className="history-list-container">
|
|
209
|
+
{loading && <div><Trans i18nKey={"history.loading"}>Chargement de l'historique..</Trans></div>}
|
|
210
|
+
{error && <div className="error-message">{error}</div>}
|
|
211
|
+
{!loading && !error && (
|
|
212
|
+
<div className="history-list">
|
|
213
|
+
{history.length > 0 ? (
|
|
214
|
+
history.map(entry =>
|
|
215
|
+
<HistoryEntry
|
|
216
|
+
key={entry._id}
|
|
217
|
+
entry={entry}
|
|
218
|
+
onPreview={handlePreview}
|
|
219
|
+
isSelected={selectedVersion === entry._v}
|
|
220
|
+
/>)
|
|
221
|
+
) : (
|
|
222
|
+
<div><Trans i18nKey={"history.noHistory"}>Aucun historique trouvé pour cet enregistrement.</Trans></div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
<div className="history-preview-container">
|
|
228
|
+
<div className="preview-header">
|
|
229
|
+
<h4><Trans i18nKey={"history.previewTitle"}>Revision Preview</Trans></h4>
|
|
230
|
+
{previewData && !previewData.error && (
|
|
231
|
+
<Button
|
|
232
|
+
className="btn-revert"
|
|
233
|
+
onClick={() => handleRevert(selectedVersion)}
|
|
234
|
+
disabled={previewLoading}
|
|
235
|
+
title={t('history.revertTooltip', 'Revert the document to this state. This creates a new version.')}
|
|
236
|
+
>
|
|
237
|
+
<Trans i18nKey={"history.revertAction"}>Revert to this version</Trans>
|
|
238
|
+
</Button>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div className="history-preview-area">
|
|
243
|
+
{previewLoading && <div><Trans i18nKey={"history.loadingPreview"}>Loading preview...</Trans></div>}
|
|
244
|
+
{!previewLoading && previewData && <CodeField value={JSON.stringify(previewData,null, 2)} language={"json"} disabled={true} />}
|
|
245
|
+
{!previewLoading && !previewData && (
|
|
246
|
+
<div className="placeholder"><Trans i18nKey={"history.selectRevision"}>Select a revision to preview its full state.</Trans></div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</Dialog>
|
|
252
|
+
);
|
|
96
253
|
};
|