data-primals-engine 1.1.4 → 1.1.5
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 +5 -1
- package/client/package-lock.json +11580 -11664
- package/client/package.json +1 -2
- package/client/src/App.jsx +9 -4
- package/client/src/ConditionBuilder.scss +24 -0
- package/client/src/ConditionBuilder2.jsx +56 -0
- package/client/src/DataEditor.jsx +1 -1
- package/client/src/DataLayout.jsx +20 -11
- package/client/src/DataTable.jsx +77 -10
- package/client/src/Dialog.scss +1 -0
- package/client/src/TourSpotlight.jsx +14 -10
- package/client/src/TutorialsMenu.scss +0 -1
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +3 -8
- package/client/src/core/dom.js +26 -0
- package/client/src/filter.js +40 -1
- package/package.json +4 -5
- package/server.js +3 -2
- package/src/constants.js +2 -0
- package/src/engine.js +18 -13
- package/src/i18n.js +75 -2
- package/src/modules/data.js +13 -2
- package/src/packs.js +4 -3
package/client/package.json
CHANGED
package/client/src/App.jsx
CHANGED
|
@@ -56,7 +56,7 @@ import { translations as allTranslations} from "../../src/i18n.js";
|
|
|
56
56
|
import {getRandom} from "../../src/core.js";
|
|
57
57
|
import {getUserHash} from "../../src/data.js";
|
|
58
58
|
import {seoTitle} from "./constants.js";
|
|
59
|
-
import {host} from "../../src/constants.js";
|
|
59
|
+
import {host, useAI} from "../../src/constants.js";
|
|
60
60
|
import i18next from "i18next";
|
|
61
61
|
import {websiteTranslations} from "./translations.js";
|
|
62
62
|
|
|
@@ -138,8 +138,8 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
138
138
|
const availableKeys = response.data;
|
|
139
139
|
|
|
140
140
|
const newConfig = {
|
|
141
|
-
openai: availableKeys.find(key => key.name === 'OPENAI_API_KEY')?.value,
|
|
142
|
-
google: availableKeys.find(key => key.name === 'GOOGLE_API_KEY')?.value
|
|
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
143
|
};
|
|
144
144
|
|
|
145
145
|
|
|
@@ -495,7 +495,12 @@ function UserPage({notifs,triggerSignin, onAuthenticated}) {
|
|
|
495
495
|
position: 'bottom',
|
|
496
496
|
},
|
|
497
497
|
],
|
|
498
|
-
|
|
498
|
+
guides: [
|
|
499
|
+
{
|
|
500
|
+
selector: '.tourStep-tutorials',
|
|
501
|
+
content: t('tourpoint.tutorials'),
|
|
502
|
+
position: 'bottom'
|
|
503
|
+
},
|
|
499
504
|
{
|
|
500
505
|
selector: '.tourStep-import-datapack',
|
|
501
506
|
content: t('tourpoint.importDatapack'),
|
|
@@ -563,6 +563,8 @@
|
|
|
563
563
|
padding: 8px;
|
|
564
564
|
border: 1px solid #ddd;
|
|
565
565
|
border-radius: 4px;
|
|
566
|
+
width: 100%;
|
|
567
|
+
box-sizing: border-box;
|
|
566
568
|
}
|
|
567
569
|
|
|
568
570
|
.switch-to-expr {
|
|
@@ -669,4 +671,26 @@
|
|
|
669
671
|
margin-left: 20px;
|
|
670
672
|
border-left: 2px solid #eee;
|
|
671
673
|
padding-left: 10px;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.special-structure-block {
|
|
677
|
+
border: 1px solid #ddd;
|
|
678
|
+
border-radius: 4px;
|
|
679
|
+
padding: 10px;
|
|
680
|
+
margin: 5px 0;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.special-structure-args {
|
|
684
|
+
margin-top: 10px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.special-arg {
|
|
688
|
+
margin-bottom: 8px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.special-arg label {
|
|
692
|
+
display: block;
|
|
693
|
+
font-weight: bold;
|
|
694
|
+
margin-bottom: 4px;
|
|
695
|
+
color: #555;
|
|
672
696
|
}
|
|
@@ -86,6 +86,14 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
86
86
|
}else if (operator === '$find') {
|
|
87
87
|
// Cas spécial pour $find qui prend une expression de recherche
|
|
88
88
|
newValue = { [operator]: { $eq: ["", ""] } };
|
|
89
|
+
}else if (operator === '$regexMatch') {
|
|
90
|
+
// Structure spéciale pour $regexMatch
|
|
91
|
+
newValue = {
|
|
92
|
+
[operator]: {
|
|
93
|
+
input: "",
|
|
94
|
+
regex: ""
|
|
95
|
+
}
|
|
96
|
+
};
|
|
89
97
|
}else if (opConfig.converter || (!opConfig.multi && !opConfig.args)) {
|
|
90
98
|
newValue = { [operator]: "" };
|
|
91
99
|
} else {
|
|
@@ -418,6 +426,54 @@ const ExpressionField = ({ value, onChange, path = [], fieldNames, isRoot = fals
|
|
|
418
426
|
</div>
|
|
419
427
|
);
|
|
420
428
|
}
|
|
429
|
+
|
|
430
|
+
if (opConfig.specialStructure) {
|
|
431
|
+
// Cas spécial pour les opérateurs comme $regexMatch
|
|
432
|
+
return (
|
|
433
|
+
<div className="expression-block special-structure-block">
|
|
434
|
+
<div className="expression-header">
|
|
435
|
+
<span className="operator">{operator}</span>
|
|
436
|
+
{editing && (
|
|
437
|
+
<div className="operator-selector-overlay">
|
|
438
|
+
<OperatorSelector onSelect={handleOperatorSelect}/>
|
|
439
|
+
<button type="button" onClick={() => setEditing(false)}>Annuler</button>
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
<div className="expression-actions">
|
|
443
|
+
<button type="button" onClick={() => setEditing(!editing)}><FaRepeat/></button>
|
|
444
|
+
<button type="button" onClick={() => openDoc(operator.substring(1))}><FaInfoCircle/></button>
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
onClick={() => onChange(null, path)}
|
|
448
|
+
className="remove-expr"
|
|
449
|
+
title={i18n.t("cb.deleteBlock")}
|
|
450
|
+
>
|
|
451
|
+
<FaTrash/>
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div className="special-structure-args">
|
|
456
|
+
{Object.entries(args).map(([key, val]) => (
|
|
457
|
+
<div key={key} className="special-arg">
|
|
458
|
+
<label>{key}</label>
|
|
459
|
+
<ExpressionField
|
|
460
|
+
value={val}
|
|
461
|
+
onChange={(newVal) => {
|
|
462
|
+
const newArgs = {...args, [key]: newVal};
|
|
463
|
+
onChange({[operator]: newArgs}, path);
|
|
464
|
+
}}
|
|
465
|
+
fieldNames={fieldNames}
|
|
466
|
+
path={[...path, operator, key]}
|
|
467
|
+
models={models}
|
|
468
|
+
currentModelFields={currentModelFields}
|
|
469
|
+
isInFindContext={isInFindContext}
|
|
470
|
+
/>
|
|
471
|
+
</div>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
421
477
|
// Rendu pour les opérateurs avec nombre d'arguments fixe
|
|
422
478
|
if (isFixedArgs) {
|
|
423
479
|
return (
|
|
@@ -508,7 +508,7 @@ export const DataEditor = forwardRef(function MyDataEditor({
|
|
|
508
508
|
return <p>Sélectionnez un modèle pour afficher le formulaire.</p>;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
const title = record ? t('
|
|
511
|
+
const title = record ? t('editData', 'Edit {{0}}', [model.name]) : `${t('add_in_model')} ${t(`model_${model.name}`, model.name)}`;
|
|
512
512
|
|
|
513
513
|
const hint = (model, field) => <div className="hint-icon"><FaCircleInfo data-field={t(`field_${model.name}_${field.name}`, field.name)} data-tooltip-id={`tooltipHint`} data-tooltip-content={i18n.t(`field_${model.name}_${field.name}_hint`, field.hint || '')} /></div>
|
|
514
514
|
|
|
@@ -211,6 +211,10 @@ function DataLayout() {
|
|
|
211
211
|
mainPartRef.current.scrollIntoView({behavior: "smooth"});
|
|
212
212
|
handleAddData(model);
|
|
213
213
|
}}
|
|
214
|
+
onDuplicateData={(data) => {
|
|
215
|
+
mainPartRef.current.scrollIntoView({behavior: "smooth"});
|
|
216
|
+
handleAddData(selectedModel, data);
|
|
217
|
+
}}
|
|
214
218
|
onShowAPI={() => {
|
|
215
219
|
setAPIInfoVisible(true);
|
|
216
220
|
setDataEditorVisible(false);
|
|
@@ -476,18 +480,23 @@ function DataLayout() {
|
|
|
476
480
|
}
|
|
477
481
|
};
|
|
478
482
|
|
|
479
|
-
const handleAddData = (model)=>{
|
|
483
|
+
const handleAddData = (model, data)=>{
|
|
480
484
|
|
|
481
485
|
handleModelSelect(model);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
486
|
+
|
|
487
|
+
if( data ){
|
|
488
|
+
setFormData(data);
|
|
489
|
+
}else {
|
|
490
|
+
const t = model ? [...model.fields].reduce((acc, field) => {
|
|
491
|
+
if (field.type === "relation") {
|
|
492
|
+
acc[field.name] = field.multiple ? [] : null;
|
|
493
|
+
} else {
|
|
494
|
+
acc[field.name] = getDefaultForType(field);
|
|
495
|
+
}
|
|
496
|
+
return acc;
|
|
497
|
+
}, {}) : [];
|
|
498
|
+
setFormData(t)
|
|
499
|
+
}
|
|
491
500
|
setEditionMode(false);
|
|
492
501
|
setDataEditorVisible(true);
|
|
493
502
|
setAPIInfoVisible(false);
|
|
@@ -539,7 +548,7 @@ function DataLayout() {
|
|
|
539
548
|
// Si un modèle est sélectionné...
|
|
540
549
|
if (selectedModel) {
|
|
541
550
|
// Récupérer les étapes du tour "datapacks"
|
|
542
|
-
const datapacksSteps = allTourSteps.
|
|
551
|
+
const datapacksSteps = allTourSteps.guides;
|
|
543
552
|
// S'assurer que ce tour existe et a des étapes
|
|
544
553
|
if (datapacksSteps && datapacksSteps.length > 0) {
|
|
545
554
|
// 1. Définir les étapes du tour qui doit être affiché
|
package/client/src/DataTable.jsx
CHANGED
|
@@ -9,13 +9,13 @@ import cronstrue from 'cronstrue/i18n';
|
|
|
9
9
|
import {
|
|
10
10
|
FaBook,
|
|
11
11
|
FaCopy,
|
|
12
|
-
FaDatabase,
|
|
12
|
+
FaDatabase, FaEraser,
|
|
13
13
|
FaFileExport,
|
|
14
14
|
FaFileImport,
|
|
15
15
|
FaFilter,
|
|
16
16
|
FaInfo,
|
|
17
17
|
FaPlus,
|
|
18
|
-
FaTrash
|
|
18
|
+
FaTrash, FaWrench
|
|
19
19
|
} from "react-icons/fa";
|
|
20
20
|
import {useNotificationContext} from "./NotificationProvider.jsx";
|
|
21
21
|
import {
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
TextField
|
|
39
39
|
} from "./Field.jsx";
|
|
40
40
|
import RelationValue from "./RelationValue.jsx";
|
|
41
|
-
import {FaPencil, FaTriangleExclamation} from "react-icons/fa6";
|
|
41
|
+
import {FaGear, FaPencil, FaTriangleExclamation} from "react-icons/fa6";
|
|
42
42
|
import RestoreConfirmationModal from "./RestoreConfirmationModal.jsx";
|
|
43
43
|
import {event_trigger, isLightColor} from "../../src/core.js";
|
|
44
44
|
import {isConditionMet} from "./DataEditor.jsx";
|
|
@@ -54,6 +54,8 @@ import TutorialsMenu from "../src/TutorialsMenu.jsx";
|
|
|
54
54
|
import {useTutorials} from "./hooks/useTutorials.jsx";
|
|
55
55
|
import PackGallery from "./PackGallery.jsx";
|
|
56
56
|
import {HiddenableCell} from "./HiddenableCell.jsx";
|
|
57
|
+
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
58
|
+
import {pagedFilterToMongoConds} from "./filter.js";
|
|
57
59
|
|
|
58
60
|
// Ajoutez cette constante pour la clé de sessionStorage
|
|
59
61
|
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
|
@@ -71,19 +73,29 @@ const Header = ({
|
|
|
71
73
|
filterValues
|
|
72
74
|
}) => {
|
|
73
75
|
|
|
76
|
+
const {t} = useTranslation()
|
|
74
77
|
const { me } = useAuthContext()
|
|
75
78
|
const {
|
|
76
79
|
models,
|
|
77
80
|
countByModel,
|
|
78
81
|
selectedModel,
|
|
79
82
|
setPagedFilters,
|
|
83
|
+
pagedFilters,
|
|
80
84
|
page
|
|
81
85
|
} = useModelContext();
|
|
82
86
|
let totalCol = 0;
|
|
87
|
+
|
|
88
|
+
const [advancedFilterVisible, setAdvancedFilterVisible] = useState(false);
|
|
89
|
+
|
|
90
|
+
const handleAdvancedFilter = () => {
|
|
91
|
+
setAdvancedFilterVisible(true);
|
|
92
|
+
}
|
|
83
93
|
return <><tr className={reversed ? ' reversed' : ''}>
|
|
84
94
|
<th className={"mini"}>
|
|
85
95
|
<div className="flex flex-row">
|
|
96
|
+
|
|
86
97
|
<Button onClick={handleFilter} className={filterActive ? ' active' : ''}><FaFilter/></Button>
|
|
98
|
+
{filterActive && <Button onClick={() => handleAdvancedFilter()}><FaWrench /></Button>}
|
|
87
99
|
<CheckboxField checked={checkedItems.length === data.length} onChange={e => {
|
|
88
100
|
if (checkedItems.length === data.length) {
|
|
89
101
|
setCheckedItems([]);
|
|
@@ -91,6 +103,27 @@ const Header = ({
|
|
|
91
103
|
setCheckedItems(data);
|
|
92
104
|
}
|
|
93
105
|
}}/>
|
|
106
|
+
|
|
107
|
+
<DialogProvider>
|
|
108
|
+
{advancedFilterVisible && (
|
|
109
|
+
<Dialog title={t("datatable.advancedFilter.title")} isClosable={true} onClose={() => setAdvancedFilterVisible(false)}>
|
|
110
|
+
<h3>Edit filter</h3>
|
|
111
|
+
<div className="msg">
|
|
112
|
+
<Trans i18nKey={"datatable.advancedFilter.desc"}>{t("datatable.advancedFilter.desc")}</Trans>
|
|
113
|
+
</div>
|
|
114
|
+
<ConditionBuilder onChange={(c) => {
|
|
115
|
+
setPagedFilters(pagedFilters => ({
|
|
116
|
+
...pagedFilters,
|
|
117
|
+
[model.name]: c || pagedFilters[model.name] || {} }));
|
|
118
|
+
}} initialValue={{ $and: pagedFilterToMongoConds(pagedFilters, model)}} models={models} model={model.name} checkedItems={checkedItems} setCheckedItems={setCheckedItems} data={data} filterActive={filterActive} onChangeFilterValue={onChangeFilterValue} setFilterValues={setFilterValues}/>
|
|
119
|
+
<Button onClick={() =>{
|
|
120
|
+
setPagedFilters(pagedFilters => ({
|
|
121
|
+
...pagedFilters,
|
|
122
|
+
[model.name]: {}}));
|
|
123
|
+
}}><Trans i18nKey={"btns.reset"}>Reset</Trans></Button>
|
|
124
|
+
</Dialog>
|
|
125
|
+
)}
|
|
126
|
+
</DialogProvider>
|
|
94
127
|
</div>
|
|
95
128
|
</th>
|
|
96
129
|
{model.fields.map(field => {
|
|
@@ -114,11 +147,12 @@ const Header = ({
|
|
|
114
147
|
onChangeFilterValue={onChangeFilterValue}/>;
|
|
115
148
|
})}
|
|
116
149
|
<th><Trans i18nKey="actions">Actions</Trans>
|
|
117
|
-
{
|
|
150
|
+
{Object.keys(pagedFilters[model.name] || {})
|
|
151
|
+
.filter(f=> Object.keys(pagedFilters[model.name]?.[f] || {}).length > 0).length > 0 && <div>
|
|
118
152
|
<button onClick={() => {
|
|
119
153
|
setFilterValues({});
|
|
120
154
|
setPagedFilters(pagedFilters => ({...pagedFilters, [model.name]: {}}));
|
|
121
|
-
}}
|
|
155
|
+
}}><FaEraser />
|
|
122
156
|
</button>
|
|
123
157
|
</div>}</th>
|
|
124
158
|
</tr>
|
|
@@ -137,6 +171,7 @@ export function DataTable({
|
|
|
137
171
|
setCheckedItems,
|
|
138
172
|
onEdit,
|
|
139
173
|
onAddData,
|
|
174
|
+
onDuplicateData,
|
|
140
175
|
onDelete,
|
|
141
176
|
onShowAPI,
|
|
142
177
|
filterValues,
|
|
@@ -229,7 +264,7 @@ export function DataTable({
|
|
|
229
264
|
|
|
230
265
|
const onChangeFilterValue = (field, value, tr) => {
|
|
231
266
|
setPagedFilters(pagedFilters => ({
|
|
232
|
-
[model.name]: {...pagedFilters[model.name] || {}, [field.name]: value || undefined}
|
|
267
|
+
...pagedFilters, [model.name]: {...pagedFilters[model.name] || {}, [field.name]: value || pagedFilters[model.name]?.[field.name] || undefined}
|
|
233
268
|
}));
|
|
234
269
|
queryClient.invalidateQueries(['api/data', model.name, 'page', page, elementsPerPage, pagedFilters[model.name], pagedSort[model.name]]);
|
|
235
270
|
}
|
|
@@ -344,6 +379,22 @@ export function DataTable({
|
|
|
344
379
|
if (!model)
|
|
345
380
|
return <></>;
|
|
346
381
|
|
|
382
|
+
// NOUVEAU : La fonction qui gère la duplication
|
|
383
|
+
const handleDuplicate = (originalData) => {
|
|
384
|
+
// 1. Créer une copie superficielle des données de la ligne
|
|
385
|
+
const dataToDuplicate = { ...originalData };
|
|
386
|
+
|
|
387
|
+
// 2. TRÈS IMPORTANT : Supprimer les champs qui ne doivent pas être copiés.
|
|
388
|
+
// - L'_id doit être supprimé pour que le système sache qu'il s'agit d'une NOUVELLE entrée.
|
|
389
|
+
// - Le _hash sera recalculé par le backend.
|
|
390
|
+
// - Les dates de création/mise à jour seront gérées par le backend.
|
|
391
|
+
delete dataToDuplicate._id;
|
|
392
|
+
delete dataToDuplicate._hash;
|
|
393
|
+
delete dataToDuplicate.createdAt; // si ce champ existe
|
|
394
|
+
delete dataToDuplicate.updatedAt; // si ce champ existe
|
|
395
|
+
|
|
396
|
+
onDuplicateData(dataToDuplicate);
|
|
397
|
+
};
|
|
347
398
|
return (
|
|
348
399
|
<div className={`datatable${filterActive ? ' filter-active' : ''}`}>
|
|
349
400
|
{<div className="flex actions flex-left">
|
|
@@ -356,7 +407,7 @@ export function DataTable({
|
|
|
356
407
|
<Button className="tourStep-import-datapack" onClick={handleShowPacks} title={t("btns.addPack")}><FaPlus/><Trans
|
|
357
408
|
i18nKey="btns.addPack">Packs...</Trans></Button>
|
|
358
409
|
|
|
359
|
-
<Button className={/^demo[0-9]{1,2}$/.test(me.username) ? "btn-primary" : "btn"} onClick={handleShowTutorialMenu} title={t("btns.showTutos")}><FaPlus/><Trans
|
|
410
|
+
<Button className={"tourStep-tutorials " + (/^demo[0-9]{1,2}$/.test(me.username) ? "btn-primary" : "btn")} onClick={handleShowTutorialMenu} title={t("btns.showTutos")}><FaPlus/><Trans
|
|
360
411
|
i18nKey="btns.showTutos">Tutoriels</Trans></Button>
|
|
361
412
|
{!/^demo[0-9]{1,2}$/.test(me.username) && (<Button onClick={handleBackup} title={t("btns.backup")}><FaDatabase/><Trans
|
|
362
413
|
i18nKey="btns.backup">Backup</Trans></Button>)}
|
|
@@ -373,6 +424,7 @@ export function DataTable({
|
|
|
373
424
|
</div>}
|
|
374
425
|
<div className="table-wrapper">
|
|
375
426
|
<Tooltip id={"tooltipFile"} clickable={true} />
|
|
427
|
+
<Tooltip id={"tooltipActions"} />
|
|
376
428
|
<Lightbox
|
|
377
429
|
inline={{
|
|
378
430
|
style: { width: "100%", maxWidth: "900px", aspectRatio: "3 / 2" },
|
|
@@ -591,8 +643,19 @@ export function DataTable({
|
|
|
591
643
|
{hiddenable(item[field.name])}</td>;
|
|
592
644
|
})}
|
|
593
645
|
<td>
|
|
594
|
-
<button
|
|
595
|
-
|
|
646
|
+
<button data-tooltip-id="tooltipActions"
|
|
647
|
+
data-tooltip-content={t('btns.edit', 'Modifier')}
|
|
648
|
+
onClick={() => handleEdit(item)}><FaPencil/></button>
|
|
649
|
+
<button
|
|
650
|
+
onClick={() => handleDuplicate(item)}
|
|
651
|
+
data-tooltip-id="tooltipActions"
|
|
652
|
+
data-tooltip-content={t('btns.duplicate', 'Dupliquer')}
|
|
653
|
+
>
|
|
654
|
+
<FaCopy/>
|
|
655
|
+
</button>
|
|
656
|
+
<button data-tooltip-id="tooltipActions"
|
|
657
|
+
data-tooltip-content={t('btns.delete', 'Supprimer')}
|
|
658
|
+
onClick={() => handleDelete(item)}><FaTrash/></button>
|
|
596
659
|
</td>
|
|
597
660
|
</tr>
|
|
598
661
|
</DialogProvider></>
|
|
@@ -600,7 +663,11 @@ export function DataTable({
|
|
|
600
663
|
</tbody>
|
|
601
664
|
|
|
602
665
|
<tfoot>
|
|
603
|
-
{data.length > 10 && (<Header reversed={true} model={model} setCheckedItems={setCheckedItems}
|
|
666
|
+
{data.length > 10 && (<Header reversed={true} model={model} setCheckedItems={setCheckedItems}
|
|
667
|
+
filterValues={filterValues} data={data}
|
|
668
|
+
setFilterValues={setFilterValues}
|
|
669
|
+
onChangeFilterValue={onChangeFilterValue}
|
|
670
|
+
checkedItems={checkedItems} filterActive={filterActive} handleFilter={handleFilter}/>)}
|
|
604
671
|
</tfoot>
|
|
605
672
|
|
|
606
673
|
</table>)}
|
package/client/src/Dialog.scss
CHANGED
|
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
4
4
|
import './TourSpotlight.scss';
|
|
5
5
|
import { useUI } from "./contexts/UIContext.jsx";
|
|
6
6
|
import useLocalStorage from "./hooks/useLocalStorage.js";
|
|
7
|
+
import {waitForElement} from "./core/dom.js";
|
|
7
8
|
|
|
8
9
|
function debounce(func, wait) {
|
|
9
10
|
let timeout;
|
|
@@ -39,27 +40,30 @@ function TourSpotlight({ name, steps = [], isOpen, onComplete, onClose, initialS
|
|
|
39
40
|
}
|
|
40
41
|
}, [isOpen, initialStepIndex]);
|
|
41
42
|
|
|
42
|
-
const calculateRectAndScroll = useCallback(() => {
|
|
43
|
+
const calculateRectAndScroll = useCallback( () => {
|
|
43
44
|
if (!isOpen || !currentStep?.selector) {
|
|
44
45
|
setTargetRect(null);
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
waitForElement(currentStep.selector).then(element => {
|
|
50
|
+
if (element) {
|
|
51
|
+
const rect = element.getBoundingClientRect();
|
|
52
|
+
if (!targetRect || rect.top !== targetRect.top || rect.left !== targetRect.left || rect.width !== targetRect.width || rect.height !== targetRect.height) {
|
|
53
|
+
setTargetRect(rect);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.warn(`[TourSpotlight] Élément cible non trouvé : ${currentStep.selector}`);
|
|
57
|
+
setTargetRect(null);
|
|
53
58
|
}
|
|
54
|
-
}
|
|
55
|
-
console.warn(`[TourSpotlight] Élément cible non trouvé : ${currentStep.selector}`);
|
|
56
|
-
setTargetRect(null);
|
|
57
|
-
}
|
|
59
|
+
});
|
|
58
60
|
}, [isOpen, currentStep?.selector, targetRect]);
|
|
59
61
|
|
|
60
62
|
useEffect(() => {
|
|
61
63
|
if (!isOpen || !name) return;
|
|
62
64
|
|
|
65
|
+
|
|
66
|
+
console.log({currentStep})
|
|
63
67
|
if (!currentStep?.selector) {
|
|
64
68
|
console.warn(`[TourSpotlight] Élément cible non défini pour le tour ${name}`);
|
|
65
69
|
return;
|
|
@@ -35,7 +35,6 @@ $color-completed-accent: #28a745; // Vert plus vif
|
|
|
35
35
|
.tutorial-item {
|
|
36
36
|
background-color: $color-item-bg;
|
|
37
37
|
border: 1px solid $color-border;
|
|
38
|
-
z-index: -1;
|
|
39
38
|
border-radius: 12px; // Coins plus arrondis pour un look moderne
|
|
40
39
|
box-shadow: 0 4px 12px $color-shadow;
|
|
41
40
|
padding: 1.5rem;
|
package/client/src/constants.js
CHANGED
|
@@ -53,7 +53,7 @@ export const MONGO_OPERATORS = {
|
|
|
53
53
|
export const profiles = {
|
|
54
54
|
'personal': ['contact', 'location', 'imageGallery', 'budget', 'currency', 'taxonomy'], // budget,
|
|
55
55
|
'developer': ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role'],
|
|
56
|
-
'company': ['alert','request','location', 'campaign', 'order', 'currency', 'product', 'cart', 'cartItem', 'invoice', '
|
|
56
|
+
'company': ['alert','request','location', 'campaign', 'order', 'currency', 'product', 'cart', 'cartItem', 'invoice', 'message', 'user', 'role', 'permission', 'token','translation', 'lang', 'webpage', 'content', 'taxonomy', 'contact', 'resource', 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'kpi', 'dashboard'],
|
|
57
57
|
'engineer': ['alert','endpoint','request','dashboard', 'kpi', 'user', 'role', 'token', 'permission', 'workflow', 'workflowRun', 'workflowStep', "channel", "message", 'workflowAction', 'workflowTrigger']
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -6,13 +6,14 @@ import {useAuthContext} from "./AuthContext.jsx";
|
|
|
6
6
|
import {getUserHash, getUserId} from "../../../src/data.js";
|
|
7
7
|
import {useTranslation} from "react-i18next";
|
|
8
8
|
import useLocalStorage from "../hooks/useLocalStorage.js";
|
|
9
|
+
import {pagedFilterToMongoConds} from "../filter.js";
|
|
9
10
|
|
|
10
11
|
const ModelContext = createContext(null);
|
|
11
12
|
|
|
12
13
|
export const ModelProvider = ({ children }) => {
|
|
13
14
|
const [models, setModels] = useState([]);
|
|
14
15
|
const [datasToLoad, setDatasToLoad] = useState([]);
|
|
15
|
-
const [pagedFilters,setPagedFilters] =
|
|
16
|
+
const [pagedFilters,setPagedFilters] = useLocalStorage('paged_filters', {});
|
|
16
17
|
const [pagedDepth,setPagedDepth] = useState({});
|
|
17
18
|
const [filteredDatasToLoad, setFilteredDatasToLoad] = useState([]);
|
|
18
19
|
const [onSuccessCallbacks, setOnSuccessCallbacks] = useState({});
|
|
@@ -188,13 +189,7 @@ export const ModelProvider = ({ children }) => {
|
|
|
188
189
|
if( sortParam.length )
|
|
189
190
|
params.append("sort", sortParam.join(','));
|
|
190
191
|
|
|
191
|
-
const c =
|
|
192
|
-
Object.keys(pagedFilters[model.name] || {}).filter(f=>typeof(pagedFilters[model.name][f]) === 'object' && Object.keys(pagedFilters[model.name][f]).length > 0).forEach(obj =>{
|
|
193
|
-
const f = model.fields.find(f=>f.name===obj);
|
|
194
|
-
if(f !== null && f !== undefined){
|
|
195
|
-
c.push(pagedFilters[model.name][obj]);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
192
|
+
const c = pagedFilterToMongoConds(pagedFilters, model)
|
|
198
193
|
const filter= JSON.stringify({filter:{$and:c}});
|
|
199
194
|
|
|
200
195
|
return fetch(`/api/data/search?${params.toString()}`, { signal, method: 'POST', body: filter, headers: { "Content-Type": "application/json"}})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waits for an element to appear in the DOM.
|
|
3
|
+
* @param {string} selector - The CSS selector of the element.
|
|
4
|
+
* @param {number} timeout - The maximum time to wait in milliseconds.
|
|
5
|
+
* @returns {Promise<Element>} A promise that resolves with the element or rejects on timeout.
|
|
6
|
+
*/
|
|
7
|
+
export function waitForElement(selector, timeout = 2000) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const intervalTime = 100; // Check every 100ms
|
|
10
|
+
let elapsedTime = 0;
|
|
11
|
+
|
|
12
|
+
const interval = setInterval(() => {
|
|
13
|
+
const element = document.querySelector(selector);
|
|
14
|
+
if (element) {
|
|
15
|
+
clearInterval(interval);
|
|
16
|
+
resolve(element);
|
|
17
|
+
} else {
|
|
18
|
+
elapsedTime += intervalTime;
|
|
19
|
+
if (elapsedTime >= timeout) {
|
|
20
|
+
clearInterval(interval);
|
|
21
|
+
reject(new Error(`Element "${selector}" not found after ${timeout}ms`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}, intervalTime);
|
|
25
|
+
});
|
|
26
|
+
}
|
package/client/src/filter.js
CHANGED
|
@@ -7,6 +7,12 @@ export const MONGO_CALC_OPERATORS = {
|
|
|
7
7
|
multi: false,
|
|
8
8
|
isFindOperator: true // Nouvelle propriété pour identifier les opérateurs $find
|
|
9
9
|
},
|
|
10
|
+
'$regexMatch': {
|
|
11
|
+
label: 'Find by regex',
|
|
12
|
+
description: 'Find a string matching a regular expression (Ecmascript)',
|
|
13
|
+
args: 2, // input et regex
|
|
14
|
+
specialStructure: true // Indique une structure spéciale
|
|
15
|
+
},
|
|
10
16
|
$and: { label: 'Et (and)', description: 'Renvoie vrai si toutes les conditions sont vérifiées', args: 1, multi: true },
|
|
11
17
|
$or: { label: 'Ou (or)', description: 'Renvoie vrai si au moins une des conditions est vérifiée.', args: 1, multi: true },
|
|
12
18
|
$not: { label: 'Non (not)', description: 'Inverse la condition (ex: non égal à).', args: 1, multi: true },
|
|
@@ -57,7 +63,7 @@ export const MONGO_CALC_OPERATORS = {
|
|
|
57
63
|
$toBool: { label: 'toBool', multi: false, converter: true },
|
|
58
64
|
$toString: { label: 'toString', multi: false, converter: true },
|
|
59
65
|
$toInt: { label: 'toInt', multi: false, converter: true },
|
|
60
|
-
$toDouble: { label: 'toDouble', multi: false, converter: true }
|
|
66
|
+
$toDouble: { label: 'toDouble', multi: false, converter: true }
|
|
61
67
|
};
|
|
62
68
|
|
|
63
69
|
export const convertInputValue = (value) => {
|
|
@@ -219,4 +225,37 @@ export const buildRootFromExpr = (query) => {
|
|
|
219
225
|
if (parsedRoot.$and || parsedRoot.$or) return parsedRoot;
|
|
220
226
|
|
|
221
227
|
return { $and: [parsedRoot] };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const pagedFilterToMongoConds = (pagedFilters, model) => {
|
|
231
|
+
const modelFilter = pagedFilters?.[model.name] || {};
|
|
232
|
+
const filterKeys = Object.keys(modelFilter);
|
|
233
|
+
|
|
234
|
+
// Si le filtre est vide, on retourne un tableau vide.
|
|
235
|
+
if (filterKeys.length === 0) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Détecte si c'est un filtre avancé (contient des opérateurs logiques de haut niveau comme $and, $or, etc.)
|
|
240
|
+
const isAdvancedFilter = filterKeys.some(key => key.startsWith('$'));
|
|
241
|
+
|
|
242
|
+
if (isAdvancedFilter) {
|
|
243
|
+
// C'est un filtre avancé du ConditionBuilder.
|
|
244
|
+
// Il est déjà une condition MongoDB complète. On le met dans un tableau pour l'insérer dans le $and global.
|
|
245
|
+
// Si le filtre avancé est vide (ex: { $and: [] }), on le retourne quand même pour que le ConditionBuilder puisse s'initialiser correctement.
|
|
246
|
+
return [modelFilter];
|
|
247
|
+
} else {
|
|
248
|
+
// C'est un filtre simple (par colonne).
|
|
249
|
+
// On transforme { champ1: cond1, champ2: cond2 } en [{ champ1: cond1 }, { champ2: cond2 }]
|
|
250
|
+
const c = [];
|
|
251
|
+
filterKeys.forEach(fieldName => {
|
|
252
|
+
if (model.fields.some(f => f.name === fieldName)) {
|
|
253
|
+
const condition = modelFilter[fieldName];
|
|
254
|
+
if (condition && typeof condition === 'object' && Object.keys(condition).length > 0) {
|
|
255
|
+
c.push(condition);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
return c;
|
|
260
|
+
}
|
|
222
261
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": ">=18.0.0",
|
|
47
|
-
"react-query": ">=3.0.0"
|
|
47
|
+
"react-query": ">=3.0.0",
|
|
48
|
+
"express": "^5.1.0"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
51
|
"@langchain/core": "^0.3.66",
|
|
@@ -60,15 +61,13 @@
|
|
|
60
61
|
"cookie-parser": "^1.4.7",
|
|
61
62
|
"cronstrue": "^3.2.0",
|
|
62
63
|
"csv-parse": "^6.1.0",
|
|
63
|
-
"data-primals-engine": "^1.0.
|
|
64
|
+
"data-primals-engine": "^1.0.14",
|
|
64
65
|
"date-fns": "^4.1.0",
|
|
65
|
-
"express": "^5.1.0",
|
|
66
66
|
"express-csrf-double-submit-cookie": "^2.0.0",
|
|
67
67
|
"express-formidable": "^1.2.0",
|
|
68
68
|
"express-mongo-sanitize": "^2.2.0",
|
|
69
69
|
"express-rate-limit": "^8.0.1",
|
|
70
70
|
"express-session": "^1.18.2",
|
|
71
|
-
"i18next": "^25.3.2",
|
|
72
71
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
73
72
|
"juice": "^11.0.1",
|
|
74
73
|
"mathjs": "^14.6.0",
|