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.
@@ -51,8 +51,7 @@
51
51
  "date-fns": "^4.1.0",
52
52
  "express-rate-limit": "^7.5.1",
53
53
  "lowlight": "^3.3.0",
54
- "uniqid": "^5.4.0",
55
- "data-primals-engine": "^1.0.11"
54
+ "uniqid": "^5.4.0"
56
55
  },
57
56
  "devDependencies": {
58
57
  "@eslint/js": "^9.17.0",
@@ -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'+getRandom(1, 99);
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' + getRandom(1, 99);
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'+getRandom(0, 99);
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
- datapacks: [
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('btns.editModel', `Edit ${model.name}`) : `${t('add_in_model')} ${t(`model_${model.name}`, model.name)}`;
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
- const t = model ? [...model.fields].reduce((acc, field) => {
483
- if( field.type === "relation"){
484
- acc[field.name] = field.multiple ? [] : null;
485
- }else {
486
- acc[field.name] = getDefaultForType(field);
487
- }
488
- return acc;
489
- }, {}) : [];
490
- setFormData(t)
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.datapacks;
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é
@@ -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
- {filterActive && <div>
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
- }}>x
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 onClick={() => handleEdit(item)}><FaPencil/></button>
595
- <button onClick={() => handleDelete(item)}><FaTrash/></button>
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} filterValues={filterValues} data={data} setFilterValues={setFilterValues} onChangeFilterValue={onChangeFilterValue} checkedItems={checkedItems} filterActive={filterActive} handleFilter={handleFilter}/>)}
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>)}
@@ -64,6 +64,7 @@
64
64
  &.btn-close {
65
65
  position: absolute;
66
66
  right: 0px;
67
+ z-index: 100;
67
68
  }
68
69
  }
69
70
  }
@@ -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
- if (
263
- value &&
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
- current[pathArray[pathArray.length - 1]] = value;
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
- const element = document.querySelector(currentStep.selector);
49
- if (element) {
50
- const rect = element.getBoundingClientRect();
51
- if (!targetRect || rect.top !== targetRect.top || rect.left !== targetRect.left || rect.width !== targetRect.width || rect.height !== targetRect.height) {
52
- setTargetRect(rect);
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
- } else {
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;
@@ -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', 'messaging', 'user', 'role', 'permission', 'token','translation', 'lang', 'webpage', 'content', 'taxonomy', 'contact', 'resource', 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'kpi', 'dashboard'],
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] = useState({});
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
+ }