data-primals-engine 1.1.4 → 1.1.6
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/.github/workflows/node.js.yml +3 -1
- package/README.md +9 -1
- package/client/package-lock.json +11580 -11664
- package/client/package.json +1 -2
- package/client/src/App.jsx +13 -8
- package/client/src/ConditionBuilder.scss +24 -0
- package/client/src/ConditionBuilder2.jsx +56 -0
- package/client/src/DataEditor.jsx +4 -2
- package/client/src/DataLayout.jsx +20 -11
- package/client/src/DataTable.jsx +77 -10
- package/client/src/Dialog.scss +1 -0
- package/client/src/Field.jsx +3 -4
- package/client/src/FlexBuilder.jsx +3 -1
- 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 +15 -1
- package/src/core.js +52 -3
- package/src/engine.js +42 -16
- package/src/i18n.js +75 -2
- package/src/migrate.js +2 -3
- package/src/modules/data.js +15 -4
- package/src/modules/mongodb.js +0 -9
- package/src/packs.js +4 -3
- package/src/migrations/20240522143000-content.js +0 -20
package/client/package.json
CHANGED
package/client/src/App.jsx
CHANGED
|
@@ -53,10 +53,10 @@ import {Helmet} from "react-helmet";
|
|
|
53
53
|
import {useCookies, CookiesProvider} from "react-cookie";
|
|
54
54
|
|
|
55
55
|
import { translations as allTranslations} from "../../src/i18n.js";
|
|
56
|
-
import {getRandom} from "../../src/core.js";
|
|
56
|
+
import {getBrowserRandom, 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
|
|
|
@@ -254,7 +254,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
254
254
|
const menu=<>{(!me) && !loc.pathname.startsWith("/user/demo") && (<Button className={"btn btn-nav btn-ellipsis btn-primary btn-big"} onClick={() => {
|
|
255
255
|
gtag("event", "nav to demo");
|
|
256
256
|
setCurrentProfile(null);
|
|
257
|
-
const username = 'demo'+
|
|
257
|
+
const username = 'demo'+getBrowserRandom(1, 99);
|
|
258
258
|
setMe({username});
|
|
259
259
|
setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
260
260
|
nav('/user/'+username, { state: { startTour: true } });
|
|
@@ -276,7 +276,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
276
276
|
|
|
277
277
|
useEffect(() => {
|
|
278
278
|
if( !cookies.username) {
|
|
279
|
-
const username ='demo' +
|
|
279
|
+
const username ='demo' + getBrowserRandom(1, 99);
|
|
280
280
|
setCookie("username", username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
281
281
|
onAuthenticated({username}, true);
|
|
282
282
|
}
|
|
@@ -360,7 +360,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
360
360
|
const handleGenerateClick = (p) => {
|
|
361
361
|
gtag('event', 'homepage model generation');
|
|
362
362
|
setCurrentTour(null);
|
|
363
|
-
const username = 'demo'+
|
|
363
|
+
const username = 'demo'+getBrowserRandom(0, 99);
|
|
364
364
|
setMe({username});
|
|
365
365
|
|
|
366
366
|
setPromptResult(null);
|
|
@@ -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 (
|
|
@@ -8,7 +8,7 @@ import RelationField from "./RelationField.jsx";
|
|
|
8
8
|
import {
|
|
9
9
|
CheckboxField,
|
|
10
10
|
CodeField,
|
|
11
|
-
ColorField,
|
|
11
|
+
ColorField, EmailField,
|
|
12
12
|
EnumField,
|
|
13
13
|
FileField, ModelField, NumberField,
|
|
14
14
|
PhoneField,
|
|
@@ -470,6 +470,8 @@ export const DataEditor = forwardRef(function MyDataEditor({
|
|
|
470
470
|
return <FileField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} maxSize={field.maxSize} mimeTypes={field.mimeTypes} value={value} onChange={(file) => handleChange({ name: name, value: file ? file.name : null })} />
|
|
471
471
|
case 'color':
|
|
472
472
|
return <ColorField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} value={value} onChange={handleChange} />
|
|
473
|
+
case 'email':
|
|
474
|
+
return <EmailField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
|
|
473
475
|
default:
|
|
474
476
|
return <TextField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
|
|
475
477
|
}
|
|
@@ -508,7 +510,7 @@ export const DataEditor = forwardRef(function MyDataEditor({
|
|
|
508
510
|
return <p>Sélectionnez un modèle pour afficher le formulaire.</p>;
|
|
509
511
|
}
|
|
510
512
|
|
|
511
|
-
const title = record ? t('
|
|
513
|
+
const title = record ? t('editData', 'Edit {{0}}', [model.name]) : `${t('add_in_model')} ${t(`model_${model.name}`, model.name)}`;
|
|
512
514
|
|
|
513
515
|
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
516
|
|
|
@@ -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
package/client/src/Field.jsx
CHANGED
|
@@ -35,6 +35,7 @@ import { PhoneInput } from 'react-international-phone';
|
|
|
35
35
|
import 'react-international-phone/style.css';
|
|
36
36
|
import CodeMirror, {basicSetup} from "@uiw/react-codemirror";
|
|
37
37
|
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
38
|
+
import {maxStringLength} from "data-primals-engine/constants";
|
|
38
39
|
|
|
39
40
|
export const Form = ({
|
|
40
41
|
name,
|
|
@@ -259,10 +260,8 @@ const EmailField = forwardRef(
|
|
|
259
260
|
if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
|
|
260
261
|
errs.push("Value length must be <= to " + maxlength);
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
!value.match(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)
|
|
265
|
-
) {
|
|
263
|
+
|
|
264
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
266
265
|
errs.push("Invalid email");
|
|
267
266
|
}
|
|
268
267
|
setErrors(errs);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
clearMappingsRecursive
|
|
20
20
|
} from './FlexTreeUtils.js';
|
|
21
21
|
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
22
|
+
import {safeAssignObject} from "data-primals-engine/core";
|
|
22
23
|
|
|
23
24
|
const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
|
|
24
25
|
const { me: user } = useAuthContext();
|
|
@@ -104,7 +105,8 @@ const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], l
|
|
|
104
105
|
current[pathArray[i]] = current[pathArray[i]] || {};
|
|
105
106
|
current = current[pathArray[i]];
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
const key = pathArray[pathArray.length - 1];
|
|
109
|
+
safeAssignObject(current, key, value);
|
|
108
110
|
return newNode;
|
|
109
111
|
};
|
|
110
112
|
|
|
@@ -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
|
+
}
|