data-primals-engine 1.3.0 → 1.3.2

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 (43) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/Dockerfile +19 -9
  3. package/DockerfileTest +19 -9
  4. package/README.md +58 -24
  5. package/client/package-lock.json +172 -1
  6. package/client/package.json +1 -0
  7. package/client/src/App.scss +4 -0
  8. package/client/src/CalendarConfigModal.jsx +60 -21
  9. package/client/src/CalendarView.jsx +107 -0
  10. package/client/src/CalendarView.scss +9 -0
  11. package/client/src/DataLayout.jsx +54 -27
  12. package/client/src/DataTable.jsx +19 -2
  13. package/client/src/Dialog.scss +4 -3
  14. package/client/src/Field.jsx +3 -2
  15. package/client/src/HistoryDialog.jsx +253 -0
  16. package/client/src/HistoryDialog.scss +320 -0
  17. package/client/src/ModelCreator.jsx +36 -15
  18. package/client/src/ModelCreatorField.jsx +5 -6
  19. package/client/src/ViewSwitcher.jsx +6 -6
  20. package/client/src/hooks/useTutorials.jsx +1 -1
  21. package/client/src/translations.js +298 -1
  22. package/package.json +2 -2
  23. package/src/email.js +1 -1
  24. package/src/engine.js +6 -6
  25. package/src/events.js +15 -5
  26. package/src/gameObject.js +0 -29
  27. package/src/modules/data/data.history.js +490 -0
  28. package/src/modules/data/data.js +139 -108
  29. package/src/modules/data/data.routes.js +4 -4
  30. package/src/modules/mongodb.js +17 -1
  31. package/src/modules/user.js +9 -3
  32. package/src/packs.js +2 -1
  33. package/src/services/stripe.js +67 -13
  34. package/src/workers/crypto-worker.js +3 -3
  35. package/test/data.backup.integration.test.js +5 -0
  36. package/test/data.history.integration.test.js +193 -0
  37. package/test/data.integration.test.js +79 -39
  38. package/test/events.test.js +27 -27
  39. package/test/globalTeardown.js +1 -0
  40. package/test/model.integration.test.js +5 -0
  41. package/test/workflow.actions.integration.test.js +2 -0
  42. package/test/workflow.integration.test.js +2 -0
  43. package/test/workflow.robustness.test.js +2 -0
@@ -0,0 +1,320 @@
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
+ }
74
+
75
+ // In C:/Dev/data-primals-engine/client/src/HistoryDialog.scss
76
+
77
+ .history-dialog .dialog-content {
78
+ // The new layout handles its own padding and structure.
79
+ padding: 0;
80
+ }
81
+
82
+ .history-dialog-content-split {
83
+ display: flex;
84
+ gap: 1rem;
85
+ height: 65vh; /* Adjust height as needed */
86
+ max-height: 700px;
87
+ }
88
+
89
+ .history-list-container {
90
+ flex: 1;
91
+ overflow-y: auto;
92
+ border-right: 1px solid #e2e2e2;
93
+ padding: 0 1rem 1rem 0;
94
+ min-width: 300px;
95
+ }
96
+
97
+ .history-preview-container {
98
+ flex: 0.8; /* Give preview a bit more space */
99
+ display: flex;
100
+ flex-direction: column;
101
+ min-width: 300px;
102
+
103
+ .preview-header {
104
+ display: flex;
105
+ justify-content: space-between;
106
+ align-items: center;
107
+ margin-bottom: 0.5rem;
108
+ padding-bottom: 0.5rem;
109
+ border-bottom: 1px solid #e2e2e2;
110
+
111
+ h4 {
112
+ margin: 0;
113
+ padding: 0;
114
+ border: none;
115
+ }
116
+
117
+ .btn-revert {
118
+ padding: 0.3rem 0.8rem;
119
+ font-size: 0.85em;
120
+ background-color: #ffc107; // A warning color like amber
121
+ color: black;
122
+ border: 1px solid #e0a800;
123
+
124
+ &:hover:not(:disabled) {
125
+ background-color: #e0a800;
126
+ }
127
+
128
+ &:disabled {
129
+ background-color: #ffca2c;
130
+ opacity: 0.7;
131
+ cursor: not-allowed;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ .history-preview-area {
138
+ flex-grow: 1;
139
+ overflow-y: auto;
140
+ background-color: #f9f9f9;
141
+ padding: 0.75rem;
142
+ border: 1px solid #ddd;
143
+ border-radius: 4px;
144
+
145
+ pre {
146
+ white-space: pre-wrap;
147
+ word-wrap: break-word;
148
+ margin: 0;
149
+ }
150
+
151
+ .placeholder {
152
+ color: #666;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ height: 100%;
157
+ text-align: center;
158
+ }
159
+ }
160
+
161
+ .history-entry {
162
+ border: 1px solid #ddd;
163
+ border-radius: 4px;
164
+ margin-bottom: 0.75rem;
165
+ padding: 0.75rem;
166
+ transition: border-color 0.2s, box-shadow 0.2s;
167
+
168
+ &.selected {
169
+ border-color: #007bff;
170
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
171
+ }
172
+ }
173
+
174
+ .history-entry-header {
175
+ display: flex;
176
+ align-items: center;
177
+ flex-wrap: wrap;
178
+ gap: 0.5rem 1rem;
179
+ margin-bottom: 0.5rem;
180
+
181
+ .btn-preview {
182
+ margin-left: auto;
183
+ padding: 0.2rem 0.6rem;
184
+ font-size: 0.8em;
185
+ }
186
+ }
187
+
188
+ .history-entry-data pre {
189
+ background-color: #f0f0f0;
190
+ padding: 0.5rem;
191
+ border-radius: 4px;
192
+ max-height: 150px;
193
+ overflow-y: auto;
194
+ margin: 0;
195
+ }
196
+
197
+ /* Dark Theme Adjustments */
198
+ .dark .history-list-container { border-right-color: #3f3f3f;
199
+ }
200
+ .dark .history-preview-container .preview-header {
201
+ border-bottom-color: #3f3f3f;
202
+ .btn-revert {
203
+ background-color: #d8a100;
204
+ border-color: #b38600;
205
+ color: white;
206
+ &:hover:not(:disabled) {
207
+ background-color: #b38600;
208
+ }
209
+ }
210
+ }
211
+ .dark .history-preview-area { background-color: #2a2a2a; border-color: #3f3f3f; }
212
+ .dark .history-entry { border-color: #3f3f3f; }
213
+ .dark .history-entry.selected { border-color: #58a6ff; box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.25); }
214
+ .dark .history-entry-data pre { background-color: #2a2a2a; }
215
+
216
+ /* Styles for human-friendly history data display */
217
+ .history-entry-data {
218
+ padding: 0.75rem 1rem;
219
+ font-size: 0.9em;
220
+ background-color: #fdfdfd;
221
+ border-top: 1px solid #f0f0f0;
222
+
223
+ /* The new implementation doesn't use <pre> here anymore */
224
+ pre {
225
+ display: none;
226
+ }
227
+ }
228
+
229
+ .changes-list, .snapshot-list {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 0.5rem;
233
+ }
234
+
235
+ .change-detail, .snapshot-item {
236
+ display: grid;
237
+ grid-template-columns: minmax(100px, 150px) 1fr;
238
+ gap: 0.5rem;
239
+ align-items: start;
240
+ }
241
+
242
+ .change-field-name, .snapshot-key {
243
+ font-weight: 600;
244
+ color: #333;
245
+ white-space: nowrap;
246
+ overflow: hidden;
247
+ text-overflow: ellipsis;
248
+ padding-top: 4px; /* Align with value */
249
+ }
250
+
251
+ .change-values {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 0.5rem;
255
+ background-color: #f0f0f0;
256
+ padding: 0.25rem 0.5rem;
257
+ border-radius: 4px;
258
+ overflow: hidden;
259
+ }
260
+
261
+ .change-arrow {
262
+ color: #888;
263
+ font-weight: bold;
264
+ }
265
+
266
+ .value-from {
267
+ text-decoration: line-through;
268
+ color: #d9534f; /* red-ish */
269
+ }
270
+
271
+ .value-to {
272
+ color: #5cb85c; /* green-ish */
273
+ font-weight: 500;
274
+ }
275
+
276
+ .snapshot-value, .value-from, .value-to {
277
+ white-space: nowrap;
278
+ overflow: hidden;
279
+ text-overflow: ellipsis;
280
+ display: block; /* Ensure it takes up the block for ellipsis */
281
+ }
282
+
283
+ .value-empty {
284
+ color: #999;
285
+ font-style: italic;
286
+ }
287
+
288
+ .value-object, .value-boolean {
289
+ font-family: 'Courier New', Courier, monospace;
290
+ background-color: #e9e9e9;
291
+ padding: 0.1rem 0.3rem;
292
+ border-radius: 3px;
293
+ display: inline-block;
294
+ }
295
+
296
+ .no-changes-text {
297
+ color: #777;
298
+ font-style: italic;
299
+ padding: 0.5rem 0;
300
+ }
301
+
302
+ /* Dark Theme Adjustments */
303
+ .dark .history-entry-data {
304
+ background-color: #2c2c2c;
305
+ border-top-color: #3a3a3a;
306
+ }
307
+ .dark .change-field-name, .dark .snapshot-key {
308
+ color: #e0e0e0;
309
+ }
310
+ .dark .change-values {
311
+ background-color: #3a3a3a;
312
+ }
313
+ .dark .value-from { color: #ff7b7b; }
314
+ .dark .value-to { color: #73d173; }
315
+ .dark .value-object, .dark .value-boolean {
316
+ background-color: #444;
317
+ }
318
+ .dark .no-changes-text, .dark .value-empty {
319
+ color: #888;
320
+ }
@@ -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}
@@ -8,9 +8,9 @@ import { Tooltip } from 'react-tooltip';
8
8
 
9
9
  import "./ViewSwitcher.scss"
10
10
  const viewOptions = [
11
- { id: 'table', icon: <FaTable />, labelKey: 'views.table' },
12
- { id: 'kanban', icon: <FaColumns />, labelKey: 'views.kanban' },
13
- //{ id: 'calendar', icon: <FaRegCalendarAlt />, labelKey: 'views.calendar' },
11
+ { id: 'table', icon: <FaTable />, labelKey: 'views.table', defaultLabel: 'Tableau' },
12
+ { id: 'kanban', icon: <FaColumns />, labelKey: 'views.kanban', defaultLabel: 'Kanban' },
13
+ { id: 'calendar', icon: <FaRegCalendarAlt />, labelKey: 'views.calendar', defaultLabel: 'Calendrier' },
14
14
  ];
15
15
 
16
16
  const ViewSwitcher = ({ currentView, onViewChange, configuredViews, onConfigureView }) => {
@@ -27,10 +27,10 @@ const ViewSwitcher = ({ currentView, onViewChange, configuredViews, onConfigureV
27
27
  <Button
28
28
  onClick={() => onViewChange(view.id)}
29
29
  className={`btn-view ${isActive ? 'active' : ''}`}
30
- title={t(view.labelKey, view.id)}
30
+ title={t(view.labelKey, view.defaultLabel)}
31
31
  >
32
32
  {view.icon}
33
- <span className="hidden md:inline-block ml-2">{t(view.labelKey, view.id)}</span>
33
+ <span className="hidden md:inline-block ml-2">{t(view.labelKey, view.defaultLabel)}</span>
34
34
  </Button>
35
35
  {/* AJOUT : Bouton de configuration pour la vue active */}
36
36
  {isActive && view.id !== 'table' && (
@@ -38,7 +38,7 @@ const ViewSwitcher = ({ currentView, onViewChange, configuredViews, onConfigureV
38
38
  onClick={onConfigureView}
39
39
  className="btn-view-settings"
40
40
  data-tooltip-id="view-settings-tooltip"
41
- data-tooltip-content={t('views.configure', { view: t(view.labelKey) })}
41
+ data-tooltip-content={t('views.configure', 'Configurer la vue {{view}}', { view: t(view.labelKey, view.defaultLabel) })}
42
42
  >
43
43
  <FaCog />
44
44
  </Button>
@@ -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();