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
@@ -61,6 +61,7 @@ import i18next from "i18next";
61
61
  import {websiteTranslations} from "./translations.js";
62
62
 
63
63
  import { Tooltip } from 'react-tooltip';
64
+ import {providers} from "../../src/modules/assistant/constants.js";
64
65
 
65
66
  let queryClient = new QueryClient();
66
67
 
@@ -113,40 +114,32 @@ function Layout ({header, routes, body, footer}) {
113
114
  },
114
115
  body: JSON.stringify({
115
116
  model: 'env', // On cible le modèle 'env'
116
- filter: {
117
- // On cherche les enregistrements dont le nom est SOIT l'un, SOIT l'autre
118
- "$or": [
119
- { "name": "OPENAI_API_KEY" },
120
- { "name": "GOOGLE_API_KEY" }
121
- ]
122
- },
123
- limit: 2 // Pas besoin de plus de 2 résultats
117
+ filter: { "name": { "$in": Object.values(providers).map(p => p.key) } },
118
+ limit: Object.values(providers).length
124
119
  })
125
120
  }).then(res => res.json());
126
121
  },
127
122
  {
128
123
  enabled: !!me && models.find(f => f.name === 'env' && me?.username === f.username), // Toujours activé uniquement si l'utilisateur est connecté
129
- staleTime: 60000, // On peut réduire le staleTime, car l'utilisateur peut ajouter/supprimer une clé à tout moment
124
+ staleTime: 60000,
130
125
  onSuccess: (response) => {
131
-
132
- // La réponse de l'API de recherche est un objet { success: true, data: [...] }
133
126
  if (!response.data) {
134
127
  setAssistantConfig(null);
135
128
  return;
136
129
  }
137
-
138
130
  const availableKeys = response.data;
131
+ const newConfig = Object.keys(providers).reduce((config, providerKey) => {
132
+ const envVarName = providers[providerKey].key;
133
+ const foundKey = availableKeys.find(key => key.name === envVarName);
134
+ if (foundKey) {
135
+ config[providerKey] = foundKey.value;
136
+ }
137
+ return config;
138
+ }, {});
139
139
 
140
- const newConfig = {
141
- openai: availableKeys.find(key => key.name === 'OPENAI_API_KEY')?.value || (useAI ? process.env.OPENAI_API_KEY : undefined),
142
- google: availableKeys.find(key => key.name === 'GOOGLE_API_KEY')?.value || (useAI ? process.env.GOOGLE_API_KEY : undefined),
143
- };
144
-
145
- // On met à jour l'état si au moins une clé est disponible
146
- if (newConfig.openai || newConfig.google) {
140
+ if (Object.values(newConfig).filter(Boolean).length > 0) {
147
141
  setAssistantConfig(newConfig);
148
142
  } else {
149
- // Important : on s'assure de remettre à null si aucune clé n'est trouvée
150
143
  setAssistantConfig(null);
151
144
  }
152
145
  },
@@ -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
  }
@@ -7,6 +7,7 @@ import { useModelContext } from "./contexts/ModelContext.jsx";
7
7
  import { Trans, useTranslation } from "react-i18next";
8
8
  import Markdown from 'react-markdown';
9
9
  import {useQueryClient} from "react-query";
10
+ import {providers} from "../../src/modules/assistant/constants.js";
10
11
 
11
12
  const AssistantChat = ({ config }) => {
12
13
  const { selectedModel } = useModelContext();
@@ -170,10 +171,10 @@ const AssistantChat = ({ config }) => {
170
171
 
171
172
  // Logique pour le sélecteur de fournisseur (OpenAI/Google)
172
173
  const availableProviders = useMemo(() => {
173
- const providers = [];
174
- if (config?.openai) providers.push({ value: 'openai', label: 'OpenAI' });
175
- if (config?.google) providers.push({ value: 'google', label: 'Google' });
176
- return providers;
174
+ const prs = Object.keys(providers).map(p => {
175
+ return config?.[p] ? ({value: p, label: p}) : null;
176
+ })
177
+ return prs.filter(Boolean);
177
178
  }, [config]);
178
179
 
179
180
  const [selectedProvider, setSelectedProvider] = useState(null);
@@ -241,7 +241,7 @@ export const DataEditor = forwardRef(function MyDataEditor({
241
241
  return <CheckboxField
242
242
  help={t('field_' + model.name + '_' + field.name + '_hint', field.hint || '')}
243
243
  key={field.name} {...inputProps} checked={value}
244
- onChange={(e) => handleChange({name: field.name, value: e.target.checked})}/>
244
+ onChange={(e) => handleChange({name: field.name, value: e})}/>
245
245
  case 'string':
246
246
  case 'string_t':
247
247
  {
@@ -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,
@@ -165,6 +166,51 @@ const Header = ({
165
166
  ;
166
167
  }
167
168
 
169
+
170
+ const RichText = ({ value, initialLang }) => {
171
+ const availableLangs = useMemo(() => (value ? Object.keys(value).filter(k => value[k]) : []), [value]);
172
+
173
+ const [selectedLang, setSelectedLang] = useState(() => {
174
+ if (availableLangs.includes(initialLang)) {
175
+ return initialLang;
176
+ }
177
+ return availableLangs[0] || null;
178
+ });
179
+
180
+ useEffect(() => {
181
+ setSelectedLang(initialLang);
182
+ }, [initialLang]);
183
+
184
+ if (!value || availableLangs.length === 0) {
185
+ return null;
186
+ }
187
+
188
+ if (availableLangs.length === 1) {
189
+ return <div className="rte-value" dangerouslySetInnerHTML={{ __html: value[availableLangs[0]] || '' }} />;
190
+ }
191
+
192
+ return (
193
+ <div className="richtext-t-cell">
194
+ <div className="lang-switcher">
195
+ {availableLangs.map(langCode => (
196
+ <button
197
+ key={langCode}
198
+ className={`lang-btn ${selectedLang === langCode ? 'active' : ''}`}
199
+ onClick={(e) => {
200
+ e.stopPropagation();
201
+ setSelectedLang(langCode);
202
+ }}
203
+ title={langCode.toUpperCase()}
204
+ >
205
+ {langCode.toUpperCase()}
206
+ </button>
207
+ ))}
208
+ </div>
209
+ <div className="rte-value" dangerouslySetInnerHTML={{ __html: value[selectedLang] || '' }} />
210
+ </div>
211
+ );
212
+ };
213
+
168
214
  export function DataTable({
169
215
  model,
170
216
  checkedItems,
@@ -193,6 +239,7 @@ export function DataTable({
193
239
  const queryClient = useQueryClient();
194
240
  const {me} = useAuthContext()
195
241
 
242
+
196
243
  const isDataLoaded = true;
197
244
  const [importVisible, setImportVisible] = useState(false);
198
245
  const [filterActive, setFilterActive] = useState(false)
@@ -203,6 +250,8 @@ export function DataTable({
203
250
  const [showExportDialog, setExportDialogVisible] = useState(false);
204
251
 
205
252
 
253
+ const [selectedRow, setSelectedRow] = useState(null);
254
+
206
255
  const [showAddPack, setAddPackVisible] = useState(false);
207
256
  const handleAddPack = () => {
208
257
  setAddPackVisible(!showAddPack);
@@ -238,7 +287,7 @@ export function DataTable({
238
287
  console.log('Données non trouvées');
239
288
  }
240
289
 
241
- 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 });
242
291
  queryClient.invalidateQueries(['api/data', item._model, 'page', page, elementsPerPage, pagedFilters[item._model], pagedSort[item._model]]);
243
292
 
244
293
  } else {
@@ -458,7 +507,7 @@ export function DataTable({
458
507
  <CheckboxField className={"input-ref"}
459
508
  checked={checkedItems.some(i => i._id === item._id)}
460
509
  onChange={(e) => {
461
- if (e.target.checked) {
510
+ if (e) {
462
511
  setCheckedItems(items => {
463
512
  return [...items, item];
464
513
  });
@@ -630,8 +679,7 @@ export function DataTable({
630
679
  }
631
680
  if (field.type === 'richtext_t') {
632
681
  return <td key={field.name} className={isLightColor(field.color)?"lighted":""}>
633
- {hiddenable(<div className="rte-value"
634
- dangerouslySetInnerHTML={{__html: item[field.name]?.[lang] || ''}}></div>)}
682
+ {hiddenable(<RichText value={item[field.name]} initialLang={lang} />)}
635
683
  </td>;
636
684
  }
637
685
  if (field.type === 'color') {
@@ -653,6 +701,14 @@ export function DataTable({
653
701
  >
654
702
  <FaCopy/>
655
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
+
656
712
  <button data-tooltip-id="tooltipActions"
657
713
  data-tooltip-content={t('btns.delete', 'Supprimer')}
658
714
  onClick={() => handleDelete(item)}><FaTrash/></button>
@@ -660,6 +716,7 @@ export function DataTable({
660
716
  </tr>
661
717
  </DialogProvider></>
662
718
  ))}
719
+
663
720
  </tbody>
664
721
 
665
722
  <tfoot>
@@ -676,6 +733,10 @@ export function DataTable({
676
733
  {importVisible && (<DataImporter onClose={() => {
677
734
  setImportVisible(false);
678
735
  }}/>)}
736
+
737
+ {selectedRow && (
738
+ <HistoryDialog onClose={() => setSelectedRow(null)} modelName={selectedModel?.name} recordId={selectedRow} />
739
+ )}
679
740
  <RestoreConfirmationModal
680
741
  isOpen={isBackupModalOpen}
681
742
  onClose={() => setIsBackupModalOpen(false)}
@@ -99,7 +99,7 @@ function ExportDialog({ isOpen, onClose, onExport, currentModel, availableModels
99
99
  name="exportSelection"
100
100
  label={t('exportDialog.exportSelection.label', "Exporter la sélection")}
101
101
  checked={exportSelection}
102
- onChange={(e) => setExportSelection(e.target.checked)}
102
+ onChange={(e) => setExportSelection(e)}
103
103
  disabled={!hasSelection}
104
104
  hint={hasSelection
105
105
  ? t('exportDialog.exportSelection.hint', "Exportera uniquement les lignes actuellement sélectionnées dans le tableau.")
@@ -119,7 +119,7 @@ function ExportDialog({ isOpen, onClose, onExport, currentModel, availableModels
119
119
  multiple={true}
120
120
  />
121
121
 
122
- <CheckboxField label={t('exportDialog.withModels.label', "Inclure les modèles ?")} checked={withModels} onChange={(e) => setWithModels(e.target.checked)} />
122
+ <CheckboxField label={t('exportDialog.withModels.label', "Inclure les modèles ?")} checked={withModels} onChange={(e) => setWithModels(e)} />
123
123
 
124
124
  {/* OU: Utiliser un composant MultiSelectField (à créer/importer) pour plusieurs modèles */}
125
125
  {/*
@@ -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 && (
@@ -1431,8 +1432,8 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
1431
1432
  <CheckboxField
1432
1433
  checked={checked}
1433
1434
  onChange={e => {
1434
- setChecked(e.target.checked);
1435
- if (!e.target.checked) {
1435
+ setChecked(e);
1436
+ if (!e) {
1436
1437
  onChange({name: field?.name, value: null});
1437
1438
  }
1438
1439
  }}
@@ -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
+ }
@@ -12,9 +12,9 @@ const KanbanCard = ({ card, model, subItemsField }) => {
12
12
  const subItems = subItemsField && card[subItemsField] ? card[subItemsField] : [];
13
13
  const tr = useTranslation();
14
14
  // Logique du Drag & Drop natif pour la carte
15
-
16
15
  const lang = (tr.i18n.resolvedLanguage || tr.i18n.language).split(/[-_]/)?.[0];
17
16
 
17
+ console.log(subItems)
18
18
  const getfield = (model, data) => {
19
19
  const v = getDataAsString(model, data, tr, models);
20
20
  if(!v){
@@ -66,6 +66,8 @@ const KanbanCard = ({ card, model, subItemsField }) => {
66
66
  <div className="kanban-card-subitems">
67
67
  {subItems.map((subItem, index) => {
68
68
  const it = subItem;
69
+ if( typeof(subItem) === 'string')
70
+ return subItem;
69
71
  const v = getDataAsString(model, subItem, tr, models);
70
72
  if (!v) {
71
73
  const r = `
@@ -81,7 +83,7 @@ const KanbanCard = ({ card, model, subItemsField }) => {
81
83
  className="image" src={`/resources/${it.guid}`}
82
84
  alt={`${it.name} (${it.guid})`}/></a>
83
85
  }
84
- return '';
86
+ return v;
85
87
  })}
86
88
  </div>
87
89
  )}
@@ -19,7 +19,7 @@ const KanbanConfigModal = ({ isOpen, onClose, onSave, model, initialSettings })
19
19
  const groupableFields = useMemo(() => {
20
20
  // ... (pas de changement ici)
21
21
  return modelFields
22
- .filter(field => ['select', 'string', 'string_t'].includes(field.type))
22
+ .filter(field => ['enum', 'model', 'string', 'string_t'].includes(field.type))
23
23
  .map(field => ({
24
24
  label: t(`field_${model.name}_${field.name}`, field.name),
25
25
  value: field.name,
@@ -29,7 +29,7 @@ const KanbanConfigModal = ({ isOpen, onClose, onSave, model, initialSettings })
29
29
  const subItemFields = useMemo(() => {
30
30
  // ... (pas de changement ici)
31
31
  return modelFields
32
- .filter(field => field.type === 'relation' && field.multiple)
32
+ .filter(field => (field.type === 'relation' && field.multiple) || field.type === 'array')
33
33
  .map(field => ({
34
34
  label: t(`field_${field.name}`, { defaultValue: field.name }),
35
35
  value: field.name,
@@ -64,11 +64,9 @@ const KanbanConfigModal = ({ isOpen, onClose, onSave, model, initialSettings })
64
64
  onSave({ groupByField, subItemsField: subItemsField || '' });
65
65
  };
66
66
 
67
- // --- MODIFICATION 3 : Supprimer le useEffect fautif ---
68
- // useEffect(() => {
69
- // setGroupByField(groupableFields[0]?.value || '');
70
- // }, []); // <--- CE BLOC EST SUPPRIMÉ
71
-
67
+ useEffect(() => {
68
+ setSubItemsField(subItemFields[0]?.value);
69
+ })
72
70
  if (!isOpen) return null;
73
71
 
74
72
  return (
@@ -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}