data-primals-engine 1.2.6-rc3 → 1.3.0
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/README.md +38 -2
- package/client/package-lock.json +247 -4354
- package/client/package.json +16 -15
- package/client/src/App.jsx +13 -20
- package/client/src/App.scss +1 -0
- package/client/src/AssistantChat.jsx +5 -4
- package/client/src/DataEditor.jsx +2 -2
- package/client/src/DataTable.jsx +47 -3
- package/client/src/ExportDialog.jsx +2 -2
- package/client/src/Field.jsx +6 -18
- package/client/src/KanbanCard.jsx +4 -2
- package/client/src/KanbanConfigModal.jsx +5 -7
- package/client/src/ModelCreatorField.jsx +9 -9
- 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/translations.js +80 -0
- package/package.json +8 -1
- package/server.js +4 -4
- package/src/constants.js +6 -0
- package/src/defaultModels.js +23 -10
- package/src/filter.js +35 -5
- 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.core.js +1 -3
- package/src/modules/data/data.js +4601 -4525
- package/src/modules/data/data.routes.js +29 -3
- package/src/modules/mongodb.js +3 -1
- package/src/modules/user.js +12 -1
- package/src/modules/workflow.js +198 -117
- package/src/packs.js +1015 -9
- package/src/services/index.js +11 -0
- package/src/services/stripe.js +141 -0
- package/test/data.integration.test.js +66 -3
- package/test/workflow.actions.integration.test.js +474 -0
- package/test/workflow.integration.test.js +1 -1
package/client/package.json
CHANGED
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@tiptap/react": "^2.11.5",
|
|
16
|
-
"react-router-dom": "^7.1.5",
|
|
17
|
-
"react-query": "^3.39.3",
|
|
18
|
-
"react-icons": "^5.0.1",
|
|
19
|
-
"react-tooltip": "^5.28.1",
|
|
20
|
-
"react-helmet": "^6.1.0",
|
|
21
|
-
"react-cookie": "^8.0.0",
|
|
22
|
-
"react-syntax-highlighter": "^15.6.1",
|
|
23
|
-
"react-international-phone": "^4.5.0",
|
|
24
16
|
"react-chartjs-2": "^5.3.0",
|
|
25
17
|
"react-code-blocks": "^0.1.6",
|
|
26
|
-
"react-
|
|
18
|
+
"react-cookie": "^8.0.0",
|
|
19
|
+
"react-ga": "^3.3.1",
|
|
20
|
+
"react-helmet": "^6.1.0",
|
|
21
|
+
"react-icons": "^5.0.1",
|
|
22
|
+
"react-international-phone": "^4.5.0",
|
|
23
|
+
"react-query": "^3.39.3",
|
|
24
|
+
"react-router-dom": "^7.1.5",
|
|
25
|
+
"react-syntax-highlighter": "^15.6.1",
|
|
26
|
+
"react-tooltip": "^5.28.1"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@babel/runtime": "^7.24.7",
|
|
@@ -43,15 +43,16 @@
|
|
|
43
43
|
"@uiw/react-codemirror": "^4.23.10",
|
|
44
44
|
"chart.js": "^4.4.9",
|
|
45
45
|
"chartjs-adapter-date-fns": "^3.0.0",
|
|
46
|
-
"react-dom": "^18.3.1",
|
|
47
|
-
"react-router": "^7.2.0",
|
|
48
|
-
"yet-another-react-lightbox": "^3.22.0",
|
|
49
|
-
"react": ">=18.0.0",
|
|
50
46
|
"classnames": "^2.5.1",
|
|
51
47
|
"date-fns": "^4.1.0",
|
|
52
48
|
"express-rate-limit": "^7.5.1",
|
|
53
49
|
"lowlight": "^3.3.0",
|
|
54
|
-
"
|
|
50
|
+
"react": ">=18.0.0",
|
|
51
|
+
"react-dom": "^18.3.1",
|
|
52
|
+
"react-router": "^7.2.0",
|
|
53
|
+
"react-switch": "^7.1.0",
|
|
54
|
+
"uniqid": "^5.4.0",
|
|
55
|
+
"yet-another-react-lightbox": "^3.22.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@eslint/js": "^9.17.0",
|
|
@@ -67,4 +68,4 @@
|
|
|
67
68
|
"sass-embedded": "^1.83.4",
|
|
68
69
|
"vite": "^6.1.1"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|
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);
|
|
@@ -239,9 +239,9 @@ export const DataEditor = forwardRef(function MyDataEditor({
|
|
|
239
239
|
onChange={(e) => handleChange({name: field.name, value: e})}/>
|
|
240
240
|
case 'boolean':
|
|
241
241
|
return <CheckboxField
|
|
242
|
-
help={
|
|
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
|
{
|
package/client/src/DataTable.jsx
CHANGED
|
@@ -165,6 +165,51 @@ const Header = ({
|
|
|
165
165
|
;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
|
|
169
|
+
const RichText = ({ value, initialLang }) => {
|
|
170
|
+
const availableLangs = useMemo(() => (value ? Object.keys(value).filter(k => value[k]) : []), [value]);
|
|
171
|
+
|
|
172
|
+
const [selectedLang, setSelectedLang] = useState(() => {
|
|
173
|
+
if (availableLangs.includes(initialLang)) {
|
|
174
|
+
return initialLang;
|
|
175
|
+
}
|
|
176
|
+
return availableLangs[0] || null;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
setSelectedLang(initialLang);
|
|
181
|
+
}, [initialLang]);
|
|
182
|
+
|
|
183
|
+
if (!value || availableLangs.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (availableLangs.length === 1) {
|
|
188
|
+
return <div className="rte-value" dangerouslySetInnerHTML={{ __html: value[availableLangs[0]] || '' }} />;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="richtext-t-cell">
|
|
193
|
+
<div className="lang-switcher">
|
|
194
|
+
{availableLangs.map(langCode => (
|
|
195
|
+
<button
|
|
196
|
+
key={langCode}
|
|
197
|
+
className={`lang-btn ${selectedLang === langCode ? 'active' : ''}`}
|
|
198
|
+
onClick={(e) => {
|
|
199
|
+
e.stopPropagation();
|
|
200
|
+
setSelectedLang(langCode);
|
|
201
|
+
}}
|
|
202
|
+
title={langCode.toUpperCase()}
|
|
203
|
+
>
|
|
204
|
+
{langCode.toUpperCase()}
|
|
205
|
+
</button>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
<div className="rte-value" dangerouslySetInnerHTML={{ __html: value[selectedLang] || '' }} />
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
168
213
|
export function DataTable({
|
|
169
214
|
model,
|
|
170
215
|
checkedItems,
|
|
@@ -458,7 +503,7 @@ export function DataTable({
|
|
|
458
503
|
<CheckboxField className={"input-ref"}
|
|
459
504
|
checked={checkedItems.some(i => i._id === item._id)}
|
|
460
505
|
onChange={(e) => {
|
|
461
|
-
if (e
|
|
506
|
+
if (e) {
|
|
462
507
|
setCheckedItems(items => {
|
|
463
508
|
return [...items, item];
|
|
464
509
|
});
|
|
@@ -630,8 +675,7 @@ export function DataTable({
|
|
|
630
675
|
}
|
|
631
676
|
if (field.type === 'richtext_t') {
|
|
632
677
|
return <td key={field.name} className={isLightColor(field.color)?"lighted":""}>
|
|
633
|
-
{hiddenable(<
|
|
634
|
-
dangerouslySetInnerHTML={{__html: item[field.name]?.[lang] || ''}}></div>)}
|
|
678
|
+
{hiddenable(<RichText value={item[field.name]} initialLang={lang} />)}
|
|
635
679
|
</td>;
|
|
636
680
|
}
|
|
637
681
|
if (field.type === 'color') {
|
|
@@ -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
|
@@ -34,7 +34,7 @@ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
|
34
34
|
import { PhoneInput } from 'react-international-phone';
|
|
35
35
|
import 'react-international-phone/style.css';
|
|
36
36
|
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
37
|
-
|
|
37
|
+
import Switch from "react-switch";
|
|
38
38
|
export const Form = ({
|
|
39
39
|
name,
|
|
40
40
|
onValidate,
|
|
@@ -492,20 +492,6 @@ const CheckboxField = forwardRef(
|
|
|
492
492
|
return (
|
|
493
493
|
<>
|
|
494
494
|
<div className={cn({field: true, "field-checkbox": true,"field-bg": true})}>
|
|
495
|
-
<div className="inline"><input
|
|
496
|
-
aria-required={required}
|
|
497
|
-
aria-readonly={readOnly}
|
|
498
|
-
readOnly={readOnly}
|
|
499
|
-
type="checkbox"
|
|
500
|
-
checked={value}
|
|
501
|
-
placeholder={placeholder}
|
|
502
|
-
id={id}
|
|
503
|
-
name={name}
|
|
504
|
-
onChange={handleChange}
|
|
505
|
-
minLength={minlength}
|
|
506
|
-
maxLength={maxlength}
|
|
507
|
-
{...rest}
|
|
508
|
-
/>
|
|
509
495
|
{label && (
|
|
510
496
|
<label
|
|
511
497
|
contentEditable={editable}
|
|
@@ -522,7 +508,9 @@ const CheckboxField = forwardRef(
|
|
|
522
508
|
)}
|
|
523
509
|
</label>
|
|
524
510
|
)}
|
|
525
|
-
|
|
511
|
+
<Switch
|
|
512
|
+
onChange={handleChange}
|
|
513
|
+
checked={value} />
|
|
526
514
|
{help && <div className="flex help">{help}</div>}
|
|
527
515
|
</div>
|
|
528
516
|
{errors.length > 0 && (
|
|
@@ -1443,8 +1431,8 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
|
|
|
1443
1431
|
<CheckboxField
|
|
1444
1432
|
checked={checked}
|
|
1445
1433
|
onChange={e => {
|
|
1446
|
-
setChecked(e
|
|
1447
|
-
if (!e
|
|
1434
|
+
setChecked(e);
|
|
1435
|
+
if (!e) {
|
|
1448
1436
|
onChange({name: field?.name, value: null});
|
|
1449
1437
|
}
|
|
1450
1438
|
}}
|
|
@@ -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 (
|
|
@@ -284,7 +284,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
284
284
|
checked={field.multiline}
|
|
285
285
|
onChange={(e) => {
|
|
286
286
|
const newFields = [...fields];
|
|
287
|
-
newFields[index].multiline = e
|
|
287
|
+
newFields[index].multiline = e;
|
|
288
288
|
setFields(newFields);
|
|
289
289
|
}}
|
|
290
290
|
help={field.multiline && t('modelcreator.multiline.hint')}
|
|
@@ -373,7 +373,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
373
373
|
checked={field.multiple}
|
|
374
374
|
onChange={(e) => {
|
|
375
375
|
const newFields = [...fields];
|
|
376
|
-
newFields[index].multiple = e
|
|
376
|
+
newFields[index].multiple = e;
|
|
377
377
|
setFields(newFields);
|
|
378
378
|
}}
|
|
379
379
|
/></div>
|
|
@@ -422,7 +422,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
422
422
|
checked={field.required}
|
|
423
423
|
onChange={(e) => {
|
|
424
424
|
const newFields = [...fields];
|
|
425
|
-
newFields[index].required = e
|
|
425
|
+
newFields[index].required = e;
|
|
426
426
|
setFields(newFields);
|
|
427
427
|
}}
|
|
428
428
|
help={field.required && t('modelcreator.required.hint')}
|
|
@@ -440,7 +440,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
440
440
|
checked={field.unique}
|
|
441
441
|
onChange={(e) => {
|
|
442
442
|
const newFields = [...fields];
|
|
443
|
-
newFields[index].unique = e
|
|
443
|
+
newFields[index].unique = e;
|
|
444
444
|
setFields(newFields);
|
|
445
445
|
}}
|
|
446
446
|
help={field.unique && t('modelcreator.unique.hint')}
|
|
@@ -522,7 +522,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
522
522
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
523
523
|
onChange={(e) => {
|
|
524
524
|
const newFields = [...fields];
|
|
525
|
-
newFields[index].default = e
|
|
525
|
+
newFields[index].default = e;
|
|
526
526
|
setFields(newFields);
|
|
527
527
|
}}
|
|
528
528
|
/>
|
|
@@ -612,7 +612,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
612
612
|
checked={field.condition !== undefined}
|
|
613
613
|
onChange={(e) => {
|
|
614
614
|
const newFields = [...fields];
|
|
615
|
-
if (e
|
|
615
|
+
if (e) {
|
|
616
616
|
newFields[index].condition = null;
|
|
617
617
|
} else {
|
|
618
618
|
delete newFields[index].condition;
|
|
@@ -701,7 +701,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
701
701
|
checked={field.asMain}
|
|
702
702
|
onChange={(e) => {
|
|
703
703
|
const newFields = [...fields];
|
|
704
|
-
newFields[index].asMain = e
|
|
704
|
+
newFields[index].asMain = e;
|
|
705
705
|
setFields(newFields);
|
|
706
706
|
}}
|
|
707
707
|
help={field.asMain && t('modelcreator.asMain.hint')}
|
|
@@ -752,7 +752,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
752
752
|
checked={field.hiddenable}
|
|
753
753
|
onChange={(e) => {
|
|
754
754
|
const newFields = [...fields];
|
|
755
|
-
newFields[index].hiddenable = e
|
|
755
|
+
newFields[index].hiddenable = e;
|
|
756
756
|
setFields(newFields);
|
|
757
757
|
}}
|
|
758
758
|
help={field.unique && t('modelcreator.hiddenable.hint')}
|
|
@@ -768,7 +768,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
768
768
|
checked={field.anonymized}
|
|
769
769
|
onChange={(e) => {
|
|
770
770
|
const newFields = [...fields];
|
|
771
|
-
newFields[index].anonymized = e
|
|
771
|
+
newFields[index].anonymized = e;
|
|
772
772
|
setFields(newFields);
|
|
773
773
|
}}
|
|
774
774
|
/>
|
|
@@ -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 {
|
|
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
|
-
|
|
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="
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
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} />
|