data-primals-engine 1.2.6 → 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/src/App.jsx +13 -20
- package/client/src/AssistantChat.jsx +5 -4
- package/client/src/DataEditor.jsx +1 -1
- package/client/src/DataTable.jsx +47 -3
- package/client/src/ExportDialog.jsx +2 -2
- package/client/src/Field.jsx +2 -2
- 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 +63 -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/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.js +4601 -4552
- package/src/modules/data/data.routes.js +29 -3
- package/src/modules/user.js +12 -1
- package/src/modules/workflow.js +133 -48
- 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 +65 -2
- package/test/workflow.actions.integration.test.js +2 -2
- package/test/workflow.integration.test.js +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,8 @@ MONGO_DB_URL=mongodb://127.0.0.1:27017
|
|
|
84
84
|
| JWT_SECRET | Secret key for signing JWT authentication tokens. | a_long_random_secret_string |
|
|
85
85
|
| OPENAI_API_KEY | Your optional OpenAI API key for AI features. | sk-xxxxxxxxxxxxxxxxxxxx |
|
|
86
86
|
| GOOGLE_API_KEY | Your optional Google (Gemini) API key for AI features. | AIzaSyxxxxxxxxxxxxxxxxxxxx |
|
|
87
|
+
| DEEPSEEK_API_KEY | Your optional DeepSeek API key for AI features. | AIzaSyxxxxxxxxxxxxxxxxxxxx |
|
|
88
|
+
| ANTHROPIC_API_KEY | Your optional Anthropic API key for AI features. | AIzaSyxxxxxxxxxxxxxxxxxxxx |
|
|
87
89
|
| AWS_ACCESS_KEY_ID | AWS access key for S3 storage (files, backups). Keep empty to disable | AKIAIOSFODNN7EXAMPLE |
|
|
88
90
|
| AWS_SECRET_ACCESS_KEY | AWS secret access key. | wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY | |
|
|
89
91
|
| AWS_REGION | Region for your S3 bucket. | eu-west-3 | |
|
|
@@ -489,15 +491,49 @@ await loadFromDump(currentUser, { modelsOnly: false });
|
|
|
489
491
|
|
|
490
492
|
### installPack(packId, user, lang)
|
|
491
493
|
|
|
492
|
-
> Installs a
|
|
494
|
+
> Installs a data pack.
|
|
493
495
|
|
|
494
496
|
Example:
|
|
495
497
|
|
|
498
|
+
```javascript
|
|
499
|
+
const result = await installPack([
|
|
500
|
+
{
|
|
501
|
+
"name": "My custom pack",
|
|
502
|
+
"description": "Markdown **description** of the pack",
|
|
503
|
+
"tags": ["customPack", "tag1", "tag2"],
|
|
504
|
+
"models": [
|
|
505
|
+
"env", // default model
|
|
506
|
+
{
|
|
507
|
+
"name": "post",
|
|
508
|
+
"description": "Defines a post",
|
|
509
|
+
"fields": [
|
|
510
|
+
{"name": "subject", "type": "string", "required": true},
|
|
511
|
+
]
|
|
512
|
+
}, // or custom
|
|
513
|
+
],
|
|
514
|
+
"data": {
|
|
515
|
+
"all": { // all languages installed data
|
|
516
|
+
"post": [
|
|
517
|
+
{"subject": "My pack first data"}
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
"en": { // English specific installed data
|
|
521
|
+
"post": [
|
|
522
|
+
{"subject": "My english first post"}
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
], user, "en");
|
|
528
|
+
// Returns installation summary
|
|
529
|
+
```
|
|
530
|
+
|
|
496
531
|
```javascript
|
|
497
532
|
const result = await installPack("61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
|
|
498
533
|
// Returns installation summary
|
|
499
534
|
```
|
|
500
535
|
|
|
536
|
+
> You can also open the pack gallery to see the JSON structure of each pack, before installing them.
|
|
501
537
|
|
|
502
538
|
---
|
|
503
539
|
|
|
@@ -538,7 +574,7 @@ A workflow is composed of two main parts: **Triggers** and **Actions**.
|
|
|
538
574
|
- **CreateData**: Create a new document in any model.
|
|
539
575
|
- **UpdateData**: Modify one or more existing documents that match a filter.
|
|
540
576
|
- **SendEmail**: Send a transactional email using dynamic data from the trigger.
|
|
541
|
-
- **
|
|
577
|
+
- **HttpRequest**: Make an HTTP request (GET, POST, etc.) to an external service or API.
|
|
542
578
|
- **ExecuteScript**: Run a custom JavaScript snippet for complex logic, data transformation, or conditional branching.
|
|
543
579
|
- **GenerateAIContent**: Use an integrated AI provider (like OpenAI or Gemini) to generate text, summarize content, or make decisions.
|
|
544
580
|
- **Wait**: Pause the workflow for a specific duration before continuing to the next step
|
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
|
},
|
|
@@ -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
|
{
|
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
|
@@ -1431,8 +1431,8 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
|
|
|
1431
1431
|
<CheckboxField
|
|
1432
1432
|
checked={checked}
|
|
1433
1433
|
onChange={e => {
|
|
1434
|
-
setChecked(e
|
|
1435
|
-
if (!e
|
|
1434
|
+
setChecked(e);
|
|
1435
|
+
if (!e) {
|
|
1436
1436
|
onChange({name: field?.name, value: null});
|
|
1437
1437
|
}
|
|
1438
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} />
|
|
@@ -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
|
-
|
|
23
|
+
|
|
24
|
+
.header-actions {
|
|
19
25
|
display: flex;
|
|
20
|
-
gap:
|
|
26
|
+
gap: 1rem;
|
|
21
27
|
align-items: center;
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|