data-primals-engine 1.2.6 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/README.md +782 -712
  3. package/client/src/App.jsx +13 -20
  4. package/client/src/App.scss +4 -0
  5. package/client/src/AssistantChat.jsx +5 -4
  6. package/client/src/DataEditor.jsx +1 -1
  7. package/client/src/DataLayout.jsx +2 -2
  8. package/client/src/DataTable.jsx +66 -5
  9. package/client/src/ExportDialog.jsx +2 -2
  10. package/client/src/Field.jsx +5 -4
  11. package/client/src/HistoryDialog.jsx +96 -0
  12. package/client/src/HistoryDialog.scss +73 -0
  13. package/client/src/KanbanCard.jsx +4 -2
  14. package/client/src/KanbanConfigModal.jsx +5 -7
  15. package/client/src/ModelCreator.jsx +36 -15
  16. package/client/src/ModelCreatorField.jsx +14 -15
  17. package/client/src/PackGallery.jsx +89 -9
  18. package/client/src/PackGallery.scss +58 -4
  19. package/client/src/RTETrans.jsx +11 -0
  20. package/client/src/RelationValue.jsx +3 -4
  21. package/client/src/constants.js +1 -1
  22. package/client/src/core/data.js +2 -1
  23. package/client/src/hooks/useTutorials.jsx +1 -1
  24. package/client/src/translations.js +217 -0
  25. package/package.json +9 -2
  26. package/server.js +4 -4
  27. package/src/HistoryDialog.jsx +86 -0
  28. package/src/HistoryDialog.scss +77 -0
  29. package/src/constants.js +6 -0
  30. package/src/defaultModels.js +23 -10
  31. package/src/email.js +1 -1
  32. package/src/engine.js +6 -6
  33. package/src/events.js +15 -5
  34. package/src/gameObject.js +0 -29
  35. package/src/i18n.js +1 -1
  36. package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
  37. package/src/modules/assistant/constants.js +16 -0
  38. package/src/modules/data/data.history.js +304 -0
  39. package/src/modules/data/data.js +306 -238
  40. package/src/modules/data/data.routes.js +33 -7
  41. package/src/modules/mongodb.js +17 -1
  42. package/src/modules/user.js +21 -4
  43. package/src/modules/workflow.js +133 -48
  44. package/src/packs.js +1016 -9
  45. package/src/services/index.js +11 -0
  46. package/src/services/stripe.js +195 -0
  47. package/src/workers/crypto-worker.js +3 -3
  48. package/test/data.backup.integration.test.js +5 -0
  49. package/test/data.history.integration.test.js +193 -0
  50. package/test/data.integration.test.js +144 -41
  51. package/test/events.test.js +27 -27
  52. package/test/globalTeardown.js +1 -0
  53. package/test/model.integration.test.js +5 -0
  54. package/test/workflow.actions.integration.test.js +4 -2
  55. package/test/workflow.integration.test.js +3 -1
  56. package/test/workflow.robustness.test.js +2 -0
@@ -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>
@@ -284,7 +283,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
284
283
  checked={field.multiline}
285
284
  onChange={(e) => {
286
285
  const newFields = [...fields];
287
- newFields[index].multiline = e.target.checked;
286
+ newFields[index].multiline = e;
288
287
  setFields(newFields);
289
288
  }}
290
289
  help={field.multiline && t('modelcreator.multiline.hint')}
@@ -373,7 +372,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
373
372
  checked={field.multiple}
374
373
  onChange={(e) => {
375
374
  const newFields = [...fields];
376
- newFields[index].multiple = e.target.checked;
375
+ newFields[index].multiple = e;
377
376
  setFields(newFields);
378
377
  }}
379
378
  /></div>
@@ -422,7 +421,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
422
421
  checked={field.required}
423
422
  onChange={(e) => {
424
423
  const newFields = [...fields];
425
- newFields[index].required = e.target.checked;
424
+ newFields[index].required = e;
426
425
  setFields(newFields);
427
426
  }}
428
427
  help={field.required && t('modelcreator.required.hint')}
@@ -440,7 +439,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
440
439
  checked={field.unique}
441
440
  onChange={(e) => {
442
441
  const newFields = [...fields];
443
- newFields[index].unique = e.target.checked;
442
+ newFields[index].unique = e;
444
443
  setFields(newFields);
445
444
  }}
446
445
  help={field.unique && t('modelcreator.unique.hint')}
@@ -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}
@@ -522,7 +521,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
522
521
  disabled={modelLocked || (isLocalUser(me) && field.locked)}
523
522
  onChange={(e) => {
524
523
  const newFields = [...fields];
525
- newFields[index].default = e.target.checked;
524
+ newFields[index].default = e;
526
525
  setFields(newFields);
527
526
  }}
528
527
  />
@@ -612,7 +611,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
612
611
  checked={field.condition !== undefined}
613
612
  onChange={(e) => {
614
613
  const newFields = [...fields];
615
- if (e.target.checked) {
614
+ if (e) {
616
615
  newFields[index].condition = null;
617
616
  } else {
618
617
  delete newFields[index].condition;
@@ -701,7 +700,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
701
700
  checked={field.asMain}
702
701
  onChange={(e) => {
703
702
  const newFields = [...fields];
704
- newFields[index].asMain = e.target.checked;
703
+ newFields[index].asMain = e;
705
704
  setFields(newFields);
706
705
  }}
707
706
  help={field.asMain && t('modelcreator.asMain.hint')}
@@ -752,7 +751,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
752
751
  checked={field.hiddenable}
753
752
  onChange={(e) => {
754
753
  const newFields = [...fields];
755
- newFields[index].hiddenable = e.target.checked;
754
+ newFields[index].hiddenable = e;
756
755
  setFields(newFields);
757
756
  }}
758
757
  help={field.unique && t('modelcreator.hiddenable.hint')}
@@ -768,7 +767,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
768
767
  checked={field.anonymized}
769
768
  onChange={(e) => {
770
769
  const newFields = [...fields];
771
- newFields[index].anonymized = e.target.checked;
770
+ newFields[index].anonymized = e;
772
771
  setFields(newFields);
773
772
  }}
774
773
  />
@@ -1,13 +1,16 @@
1
1
  import React, { useState } from 'react';
2
2
  import {useMutation, useQuery, useQueryClient} from 'react-query';
3
3
  import { Trans, useTranslation } from 'react-i18next';
4
- import { FaBoxOpen, FaChevronLeft, FaDownload, FaStar, FaTag, FaUser } from 'react-icons/fa';
4
+ import {FaBoxOpen, FaChevronLeft, FaDownload, FaPlus, FaStar, FaTag, FaUser} from 'react-icons/fa';
5
5
  import Button from './Button.jsx';
6
6
  import { useNotificationContext } from './NotificationProvider.jsx';
7
7
  import { useModelContext } from './contexts/ModelContext.jsx';
8
8
 
9
9
  import './PackGallery.scss';
10
10
  import {elementsPerPage} from "../../src/constants.js";
11
+ import Markdown from "react-markdown";
12
+ import {Dialog, DialogProvider} from "./Dialog.jsx";
13
+ import {TextField} from "./Field.jsx";
11
14
 
12
15
  // --- API Fetching Functions ---
13
16
  const fetchPacks = async (sortBy, lang) => {
@@ -117,7 +120,8 @@ const PackDetail = ({ packId, onBack }) => {
117
120
  </div>
118
121
  <div className="pack-content">
119
122
  <h2><Trans i18nKey="packs.description">Description</Trans></h2>
120
- <div className="description-content" dangerouslySetInnerHTML={{__html: pack.description || ''}}/>
123
+
124
+ <div className="description-content"><Markdown>{pack.description}</Markdown></div>
121
125
 
122
126
  <h2><Trans i18nKey="packs.modelsIncluded">Modèles inclus</Trans></h2>
123
127
  <p>{renderModelsList(pack.models)}</p>
@@ -127,6 +131,8 @@ const PackDetail = ({ packId, onBack }) => {
127
131
  <pre>{JSON.stringify(pack.data, null, 2)}</pre>
128
132
  </div>
129
133
  </div>
134
+
135
+
130
136
  </div>
131
137
  );
132
138
  };
@@ -151,11 +157,15 @@ const installPackMutationFn = async ({packId, lang}) => {
151
157
  const PackGallery = () => {
152
158
  const { t, i18n } = useTranslation();
153
159
  const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
160
+ const { addNotification } = useNotificationContext();
154
161
 
162
+ const [showManualInstallDialog, setShowManualInstallDialog] = useState(false);
163
+ const [manualPackJson, setManualPackJson] = useState('');
155
164
  const [view, setView] = useState('list'); // 'list' ou 'detail'
156
165
  const [selectedPackId, setSelectedPackId] = useState(null);
157
166
  const [sortBy, setSortBy] = useState({ field: '_updatedAt', order: -1 });
158
167
 
168
+ const queryClient = useQueryClient()
159
169
  const { data: packs, isLoading, isError } = useQuery(['packs', sortBy], () => fetchPacks(sortBy, lang));
160
170
 
161
171
  const handleSelectPack = (packId) => {
@@ -163,6 +173,40 @@ const PackGallery = () => {
163
173
  setView('detail');
164
174
  };
165
175
 
176
+ const handleManualInstall = async () => {
177
+ try {
178
+ const packData = JSON.parse(manualPackJson.trim());
179
+ addNotification({ status: 'info', title: t('packs.install.started', `Installation du pack personnalisé...`) });
180
+
181
+ const response = await fetch('/api/packs/install', {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ },
186
+ body: JSON.stringify({ packData, lang }),
187
+ });
188
+
189
+ if (!response.ok) {
190
+ throw new Error(await response.text());
191
+ }
192
+
193
+ addNotification({
194
+ status: 'completed',
195
+ title: t('datatable.pack.install.success', 'Pack installé avec succès !')
196
+ });
197
+ setShowManualInstallDialog(false);
198
+ setManualPackJson('');
199
+ queryClient.invalidateQueries(['packs', sortBy]); // Rafraîchir la liste
200
+
201
+ } catch (error) {
202
+ addNotification({
203
+ status: 'error',
204
+ title: t('datatable.pack.install.error', `Échec de l'installation`),
205
+ message: error.message
206
+ });
207
+ }
208
+ };
209
+
166
210
  const handleBackToList = () => {
167
211
  setSelectedPackId(null);
168
212
  setView('list');
@@ -182,16 +226,52 @@ const PackGallery = () => {
182
226
  <>
183
227
  <div className="gallery-header">
184
228
  <h1><Trans i18nKey="packs.galleryTitle">Galerie de Packs</Trans></h1>
185
- <div className="sort-options">
186
- <span><Trans i18nKey="packs.sortBy">Trier par :</Trans></span>
187
- <Button onClick={() => setSortBy({ field: '_updatedAt', order: -1 })} className={sortBy.field === '_updatedAt' ? 'active' : ''}>
188
- <Trans i18nKey="packs.sort.lastUpdated">Derniers ajouts</Trans>
189
- </Button>
190
- <Button onClick={() => setSortBy({ field: 'stars', order: -1 })} className={sortBy.field === 'stars' ? 'active' : ''}>
191
- <Trans i18nKey="packs.sort.mostStarred">Plus populaires</Trans>
229
+ <div className="header-actions">
230
+ <div className="sort-options">
231
+ <span><Trans i18nKey="packs.sortBy">Trier par :</Trans></span>
232
+ <Button onClick={() => setSortBy({ field: '_updatedAt', order: -1 })} className={sortBy.field === '_updatedAt' ? 'active' : ''}>
233
+ <Trans i18nKey="packs.sort.lastUpdated">Derniers ajouts</Trans>
234
+ </Button>
235
+ <Button onClick={() => setSortBy({ field: 'stars', order: -1 })} className={sortBy.field === 'stars' ? 'active' : ''}>
236
+ <Trans i18nKey="packs.sort.mostStarred">Plus populaires</Trans>
237
+ </Button>
238
+ </div>
239
+ <Button
240
+ onClick={() => setShowManualInstallDialog(true)}
241
+ className="add-pack-button"
242
+ >
243
+ <FaPlus /> <Trans i18nKey="packs.manualInstall">Importer</Trans>
192
244
  </Button>
193
245
  </div>
194
246
  </div>
247
+ {showManualInstallDialog && (
248
+ <DialogProvider>
249
+ <Dialog
250
+ isModal={true}
251
+ onClose={() => setShowManualInstallDialog(false)}
252
+ title={t('packs.manualInstall.title', 'Installation manuelle de pack')}
253
+ >
254
+ <div className="manual-install-dialog">
255
+ <p>
256
+ <Trans i18nKey="packs.manualInstall.instructions">
257
+ Collez ici le JSON de configuration du pack que vous souhaitez installer.
258
+ </Trans>
259
+ </p>
260
+ <TextField
261
+ multiline={true}
262
+ value={manualPackJson}
263
+ onChange={(e) => setManualPackJson(e.target.value)}
264
+ placeholder={t('packs.manualInstall.placeholder', '{"name": "Mon Pack", "description": "...", "models": [...], "data": [...]}')}
265
+ rows={15}
266
+ />
267
+ <div className="flex actions right">
268
+ <Button onClick={() => setShowManualInstallDialog(false)}><Trans i18nKey={"btns.cancel"}>Annuler</Trans></Button>
269
+ <Button disabled={!manualPackJson.trim()} className="btn-primary" onClick={() => handleManualInstall()}><Trans i18nKey={"packs.install"}>Installer</Trans></Button>
270
+ </div>
271
+ </div>
272
+ </Dialog>
273
+ </DialogProvider>
274
+ )}
195
275
  <div className="pack-list">
196
276
  {packs.map(pack => (
197
277
  <PackCard key={pack._id} pack={pack} onSelect={handleSelectPack} />
@@ -7,20 +7,38 @@
7
7
 
8
8
  .gallery-header {
9
9
  display: flex;
10
+ flex-wrap: wrap;
11
+ gap: 1rem ;
10
12
  justify-content: space-between;
11
13
  align-items: center;
12
14
  padding-bottom: 1rem;
13
15
  border-bottom: 1px solid #e0e0e0;
14
16
  margin-bottom: 1rem;
17
+
15
18
  h1 {
16
19
  margin: 0;
20
+ flex: 1; // Permet au titre de prendre l'espace disponible
21
+ min-width: 200px; // Empêche le titre de devenir trop petit
17
22
  }
18
- .sort-options {
23
+
24
+ .header-actions {
19
25
  display: flex;
20
- gap: 0.5rem;
26
+ gap: 1rem;
21
27
  align-items: center;
22
- .btn.active {
23
- color: white;
28
+
29
+ .sort-options {
30
+ display: flex;
31
+ gap: 0.5rem;
32
+ align-items: center;
33
+ flex-wrap: wrap; // Pour le responsive
34
+
35
+ .btn.active {
36
+ color: white;
37
+ }
38
+ }
39
+
40
+ .add-pack-button {
41
+ white-space: nowrap; // Empêche le texte du bouton de se casser
24
42
  }
25
43
  }
26
44
  }
@@ -153,4 +171,40 @@
153
171
  }
154
172
  }
155
173
  }
174
+ }
175
+
176
+ // À ajouter à la fin de votre fichier CSS
177
+ .manual-install-dialog {
178
+ width: 100%;
179
+
180
+ p {
181
+ margin-top: 0;
182
+ color: #666;
183
+ font-size: 0.9em;
184
+ }
185
+
186
+ textarea {
187
+ width: 100%;
188
+ padding: 1rem;
189
+ border: 1px solid #ddd;
190
+ border-radius: 4px;
191
+ font-family: monospace;
192
+ resize: vertical;
193
+ min-height: 300px;
194
+ background-color: #f9f9f9;
195
+ color: #333;
196
+
197
+ &:focus {
198
+ outline: none;
199
+ border-color: var(--primary-color);
200
+ box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2);
201
+ }
202
+ }
203
+
204
+ .dialog-actions {
205
+ display: flex;
206
+ justify-content: flex-end;
207
+ gap: 0.5rem;
208
+ margin-top: 1rem;
209
+ }
156
210
  }
@@ -53,6 +53,16 @@ const RTETrans = ({ value, onChange, field }) => {
53
53
  };
54
54
  const languagesToAdd = availableLangs.filter(lang => !existingLangs.includes(lang.code));
55
55
 
56
+ const confirmDelete = (e, lang) => {
57
+ e.preventDefault();
58
+ e.stopPropagation();
59
+ if( !confirm(t('rte.confirmLangDeletion', 'Confirm lang deletion ?')))
60
+ return;
61
+ const val = {...value};
62
+ delete val[lang];
63
+ onChange(val);
64
+ setActiveTab(Object.keys(val)[0] || null);
65
+ }
56
66
  return (
57
67
  <div className="rte-trans-container">
58
68
  <div className="tabs-container flex items-center border-b border-gray-200">
@@ -64,6 +74,7 @@ const RTETrans = ({ value, onChange, field }) => {
64
74
  title={availableLangs.find(f=>f.code===lang)?.name.value || ''}
65
75
  >
66
76
  {lang.toUpperCase()}
77
+ <button onClick={(e) => confirmDelete(e, lang)}>x</button>
67
78
  </button>
68
79
  ))}
69
80
  {languagesToAdd.length > 0 && (
@@ -187,8 +187,7 @@ const RelationValue = ({ field, data, align }) => {
187
187
  if (f.type === 'code') {
188
188
  let t;
189
189
  try {
190
- t = f.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : '';
191
-
190
+ t = f.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : t = intVal[f.name].toString();
192
191
  } catch (e) {
193
192
  t = intVal[f.name].toString();
194
193
  }
@@ -196,8 +195,8 @@ const RelationValue = ({ field, data, align }) => {
196
195
  <dt>{columnName} {span}</dt>
197
196
  <dd>
198
197
  <CodeField onChange={() => {
199
- }} language={field.language || 'json'}
200
- name={f.name} value={field.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : t} />
198
+ }} language={f.language || 'json'}
199
+ name={f.name} value={f.language === 'json' ? JSON.stringify(intVal[f.name], null, 2) : t} />
201
200
  </dd>
202
201
  </>;
203
202
  }
@@ -65,7 +65,7 @@ export const OPERAND_TYPES = {
65
65
 
66
66
 
67
67
  export const getHost = () => {
68
- return process.env.HOST || host || 'localhost';
68
+ return import.meta.env.HOST || host || 'localhost';
69
69
  }
70
70
 
71
71
 
@@ -1,4 +1,5 @@
1
- import {getHost, MONGO_OPERATORS} from "../constants.js";
1
+ import { MONGO_OPERATORS} from "../constants.js";
2
+ import {getHost} from "../../../src/constants";
2
3
 
3
4
  const isProd = import.meta.env.MODE === 'production';
4
5
  export const urlData = isProd ? 'https://'+getHost()+'/' : 'http://localhost:7633/';
@@ -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();