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.
- package/CHANGELOG.md +440 -0
- package/README.md +782 -712
- package/client/src/App.jsx +13 -20
- package/client/src/App.scss +4 -0
- package/client/src/AssistantChat.jsx +5 -4
- package/client/src/DataEditor.jsx +1 -1
- package/client/src/DataLayout.jsx +2 -2
- package/client/src/DataTable.jsx +66 -5
- package/client/src/ExportDialog.jsx +2 -2
- package/client/src/Field.jsx +5 -4
- package/client/src/HistoryDialog.jsx +96 -0
- package/client/src/HistoryDialog.scss +73 -0
- package/client/src/KanbanCard.jsx +4 -2
- package/client/src/KanbanConfigModal.jsx +5 -7
- package/client/src/ModelCreator.jsx +36 -15
- package/client/src/ModelCreatorField.jsx +14 -15
- package/client/src/PackGallery.jsx +89 -9
- package/client/src/PackGallery.scss +58 -4
- package/client/src/RTETrans.jsx +11 -0
- package/client/src/RelationValue.jsx +3 -4
- package/client/src/constants.js +1 -1
- package/client/src/core/data.js +2 -1
- package/client/src/hooks/useTutorials.jsx +1 -1
- package/client/src/translations.js +217 -0
- package/package.json +9 -2
- package/server.js +4 -4
- package/src/HistoryDialog.jsx +86 -0
- package/src/HistoryDialog.scss +77 -0
- package/src/constants.js +6 -0
- package/src/defaultModels.js +23 -10
- package/src/email.js +1 -1
- package/src/engine.js +6 -6
- package/src/events.js +15 -5
- package/src/gameObject.js +0 -29
- package/src/i18n.js +1 -1
- package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
- package/src/modules/assistant/constants.js +16 -0
- package/src/modules/data/data.history.js +304 -0
- package/src/modules/data/data.js +306 -238
- package/src/modules/data/data.routes.js +33 -7
- package/src/modules/mongodb.js +17 -1
- package/src/modules/user.js +21 -4
- package/src/modules/workflow.js +133 -48
- package/src/packs.js +1016 -9
- package/src/services/index.js +11 -0
- package/src/services/stripe.js +195 -0
- package/src/workers/crypto-worker.js +3 -3
- package/test/data.backup.integration.test.js +5 -0
- package/test/data.history.integration.test.js +193 -0
- package/test/data.integration.test.js +144 -41
- package/test/events.test.js +27 -27
- package/test/globalTeardown.js +1 -0
- package/test/model.integration.test.js +5 -0
- package/test/workflow.actions.integration.test.js +4 -2
- package/test/workflow.integration.test.js +3 -1
- package/test/workflow.robustness.test.js +2 -0
package/client/src/App.jsx
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
},
|
package/client/src/App.scss
CHANGED
|
@@ -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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
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
|
|
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
|
|
package/client/src/DataTable.jsx
CHANGED
|
@@ -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
|
|
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(<
|
|
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
|
|
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
|
|
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
|
{/*
|
package/client/src/Field.jsx
CHANGED
|
@@ -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-
|
|
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
|
|
1435
|
-
if (!e
|
|
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 => ['
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
546
|
-
<
|
|
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}
|