data-primals-engine 1.3.0 → 1.3.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.
@@ -1294,6 +1294,10 @@ select[multiple]{
1294
1294
 
1295
1295
  .model-creator {
1296
1296
  .field-checkbox {
1297
+ padding: 8px;
1298
+ display: flex;
1299
+ gap: 8px;
1300
+ align-items: flex-start;
1297
1301
  .inline {
1298
1302
  flex-direction: row-reverse;
1299
1303
  }
@@ -327,11 +327,11 @@ function DataLayout() {
327
327
  };
328
328
  addNotification(notificationData);
329
329
  },
330
- onSuccess: (data) => {
330
+ onSuccess: async (data) => {
331
331
 
332
332
  console.log('Données enregistrées:', data, selectedModel);
333
333
 
334
- Event.Trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', "custom", "data", {
334
+ await Event.Trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', "custom", "data", {
335
335
  model: selectedModel.name,
336
336
  });
337
337
 
@@ -14,7 +14,7 @@ import {
14
14
  FaDatabase, FaEraser,
15
15
  FaFileExport,
16
16
  FaFileImport,
17
- FaFilter,
17
+ FaFilter, FaHistory,
18
18
  FaInfo,
19
19
  FaPlus,
20
20
  FaTrash, FaWrench
@@ -59,6 +59,7 @@ import ConditionBuilder from "./ConditionBuilder.jsx";
59
59
  import {pagedFilterToMongoConds} from "./filter.js";
60
60
  import {isConditionMet} from "../../src/filter";
61
61
  import {DataImporter} from "./DataImporter.jsx";
62
+ import {HistoryDialog} from "./HistoryDialog.jsx";
62
63
 
63
64
  const Header = ({
64
65
  reversed = false,
@@ -238,6 +239,7 @@ export function DataTable({
238
239
  const queryClient = useQueryClient();
239
240
  const {me} = useAuthContext()
240
241
 
242
+
241
243
  const isDataLoaded = true;
242
244
  const [importVisible, setImportVisible] = useState(false);
243
245
  const [filterActive, setFilterActive] = useState(false)
@@ -248,6 +250,8 @@ export function DataTable({
248
250
  const [showExportDialog, setExportDialogVisible] = useState(false);
249
251
 
250
252
 
253
+ const [selectedRow, setSelectedRow] = useState(null);
254
+
251
255
  const [showAddPack, setAddPackVisible] = useState(false);
252
256
  const handleAddPack = () => {
253
257
  setAddPackVisible(!showAddPack);
@@ -283,7 +287,7 @@ export function DataTable({
283
287
  console.log('Données non trouvées');
284
288
  }
285
289
 
286
- Event.Trigger('API_DELETE_DATA', "custom", "data",{ model: item._model, id: item._id });
290
+ await Event.Trigger('API_DELETE_DATA', "custom", "data",{ model: item._model, id: item._id });
287
291
  queryClient.invalidateQueries(['api/data', item._model, 'page', page, elementsPerPage, pagedFilters[item._model], pagedSort[item._model]]);
288
292
 
289
293
  } else {
@@ -697,6 +701,14 @@ export function DataTable({
697
701
  >
698
702
  <FaCopy/>
699
703
  </button>
704
+ {selectedModel?.history?.enabled && (<button
705
+ onClick={() => setSelectedRow(item._id)}
706
+ data-tooltip-id="tooltipActions"
707
+ data-tooltip-content={t('btns.showHistory', 'Voir l\'historique')}
708
+ >
709
+ <FaHistory />
710
+ </button>)}
711
+
700
712
  <button data-tooltip-id="tooltipActions"
701
713
  data-tooltip-content={t('btns.delete', 'Supprimer')}
702
714
  onClick={() => handleDelete(item)}><FaTrash/></button>
@@ -704,6 +716,7 @@ export function DataTable({
704
716
  </tr>
705
717
  </DialogProvider></>
706
718
  ))}
719
+
707
720
  </tbody>
708
721
 
709
722
  <tfoot>
@@ -720,6 +733,10 @@ export function DataTable({
720
733
  {importVisible && (<DataImporter onClose={() => {
721
734
  setImportVisible(false);
722
735
  }}/>)}
736
+
737
+ {selectedRow && (
738
+ <HistoryDialog onClose={() => setSelectedRow(null)} modelName={selectedModel?.name} recordId={selectedRow} />
739
+ )}
723
740
  <RestoreConfirmationModal
724
741
  isOpen={isBackupModalOpen}
725
742
  onClose={() => setIsBackupModalOpen(false)}
@@ -93,6 +93,7 @@ const TextField = forwardRef(function TextField(
93
93
  searchable,
94
94
  labelProps,
95
95
  showErrors=false,
96
+ after,
96
97
  ...rest
97
98
  },
98
99
  ref,
@@ -186,7 +187,7 @@ const TextField = forwardRef(function TextField(
186
187
  ></textarea>
187
188
  )}
188
189
 
189
- <div className={"flex flex-row flex-1 flex-start"}>
190
+ <div className={"flex flex-1 flex-no-gap flex-start"}>
190
191
  {!mult && (
191
192
  <input
192
193
  ref={inputRef}
@@ -207,7 +208,7 @@ const TextField = forwardRef(function TextField(
207
208
  {...rest}
208
209
  />
209
210
  )}
210
-
211
+ {after}
211
212
  </div>
212
213
  </div>
213
214
  {errors.length > 0 && (
@@ -0,0 +1,96 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Dialog } from "./Dialog.jsx";
3
+ import { FaHistory } from "react-icons/fa";
4
+ import {Trans, useTranslation} from "react-i18next";
5
+ import "./HistoryDialog.scss";
6
+
7
+ // Sub-component for displaying a single history entry
8
+ const HistoryEntry = ({ entry }) => {
9
+ const { t } = useTranslation();
10
+ // The backend has transformed the data for us.
11
+ // _id: history doc id, _v: version, _op: operation type (i,u,d), _rid: original record id
12
+ const { _id, _v, _op, _updatedAt, _user, _rid, ...data } = entry; // ...data collects the remaining fields
13
+
14
+ const operationDetails = {
15
+ // The keys 'i', 'u', 'd' correspond to what the backend sends now.
16
+ 'i': { label: t('history.op.create', 'Creation'), className: 'op-insert' },
17
+ 'u': { label: t('history.op.update', 'Update'), className: 'op-update' },
18
+ 'd': { label: t('history.op.delete', 'Deletion'), className: 'op-delete' },
19
+ };
20
+ const opInfo = operationDetails[_op] || { label: _op, className: '' };
21
+
22
+ return (
23
+ <div className="history-entry">
24
+ <div className="history-entry-header">
25
+ <span className={`op-badge ${opInfo.className}`}>{opInfo.label}</span>
26
+ <span className="history-date">{t('history.datePrefix', 'On')} {new Date(_updatedAt).toLocaleString()}</span>
27
+ {_user && <span className="history-user">{t('history.byUser', 'by')} <strong>{_user}</strong></span>}
28
+ <span className="history-version">{t('history.version', 'Version:')} {_v}</span>
29
+ </div>
30
+ <div className="history-entry-data">
31
+ <pre>{JSON.stringify(data, null, 2)}</pre>
32
+ </div>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
38
+ const [history, setHistory] = useState([]);
39
+ const [loading, setLoading] = useState(true);
40
+ const [error, setError] = useState(null);
41
+ const { t, i18n } = useTranslation();
42
+
43
+ useEffect(() => {
44
+ if (!modelName || !recordId) return;
45
+
46
+ const fetchHistory = async () => {
47
+ setLoading(true);
48
+ setError(null);
49
+ try {
50
+ // Assuming the API endpoint for history is structured like this
51
+ const query = await fetch(`/api/data/history/${modelName}/${recordId}`, {
52
+ headers: {
53
+ "Content-Type": "application/json"
54
+ }
55
+ });
56
+ const response = await query.json();
57
+ if (response.success) {
58
+ // L'API devrait retourner l'historique trié du plus récent au plus ancien
59
+ setHistory(response.data);
60
+ } else {
61
+ setError(response.error || i18n.t("history.error", "Could not load history."));
62
+ }
63
+ } catch (err) {
64
+ setError(i18n.t("history.error", "Could not load history."));
65
+ console.error(err);
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ fetchHistory();
72
+ }, [modelName, recordId]);
73
+
74
+ return (
75
+ <Dialog
76
+ title={<><FaHistory style={{ marginRight: '8px' }} /> <Trans i18nKey={"history.title"}>Historique de l'enregistrement</Trans></>}
77
+ onClose={onClose}
78
+ isClosable={true}
79
+ className="history-dialog"
80
+ >
81
+ <div className="history-dialog-content">
82
+ {loading && <div><Trans i18nKey={"history.loading"}>Chargement de l'historique..</Trans></div>}
83
+ {error && <div className="error-message">{error}</div>}
84
+ {!loading && !error && (
85
+ <div className="history-list">
86
+ {history.length > 0 ? (
87
+ history.map(entry => <HistoryEntry key={entry._id} entry={entry} />)
88
+ ) : (
89
+ <div><Trans i18nKey={"history.noHistory"}>Aucun historique trouvé pour cet enregistrement.</Trans></div>
90
+ )}
91
+ </div>
92
+ )}
93
+ </div>
94
+ </Dialog>
95
+ );
96
+ };
@@ -0,0 +1,73 @@
1
+ .history-dialog {
2
+
3
+ .dialog-content {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100%;
7
+ padding: 0; // Le padding sera sur le content wrapper
8
+ }
9
+ }
10
+
11
+ .history-dialog-content {
12
+ overflow-y: auto;
13
+ padding: 1rem;
14
+ background-color: #f4f4f4;
15
+ }
16
+
17
+ .history-list {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 1rem;
21
+ }
22
+
23
+ .history-entry {
24
+ border: 1px solid #ddd;
25
+ border-radius: 4px;
26
+ background-color: #fff;
27
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
28
+
29
+ .history-entry-header {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 1rem;
33
+ padding: 0.5rem 1rem;
34
+ background-color: #f9f9f9;
35
+ border-bottom: 1px solid #eee;
36
+ font-size: 0.9em;
37
+ color: #333;
38
+
39
+ .op-badge {
40
+ padding: 0.2rem 0.6rem;
41
+ border-radius: 12px;
42
+ color: white;
43
+ font-weight: bold;
44
+ font-size: 0.8em;
45
+ text-transform: uppercase;
46
+ }
47
+
48
+ .op-insert { background-color: #28a745; } // green
49
+ .op-update { background-color: #007bff; } // blue
50
+ .op-delete { background-color: #dc3545; } // red
51
+
52
+ .history-version {
53
+ margin-left: auto;
54
+ font-weight: bold;
55
+ }
56
+ }
57
+
58
+ .history-entry-data {
59
+ padding: 1rem;
60
+
61
+ pre {
62
+ white-space: pre-wrap;
63
+ word-wrap: break-word;
64
+ background-color: #2d2d2d;
65
+ color: #f8f8f2;
66
+ padding: 1rem;
67
+ border-radius: 4px;
68
+ margin: 0;
69
+ font-family: 'Courier New', Courier, monospace;
70
+ font-size: 0.85em;
71
+ }
72
+ }
73
+ }
@@ -41,6 +41,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
41
41
  const [modelName, setModelName] = useState(initialModel?.name || ''); // Utilisation de initialModel
42
42
  const [modelMaxRequestData, setModelMaxRequestData] = useState(initialModel?.maxRequestData || ''); // Utilisation de initialModel
43
43
  const [modelDescription, setModelDescription] = useState(initialModel?.name ? t(`model_description_${initialModel?.name}`, initialModel?.description) : '');
44
+ const [modelHistory, setModelHistory] = useState(!!initialModel?.history || undefined);
44
45
  const [fields, setFields] = useState([]);
45
46
  const [changed, setChanged] = useState(false);
46
47
 
@@ -53,6 +54,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
53
54
  setModelMaxRequestData(initialModel.maxRequestData || defaultMaxRequestData);
54
55
  setModelDescription(initialModel.name ? t(`model_description_${initialModel.name}`, initialModel.description) : '');
55
56
  setFields([...(initialModel.fields || []).map(m => ({...m}))]);
57
+ setModelHistory(initialModel.history);
56
58
  } else {
57
59
  // Mode création : on réinitialise tout pour une nouvelle génération
58
60
  setModelName('');
@@ -60,6 +62,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
60
62
  setFields([]);
61
63
  setUseAI(true); // On active l'IA par défaut
62
64
  setModelVisible(false);
65
+ setModelHistory(false);
63
66
  }
64
67
  }, [initialModel]);
65
68
 
@@ -130,6 +133,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
130
133
  setModelDescription('');
131
134
  setModelMaxRequestData(defaultMaxRequestData);
132
135
  setFields([{ name: '', type: 'string' }]);
136
+ setModelHistory(undefined);
133
137
  }
134
138
  setDatasToLoad(datas=>[...datas, modelName]);
135
139
 
@@ -208,6 +212,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
208
212
  name: modelName,
209
213
  description: modelDescription,
210
214
  maxRequestData: modelMaxRequestData,
215
+ history: modelHistory,
211
216
  fields: (fields || []).map((field) => {
212
217
  delete field['_isNewField'];
213
218
  let otherFields = [];
@@ -312,8 +317,8 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
312
317
  }
313
318
 
314
319
  const handleRenameField = (fi, oldVal) => {
315
- const prompted = prompt(t('core.renameFields', 'Renommer le champ'), oldVal);
316
- if (prompted !== oldVal) {
320
+ const prompted = window.prompt(t('core.renameFields', 'Renommer le champ'), oldVal);
321
+ if (prompted && prompted !== oldVal) {
317
322
  mutationRename.mutateAsync({oldName: oldVal, newName: prompted}).then(e =>{
318
323
  const newFields = [...fields];
319
324
  const field = newFields[fi];
@@ -542,21 +547,22 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
542
547
  </div>
543
548
 
544
549
  <div className="field">
545
- <div className="flex field-bg">
546
- <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
550
+ <div className="checkbox-label flex flex-1">
551
+ <CheckboxField
552
+ label={<Trans i18nKey={"history.title"}>Historique</Trans>}
553
+ disabled={modelLocked || (isLocalUser(me) && field.locked)}
554
+ checked={field.required}
555
+ onChange={(e) => {
556
+ const newFields = [...fields];
557
+ newFields[index].history = e ? { enabled: true } : undefined;
558
+ setFields(newFields);
559
+ }}
560
+ help={field.required && t('modelcreator.history.hint')}
561
+ />
547
562
  </div>
548
- <TextField
549
- multiline
550
- help={t('modelcreator.field.description')}
551
- id="modelDescription"
552
- disabled={modelLocked}
553
- value={modelDescription}
554
- onChange={(e) => {
555
- setModelDescription(e.target.value);
556
- setChanged(true)
557
- }}
558
- />
559
563
  </div>
564
+
565
+
560
566
  <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
561
567
  {fields.map((field, index) => <ModelCreatorField
562
568
  key={initialModel?.name + '_field_' + index}
@@ -601,6 +607,21 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
601
607
  setChanged(true)
602
608
  }}
603
609
  />
610
+
611
+ <div className="flex flex-no-wrap">
612
+ <div className="checkbox-label flex flex-1">
613
+ <CheckboxField
614
+ label={<Trans i18nKey={"history"}>Historique</Trans>}
615
+ help={t('modelcreator.field.history', '')}
616
+ disabled={modelLocked}
617
+ checked={!!modelHistory}
618
+ onChange={(e) => {
619
+ setModelHistory(e? { enabled: true }: false);
620
+ }}
621
+ />
622
+ </div>
623
+ </div>
624
+
604
625
  <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
605
626
  {fields.map((field, index) => <ModelCreatorField
606
627
  key={initialModel?.name + '_field_' + index}
@@ -74,11 +74,10 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
74
74
  }}
75
75
  help={t('modelcreator.name.hint')}
76
76
  required
77
+ after={!(!modelLocked && isLocalUser(me) && field.locked) && !field._isNewField && (
78
+ <Button type={"button"} className={"btn-form btn-last"}
79
+ onClick={() => handleRenameField(index, field.name)}><FaEdit/></Button>)}
77
80
  />
78
- {!(!modelLocked && isLocalUser(me) && field.locked) && !field._isNewField && (
79
- <Button type={"button"} className={"btn-form btn-last"}
80
- onClick={() => handleRenameField(index, field.name)}><FaEdit/></Button>)}
81
-
82
81
  </div>
83
82
 
84
83
  </div>
@@ -449,11 +448,11 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
449
448
  </div>
450
449
 
451
450
  {!['file', 'relation', 'array', 'calculated'].includes(field.type) && (<div
452
- className="flex flex-no-wrap field-bg mg-item">
451
+ className="flex flex-no-wrap mg-item">
453
452
 
454
453
  {['string_t', 'string', 'richtext', 'password', 'url', 'phone', 'email'].includes(field.type) && (<>
455
454
  {hint('modelcreator.default.hint')}
456
- <div className="flex flex-1">
455
+ <div className="flex flex-1 field-bg">
457
456
  <TextField
458
457
  className="flex-1"
459
458
  value={field.default}
@@ -195,7 +195,7 @@ export const useTutorials = () => {
195
195
 
196
196
  // Écoute les événements de modification de données pour vérifier la progression en arrière-plan.
197
197
  useEffect(() => {
198
- const handleDataChange = (payload) => {
198
+ const handleDataChange = async (payload) => {
199
199
  console.log(`[Tutoriels] Événement de données reçu.`, payload);
200
200
  if (me?.activeTutorial) {
201
201
  triggerTutorialCheck();