data-primals-engine 1.6.0 → 1.6.2-rc1

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.
@@ -1,6 +1,6 @@
1
1
  import React, {forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
2
2
 
3
- import { FaUndo, FaRedo } from 'react-icons/fa';
3
+ import {FaUndo, FaRedo, FaSitemap} from 'react-icons/fa';
4
4
  import "./App.scss";
5
5
  import {useMutation, useQuery, useQueryClient} from "react-query";
6
6
  import ModelCreator from "./ModelCreator.jsx";
@@ -11,7 +11,7 @@ import {Event} from "../../src/events.js";
11
11
 
12
12
  import {
13
13
  FaBook,
14
- FaBoxOpen, FaDatabase,
14
+ FaBoxOpen, FaDatabase, FaProjectDiagram,
15
15
  FaEye, FaFileImport,
16
16
  FaFilter, FaInfo, FaPlus,
17
17
  } from "react-icons/fa";
@@ -46,6 +46,7 @@ import {AssistantChat, NotificationList} from "../index.js";
46
46
  import { useCommand } from './contexts/CommandContext.jsx';
47
47
 
48
48
  import "./DataLayout.scss"
49
+ import WorkflowEditor from "./WorkflowEditor.jsx";
49
50
 
50
51
  const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
51
52
  <div className="p-4 border border-dashed rounded-md mt-4 text-center bg-gray-50">
@@ -57,12 +58,52 @@ const NotConfiguredPlaceholder = ({ type, onConfigure }) => (
57
58
  </div>
58
59
  );
59
60
 
61
+ const WorkflowSelectorModal = ({ onClose, onSelectWorkflow }) => {
62
+ const { t } = useTranslation();
63
+ const { searchData } = useModelContext();
64
+ const { me } = useAuthContext();
65
+ const { data: activeWorkflows, isLoading } = useQuery(
66
+ 'activeWorkflowsList',
67
+ () => fetch('/api/data/search?_user='+me.username, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({
71
+ model: 'workflow',
72
+ sort: 'name:asc',limit: 5
73
+ })
74
+ }).then(res => res.json()),
75
+ {
76
+ select: (data) => data?.data || []
77
+ }
78
+ );
79
+
80
+ return (
81
+ <Dialog title={t('workflow.select.title', 'Sélectionner un workflow')} isClosable={true} onClose={onClose}>
82
+ {isLoading && <p>{t('loading', 'Chargement...')}</p>}
83
+ {!isLoading && (!activeWorkflows || activeWorkflows.length === 0) && (
84
+ <p>{t('workflow.select.noActive', 'Aucun workflow actif trouvé.')}</p>
85
+ )}
86
+ <ul className="workflow-selector-list">
87
+ {activeWorkflows?.map(wf => (
88
+ <li key={wf._id} onClick={() => onSelectWorkflow(wf._id)}>
89
+ <FaSitemap />
90
+ <span className="font-bold">{wf.name.value}</span>
91
+ <span className="text-sm text-gray-500 ml-2">{wf.description}</span>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ </Dialog>
96
+ );
97
+ };
98
+
60
99
  function DataLayout({refreshUI}) {
61
100
  const [ searchParams, setSearchParams ] = useSearchParams();
62
101
  const [viewSettings, setViewSettings] = useLocalStorage('viewSettings', {});
63
102
 
64
103
  const [isCalendarModalOpen, setCalendarModalOpen] = useState(false);
65
104
  const [isKanbanModalOpen, setKanbanModalOpen] = useState(false);
105
+ const [isWorkflowListModalOpen, setWorkflowListModalOpen] = useState(false);
106
+
66
107
  const [showPackGallery, setShowPackGallery] = useState(false);
67
108
  const [checkedItems, setCheckedItems] = useState([])
68
109
 
@@ -115,6 +156,11 @@ function DataLayout({refreshUI}) {
115
156
  const nav = useNavigate();
116
157
  const mod = searchParams.get('model');
117
158
  const loc = useLocation();
159
+
160
+
161
+ // --- AJOUT : Récupérer le paramètre de l'URL pour l'édition de workflow ---
162
+ const workflowToEditId = searchParams.get('edit-workflow');
163
+
118
164
  useEffect(() =>{
119
165
  if (selectedModel?.name) {
120
166
  nav('/user/' + getUserHash(me) + '/?model=' + selectedModel?.name);
@@ -129,13 +175,6 @@ function DataLayout({refreshUI}) {
129
175
  setEditionMode(true);
130
176
  }, []);
131
177
 
132
- useEffect(() =>{
133
- const m = models.find(f => f.name === mod);
134
- setSelectedModel(m);
135
- setEditionMode(!m);
136
- }, [mod, models, searchParams])
137
-
138
-
139
178
  // La vue courante est dérivée du modèle sélectionné et des préférences stockées.
140
179
  const currentView = useMemo(() => {
141
180
  if (!selectedModel) return 'table';
@@ -247,6 +286,10 @@ function DataLayout({refreshUI}) {
247
286
  return configuredViews.kanban
248
287
  ? <KanbanView settings={currentModelViewSettings.kanban} model={selectedModel} />
249
288
  : <NotConfiguredPlaceholder type="kanban" onConfigure={() => setKanbanModalOpen(true)} />;
289
+ case 'workflow':
290
+ return workflowToEditId
291
+ ? <WorkflowEditor workflowId={workflowToEditId} />
292
+ : <NotConfiguredPlaceholder type="kanban" onConfigure={() => setKanbanModalOpen(true)} />;
250
293
  case 'table':
251
294
  default:
252
295
  // Le DataTable existant est retourné par défaut
@@ -281,7 +324,7 @@ function DataLayout({refreshUI}) {
281
324
  setRelationFilters({});
282
325
  setCheckedItems([])
283
326
  setFilterValues({});
284
- setEditionMode(false);
327
+
285
328
  if (!model) {
286
329
  setSelectedModel(null);
287
330
  return;
@@ -289,7 +332,9 @@ function DataLayout({refreshUI}) {
289
332
 
290
333
  // Maintient la vue actuelle si elle est configurée pour le nouveau modèle, sinon revient à la vue "table"
291
334
  const modelSettings = viewSettings[model.name] || {};
292
- if (currentView === 'calendar' && (!modelSettings.calendar?.titleField || !modelSettings.calendar?.startField || !modelSettings.calendar?.endField)) {
335
+ if( currentView === 'workflow') {
336
+ setCurrentView('table')
337
+ }else if (currentView === 'calendar' && (!modelSettings.calendar?.titleField || !modelSettings.calendar?.startField || !modelSettings.calendar?.endField)) {
293
338
  setCurrentView('table');
294
339
  } else if (currentView === 'kanban' && !modelSettings.kanban?.groupByField) {
295
340
  setCurrentView('table');
@@ -586,7 +631,7 @@ function DataLayout({refreshUI}) {
586
631
  isOpen={isTourOpen}
587
632
  onClose={closeTour}
588
633
  />)}</>
589
- <div className="flex actions">
634
+ <div className="flex mg-1">
590
635
 
591
636
  {<ViewSwitcher
592
637
  currentView={currentView}
@@ -594,6 +639,9 @@ function DataLayout({refreshUI}) {
594
639
  configuredViews={configuredViews}
595
640
  onConfigureView={handleConfigureCurrentView}
596
641
  />}
642
+ <Button onClick={() => setWorkflowListModalOpen(true)} className={currentView === 'workflow' ? 'active' : ''} title={t('views.workflow', 'Workflow')}>
643
+ <FaProjectDiagram />
644
+ </Button>
597
645
  <div className="flex items-center gap-1 p-1 bg-gray-200 rounded-md">
598
646
  <Button onClick={undo} disabled={!canUndo} title={t('btns.undo', 'Annuler')}>
599
647
  <FaUndo />
@@ -604,14 +652,14 @@ function DataLayout({refreshUI}) {
604
652
  </div>
605
653
  <Button onClick={() => {
606
654
  setImportModalVisible(true);
607
- }} className="btn tourStep-import-model"><FaFileImport/><Trans
608
- i18nKey="btns.importModels">Modèles</Trans></Button>
655
+ }} className="btn tourStep-import-model"><FaFileImport/><span className={"no-mobile-text"}> <Trans
656
+ i18nKey="btns.importModels">Modèles</Trans></span></Button>
609
657
  <Button onClick={() => {
610
658
  setShowPackGallery(true);
611
- }} className="btn tourStep-import-pack"><FaBoxOpen/><Trans
612
- i18nKey="btns.importPacks">Packs</Trans></Button>
613
- <Button className={"tourStep-tutorials btn"} onClick={handleShowTutorialMenu} title={t("btns.showTutos")}><FaBookAtlas/><Trans
614
- i18nKey="btns.showTutos">Tutoriels</Trans></Button>
659
+ }} className="btn tourStep-import-pack"><FaBoxOpen/><span className={"no-mobile-text"}><Trans
660
+ i18nKey="btns.importPacks">Packs</Trans></span></Button>
661
+ <Button className={"tourStep-tutorials btn"} onClick={handleShowTutorialMenu} title={t("btns.showTutos")}><FaBookAtlas/><span className={"no-mobile-text"}><Trans
662
+ i18nKey="btns.showTutos">Tutoriels</Trans></span></Button>
615
663
 
616
664
  <DialogProvider>
617
665
  {tutorialDialogVisible && (
@@ -619,12 +667,24 @@ function DataLayout({refreshUI}) {
619
667
  <TutorialsMenu />
620
668
  </Dialog>
621
669
  )}
670
+ {isWorkflowListModalOpen && (
671
+ <WorkflowSelectorModal
672
+ onClose={() => setWorkflowListModalOpen(false)}
673
+ onSelectWorkflow={(id) => {
674
+ nav(`?edit-workflow=${id}`);
675
+ setEditionMode(false)
676
+ mainPartRef.current.scrollIntoView();
677
+ setCurrentView('workflow'); // Ajout pour forcer le changement de vue
678
+ setWorkflowListModalOpen(false);
679
+ }}
680
+ />
681
+ )}
622
682
  </DialogProvider>
623
683
  <Button className="btn" onClick={() => {
624
684
  setAPIInfoVisible(true);
625
685
  setDataEditorVisible(false);
626
686
  setEditionMode(false);
627
- }}><FaBook/> {t('btns.api', 'API')}</Button>
687
+ }}><FaBook/><span className={"no-mobile-text"}>{t('btns.api', 'API')}</span></Button>
628
688
  </div>
629
689
 
630
690
  <div className="datalayout flex flex-start">
@@ -655,6 +715,7 @@ function DataLayout({refreshUI}) {
655
715
  setDataEditorVisible(false);
656
716
  setAPIInfoVisible(false);
657
717
  setEditionMode(true);
718
+
658
719
  mainPartRef.current.scrollIntoView({behavior: 'smooth'});
659
720
  gtag("event", "select_content", {
660
721
  content_type: "edit_model",
@@ -684,6 +745,10 @@ function DataLayout({refreshUI}) {
684
745
  handleModelSelect(model);
685
746
  }}/>)}
686
747
 
748
+ {/* --- AJOUT : Affichage conditionnel de l'éditeur de workflow --- */}
749
+ {workflowToEditId && (
750
+ <WorkflowEditor workflowId={workflowToEditId} />
751
+ )}
687
752
  <div className="hidden-anchor" ref={mainPartRef}></div>
688
753
 
689
754
  {showDataEditor && (<DataEditor
@@ -700,7 +765,7 @@ function DataLayout({refreshUI}) {
700
765
 
701
766
 
702
767
  {selectedModel && showAPIInfo && <APIInfo/>}
703
- {selectedModel && !showAPIInfo && !generatedModels.some(g => g.name === selectedModel?.name) && (<div className="datas">
768
+ {selectedModel && !showAPIInfo && !workflowToEditId && !generatedModels.find(g => g.name === selectedModel?.name) && (<div className="datas">
704
769
 
705
770
  <h2 className={"field-bg p-2"}>{t(`model_${selectedModel?.name}`, selectedModel?.name)} <>({countByModel?.[selectedModel?.name]})</></h2>
706
771
 
@@ -1,14 +1,40 @@
1
1
 
2
2
 
3
3
  /* website*/
4
- #content .datalayout {
4
+ .datalayout {
5
5
  padding: 0;
6
6
 
7
- h1 {
8
- margin: 24px 8px;
9
- }
7
+ }
8
+
9
+ .workflow-selector-list {
10
+ list-style: none;
11
+ padding: 0;
12
+ margin: 0;
10
13
 
11
- h2 {
12
- margin: 20px 4px 0 4px;
14
+ li {
15
+ padding: 12px 15px;
16
+ border-bottom: 1px solid #eee;
17
+ cursor: pointer;
18
+ transition: background-color 0.2s ease-in-out;
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 10px;
22
+
23
+ &:hover {
24
+ background-color: #f5f5f5;
25
+ }
13
26
  }
27
+ }
28
+
29
+ .properties-panel {
30
+ position: relative; /* Nécessaire pour positionner le resizer */
31
+ }
32
+
33
+ .resizer {
34
+ width: 5px;
35
+ height: 100%;
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ cursor: col-resize;
14
40
  }
@@ -3,8 +3,8 @@ import {useModelContext} from "./contexts/ModelContext.jsx";
3
3
  import {Trans, useTranslation} from "react-i18next";
4
4
  import {useMutation, useQuery, useQueryClient} from "react-query";
5
5
  import {useAuthContext} from "./contexts/AuthContext.jsx";
6
- import React, {useEffect, useMemo, useRef, useState} from "react";
7
- import {getUserId} from "../../src/data.js";
6
+ import React, {useEffect, useMemo, useRef, useState} from "react";
7
+ import {getUserHash, getUserId} from "../../src/data.js";
8
8
  import cronstrue from 'cronstrue/i18n';
9
9
  import {Event} from "../../src/events.js";
10
10
 
@@ -43,7 +43,7 @@ import RelationValue from "./RelationValue.jsx";
43
43
  import {FaGear, FaPencil, FaTriangleExclamation} from "react-icons/fa6";
44
44
  import RestoreConfirmationModal from "./RestoreConfirmationModal.jsx";
45
45
  import {event_trigger, isLightColor} from "../../src/core.js";
46
- import {Tooltip} from "react-tooltip";
46
+ import {Tooltip} from "react-tooltip";
47
47
  import ExportDialog from "./ExportDialog.jsx";
48
48
  import Captions from "yet-another-react-lightbox/plugins/captions";
49
49
  import {Zoom} from "yet-another-react-lightbox/plugins";
@@ -59,7 +59,9 @@ import {isConditionMet} from "../../src/filter";
59
59
  import {DataImporter} from "./DataImporter.jsx";
60
60
  import {HistoryDialog} from "./HistoryDialog.jsx";
61
61
  import { useCommand } from './contexts/CommandContext.jsx';
62
- import {Config} from "../../src/config.js";
62
+ import {Config} from "../../src/config.js";
63
+ import {useNavigate} from "react-router-dom";
64
+ import {NavLink} from "react-router";
63
65
 
64
66
  const Header = ({
65
67
  reversed = false,
@@ -261,7 +263,7 @@ export function DataTable({
261
263
  filterValues,
262
264
  setFilterValues = () => {},
263
265
  data: propData,
264
- advanced= true, selectionMode= false, deleteApiCall, queryClient
266
+ advanced= true, selectionMode= false, deleteApiCall
265
267
  }) {
266
268
  const {
267
269
  models,
@@ -279,6 +281,7 @@ export function DataTable({
279
281
  const {me} = useAuthContext();
280
282
  const { execute, DeleteCommand } = useCommand();
281
283
 
284
+ const queryClient = useQueryClient();
282
285
  // Si des données sont passées en props, on les utilise, sinon on prend celles du contexte.
283
286
  const data = propData || paginatedDataByModel[model?.name] || [];
284
287
 
@@ -438,15 +441,12 @@ export function DataTable({
438
441
  onDuplicateData(dataToDuplicate);
439
442
  };
440
443
 
441
- const desc = t(`model_description_${selectedModel.name}`, selectedModel.description);
444
+ const nav = useNavigate();
445
+
446
+ const desc = t(`model_description_${selectedModel?.name}`, selectedModel?.description || '');
442
447
  return (
443
448
  <div className={`datatable${filterActive ? ' filter-active' : ''}`}>
444
- {advanced && !selectionMode && <div className="flex actions">
445
- {t(`model_${selectedModel?.name}`, selectedModel?.name) !== selectedModel?.name && (
446
- <span className="badge"><strong>model</strong> : {selectedModel?.name}</span>)}
447
- {selectedModel.name === 'dashboard' && <Button className={"btn"} onClick={() => {
448
- nav('/user/'+getUserHash(me)+'/dashboards');
449
- }}><FaEye /> Tableaux de bord</Button> }
449
+ {advanced && !selectionMode && <div className="flex">
450
450
  {desc && <p className="model-desc hint">{desc}</p>}
451
451
  <Button onClick={handleImport} title={t("btns.import")}><FaFileImport/><Trans
452
452
  i18nKey="btns.import">Importer</Trans></Button>
@@ -736,6 +736,13 @@ export function DataTable({
736
736
  >
737
737
  <FaHistory />
738
738
  </button>)}
739
+ {/* AJOUT : Bouton pour ouvrir l'éditeur de workflow */}
740
+ {model.name === 'workflow' && (
741
+ <NavLink to={`?edit-workflow=${item._id}`} className="datatable-action-btn workflow-edit"
742
+ data-tooltip-id="tooltipActions" data-tooltip-content={t('workflow.editor.title', "Éditeur de Workflow")}>
743
+ <FaGear />
744
+ </NavLink>
745
+ )}
739
746
 
740
747
  <button data-tooltip-id="tooltipActions"
741
748
  data-tooltip-content={t('btns.delete', 'Supprimer')}
@@ -57,13 +57,15 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
57
57
  setFields([...(initialModel.fields || []).map(m => ({...m}))]);
58
58
  setModelHistory(initialModel.history);
59
59
  setModelIcon(initialModel.icon);
60
+ setUseAI(false);
61
+ setPrompt(null);
62
+ setHomePrompt(null);
60
63
  } else {
61
64
  // Mode création : on réinitialise tout pour une nouvelle génération
62
65
  setModelName('');
63
66
  setModelDescription('');
64
67
  setFields([]);
65
68
  setUseAI(true); // On active l'IA par défaut
66
- setModelVisible(false);
67
69
  setModelHistory(false);
68
70
  setModelIcon(null);
69
71
  }
@@ -363,7 +365,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
363
365
  }
364
366
 
365
367
  const [useAI, setUseAI] = useState(true);
366
- const [showModel, setModelVisible] = useState(true);
368
+ const [showModel, setModelVisible] = useState(false);
367
369
  const [prompt, setPrompt] = useState('');
368
370
 
369
371
  const [homePrompt, setHomePrompt] = useLocalStorage("ai_model_prompt", null);
@@ -471,7 +473,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
471
473
  return c && (
472
474
  <><p className="ws-pre-line">{c}</p></>)
473
475
  }} />
474
- <h2>
476
+ <h2 className={"field-bg p-2"}>
475
477
  {!initialModel && <Trans i18nKey={"btns.createModel"}>Créer un modèle</Trans>}
476
478
  {!!initialModel && <><Trans i18nKey={"btns.editModel"}>Editer le modèle</Trans> &#34;{t(`model_${modelName}`, modelName)}&#34;</>}
477
479
  </h2>
@@ -517,7 +519,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
517
519
  )}
518
520
 
519
521
  {/* Layout principal pour afficher la liste et le formulaire côte à côte APRES génération */}
520
- {showModel && useAI && (
522
+ {useAI && showModel && !initialModel && (
521
523
  <div className="flex model-generation-layout">
522
524
  {/* Colonne de gauche: Liste des modèles générés */}
523
525
  {generatedModels.some(g => models.find(f => f.name === g.name && f._user === g._user)) && (
@@ -558,15 +560,13 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
558
560
  <div className="field">
559
561
  <div className="checkbox-label flex flex-1">
560
562
  <CheckboxField
561
- label={<Trans i18nKey={"history.title"}>Historique</Trans>}
562
- disabled={modelLocked || (isLocalUser(me) && field.locked)}
563
- checked={field.required}
563
+ label={<Trans i18nKey={"history"}>Historique</Trans>}
564
+ help={t('modelcreator.field.history', '')}
565
+ disabled={modelLocked}
566
+ checked={!!modelHistory}
564
567
  onChange={(e) => {
565
- const newFields = [...fields];
566
- newFields[index].history = e ? { enabled: true } : undefined;
567
- setFields(newFields);
568
+ setModelHistory(e? { enabled: true }: false);
568
569
  }}
569
- help={field.required && t('modelcreator.history.hint')}
570
570
  />
571
571
  </div>
572
572
  </div>
@@ -587,7 +587,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
587
587
  )}
588
588
 
589
589
  {/* Affichage du formulaire en mode manuel ou édition */}
590
- {(!useAI || initialModel) && (
590
+ {(!useAI || !!initialModel) && (
591
591
  <div className="model-form-container">
592
592
  <div className="flex field-bg">
593
593
  <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
@@ -1,7 +1,7 @@
1
1
  .model-creator {
2
2
  flex: 1;
3
3
  width: 100%;
4
- max-width: 720px;
4
+ max-width: 1024px;
5
5
  form {
6
6
  padding: 0;
7
7
  @media only screen and (min-width: 480px){
@@ -1,7 +1,7 @@
1
1
  import React, {useState, useEffect} from 'react';
2
2
  import { useModelContext } from './contexts/ModelContext.jsx';
3
3
  import { useAuthContext } from './contexts/AuthContext.jsx';
4
- import {Trans, useTranslation} from 'react-i18next';
4
+ import {Trans, useTranslation} from '../../src/i18n.js';
5
5
  import {FaArrowDown, FaInfo} from 'react-icons/fa';
6
6
  import { CheckboxField } from './Field.jsx';
7
7
  import Button from './Button.jsx';
@@ -94,6 +94,9 @@ const ModelImporterItem = ({ model, onUnselect, onSelect, selected }) => {
94
94
  useEffect(() => {
95
95
  setSelected(selected);
96
96
  }, [selected]);
97
+
98
+ if( !model )
99
+ return;
97
100
  const deps = [...new Set((model.fields || []).filter(f=> f.type === 'relation' && f.relation !== model?.name).map(f=> t(`model_${f.relation}`, f.relation)))];
98
101
  return <div className={`model-importer-item ${selected ? 'active' : ''}`} onClick={() => {
99
102
  setSelected(!selected);
@@ -30,7 +30,7 @@ const ViewSwitcher = ({ currentView, onViewChange, configuredViews, onConfigureV
30
30
  title={t(view.labelKey, view.defaultLabel)}
31
31
  >
32
32
  {view.icon}
33
- <span className="hidden md:inline-block ml-2">{t(view.labelKey, view.defaultLabel)}</span>
33
+ {/*<span className="hidden md:inline-block ml-2">{t(view.labelKey, view.defaultLabel)}</span>*/}
34
34
  </Button>
35
35
  {/* AJOUT : Bouton de configuration pour la vue active */}
36
36
  {isActive && view.id !== 'table' && (