data-primals-engine 1.0.11 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +212 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,120 @@
1
+ import React, {useState, useEffect} from 'react';
2
+ import { useModelContext } from './contexts/ModelContext.jsx';
3
+ import { useAuthContext } from './contexts/AuthContext.jsx';
4
+ import {Trans, useTranslation} from 'react-i18next';
5
+ import {FaArrowDown, FaInfo} from 'react-icons/fa';
6
+ import { CheckboxField } from './Field.jsx';
7
+ import Button from './Button.jsx';
8
+ import { useQueryClient } from 'react-query';
9
+ import { useNotificationContext } from './NotificationProvider.jsx';
10
+ import {metaModels} from "data-primals-engine/constants";
11
+
12
+
13
+ const ModelImporter = ({ onImport }) => {
14
+
15
+ const { models } = useModelContext();
16
+ const { me } = useAuthContext();
17
+ const { t } = useTranslation();
18
+
19
+ const [selectedModels, setSelectedModels] = useState([]);
20
+ return <>
21
+ <h2>{t('btns.importModels', 'Importer un modèle')}</h2>
22
+ <div className="models-import">
23
+ <div className="msg msg-info">
24
+ <FaInfo/>
25
+ <Trans i18nKey="modelimporter.info">
26
+ Importez des modèles dans votre base de donnée pour générer rapidement votre API
27
+ ou étendre ceux-ci.
28
+ </Trans>
29
+ </div>
30
+ {Object.keys(metaModels).map(metaModel => {
31
+ const mods = metaModels[metaModel]['load'];
32
+ const found = true;
33
+ return found ? <>
34
+ <h2 className="shadow flex">
35
+ <div className="flex-1">{t(`metamodel_${metaModel}`, metaModel)}</div>
36
+ <FaArrowDown style={{opacity:0.2}}/>
37
+ </h2>
38
+ <div className="list">{mods.map(m => {
39
+ const model = models.find(f=> f.name === m);
40
+ if( !model )
41
+ console.warn("model" + m + " not found");
42
+ if (model && !models.find(m => m.name === model.name && m._user === me?.username))
43
+ return <><ModelImporterItem model={model} onUnselect={(model) => {
44
+ setSelectedModels(models => models.filter(f => f.name !== model.name));
45
+ }} onSelect={(model) => {
46
+ if(!selectedModels.find(f => f.name === model.name)) {
47
+ gtag('event', 'select_content', {
48
+ content_type: 'import_model',
49
+ content_id: model.name
50
+ });
51
+
52
+ const rec = [];
53
+
54
+ const addRequiredMods = (model) => {
55
+ let mods = [];
56
+ model.fields.forEach(field =>{
57
+ if( field.type ==='relation'){
58
+ const relMod = models.find(f => f.name === field.relation);
59
+ if( !relMod)
60
+ return;
61
+ if (models.some(m => m._user === me?.username && m.name === relMod.name))
62
+ return;
63
+ if( !rec.includes(relMod.name) ) {
64
+ mods.push(relMod);
65
+ rec.push(relMod.name);
66
+ mods = mods.concat(addRequiredMods(relMod));
67
+ }
68
+ }
69
+ })
70
+ return [...new Set(mods)];
71
+ };
72
+
73
+ const allMods = [model, ...addRequiredMods(model)];
74
+
75
+ setSelectedModels(models => [...new Set([...models, ...allMods])])
76
+ }
77
+ }} selected={selectedModels.some(f=>f.name===model.name)} /></>
78
+ })}</div></> : <></>;
79
+ })}
80
+ </div>
81
+ <div className="flex actions">
82
+ <Button onClick={() => {
83
+ onImport(selectedModels);
84
+ }}>Importer les modèles ( {selectedModels.length} )</Button>
85
+ </div>
86
+ </>
87
+ }
88
+
89
+ const ModelImporterItem = ({ model, onUnselect, onSelect, selected }) => {
90
+
91
+ const { t } = useTranslation();
92
+ const [, setSelected] = useState(false);
93
+
94
+ useEffect(() => {
95
+ setSelected(selected);
96
+ }, [selected]);
97
+ const deps = [...new Set((model.fields || []).filter(f=> f.type === 'relation' && f.relation !== model?.name).map(f=> t(`model_${f.relation}`, f.relation)))];
98
+ return <div className={`model-importer-item ${selected ? 'active' : ''}`} onClick={() => {
99
+ setSelected(!selected);
100
+ if (!selected)
101
+ onSelect(model);
102
+ else
103
+ onUnselect(model);
104
+ }}>
105
+ <CheckboxField checked={selected} onChange={e => {
106
+ setSelected(e.target.checked)
107
+ if (e.target.checked)
108
+ onSelect(model);
109
+ else
110
+ onUnselect(model);
111
+ }} label={t(`model_${model.name}`, model.name)} />
112
+ <ul>
113
+ <li>{t(`model_description_${model.name}`, model.description)}</li>
114
+ {deps.length > 0 &&<li> <><Trans i18nKey="modelimporter.requires">Requiert : </Trans> {deps.join(', ')}</></li>}
115
+ </ul>
116
+ </div>;
117
+ }
118
+
119
+
120
+ export { ModelImporter, ModelImporterItem };
@@ -0,0 +1,205 @@
1
+ import {useModelContext} from "./contexts/ModelContext.jsx";
2
+ import {Trans, useTranslation} from "react-i18next";
3
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
4
+ import {useMutation, useQueryClient} from "react-query";
5
+ import React, {useEffect, useMemo, useState} from "react";
6
+ import {FaBook, FaEdit, FaFileImport, FaPlus} from "react-icons/fa";
7
+ import Button from "./Button.jsx";
8
+ import useLocalStorage from "./hooks/useLocalStorage.js";
9
+ import {Tooltip} from "react-tooltip";
10
+ import {useUI} from "./contexts/UIContext.jsx";
11
+ import {profiles} from "./constants.js";
12
+
13
+ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditModel, onAPIInfo, onNewData }) {
14
+ const {allTourSteps, setIsTourOpen,setCurrentTourSteps, setTourStepIndex, currentTour, setCurrentTour} = useUI();
15
+
16
+ const {models, setSelectedModel, selectedModel, countByModel, generatedModels} = useModelContext();
17
+ const {t} = useTranslation();
18
+ const {me} = useAuthContext();
19
+ const queryClient = useQueryClient()
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+
22
+ const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
23
+ useEffect(() =>{
24
+ if (me) {
25
+ queryClient.invalidateQueries('api/models');
26
+ }
27
+ }, [me])
28
+
29
+ const filteredModels = useMemo(() => {
30
+ if (!models) return [];
31
+ if (!searchTerm.trim()) return models; // Si la recherche est vide, retourne tous les modèles
32
+
33
+ const lowerSearchTerm = searchTerm.toLowerCase();
34
+ return models.filter(model => {
35
+ return (model.name && t('model_' + model.name, model.name).toLowerCase().includes(lowerSearchTerm)) ||
36
+ (model.fields.some(f =>
37
+ f.name?.toLowerCase().includes(lowerSearchTerm) ||
38
+ f.hint?.toLowerCase().includes(lowerSearchTerm)) ||
39
+ (model.description && model.description.toLowerCase().includes(lowerSearchTerm)))
40
+ });
41
+ }, [models, searchTerm]);
42
+
43
+ const handleSelectModel = (model) => {
44
+ setSelectedModel(model);
45
+ if (onModelSelect) {
46
+ onModelSelect(model); // Si vous avez une prop callback supplémentaire
47
+ }
48
+ };
49
+
50
+ const importModelsMutation = useMutation((profile) => {
51
+ return fetch('/api/models/import', { method: 'POST', headers: {
52
+ 'Content-Type': 'application/json'
53
+ },
54
+ body: JSON.stringify({ install: true, models: ['request', ...profiles[profile]] })
55
+ })
56
+ });
57
+ const handleProfile = (profile) => {
58
+ importModelsMutation.mutateAsync(profile).then(e => {
59
+ setCurrentProfile(profile);
60
+ gtag('event', 'profile ' + profile);
61
+ queryClient.invalidateQueries('api/models');
62
+
63
+ const profileSteps = allTourSteps[profile];
64
+ if (profileSteps) {
65
+ setCurrentTourSteps(profileSteps);
66
+ setTourStepIndex(0);
67
+ setIsTourOpen(true); // Start the tour
68
+ } else {
69
+ console.warn(`No tour steps defined for profile: ${profile}`);
70
+ }
71
+ });
72
+ };
73
+
74
+ const [mods, setMods] = useState([]);
75
+ useEffect(() => {
76
+ if( countByModel && selectedModel)
77
+ {
78
+ setMods((m) => {
79
+ const counts = [...m];
80
+ const mod = counts.find(f => f.mod === selectedModel.name);
81
+ if( mod ){
82
+ mod.count = countByModel[selectedModel.name];
83
+ return counts.map((c => {
84
+ if( c.mod === mod.mod) {
85
+ return {...mod};
86
+ }
87
+ return c;
88
+ }));
89
+ }else{
90
+ return [...m, { mod: selectedModel.name, count: countByModel[selectedModel.name]}];
91
+ }
92
+ })
93
+ }
94
+ }, [countByModel, selectedModel]);
95
+
96
+ // --- NEW: Effect to update tour steps when a profile is already selected on mount ---
97
+ useEffect(() => {
98
+ if (currentProfile && currentTour && /^demo[0-9]{1,2}$/.test(me?.username)) { // Only run on demo user for simplicity
99
+ const profileSteps = allTourSteps[currentProfile];
100
+ if (profileSteps) {
101
+ setCurrentTourSteps(profileSteps);
102
+ setTourStepIndex(0)
103
+ setIsTourOpen(true);
104
+ } else {
105
+ console.warn(`No tour steps defined for profile: ${currentProfile}`);
106
+ }
107
+ }
108
+ }, [currentProfile, me?.username]);
109
+
110
+ if (!models) {
111
+ return <div className="loading-models">{t('models.loading', 'Chargement des modèles...')}</div>;
112
+ }
113
+
114
+ return <>
115
+ {!currentProfile && /^demo[0-9]{1,2}$/.test(me.username) && <div className="tourStep-profile profiles flex-stretch flex">
116
+ <div className=" flex flex-centered flex-big-gap">
117
+ <a href="#" data-tooltip-id="tooltipProfile" data-tooltip-content={t('profiles.personal.desc')} className="profile-link" onClick={() => handleProfile('personal')}>
118
+ <img src="/profilPersonal.jpg" alt={"Personal profile"} width={256} height={256}/>
119
+ </a>
120
+ <a href="#" data-tooltip-id="tooltipProfile" data-tooltip-content={t('profiles.developer.desc')} className="profile-link" onClick={() => handleProfile('developer')}>
121
+ <img src="/profilDeveloper.jpg" alt={"Developer profile"} width={256} height={256}/>
122
+ </a>
123
+ <a href="#" data-tooltip-id="tooltipProfile" data-tooltip-content={t('profiles.company.desc')} className="profile-link" onClick={() => handleProfile('company')}>
124
+ <img src="/profilCompany.jpg" alt={"Company profile"} width={256} height={256}/>
125
+ </a>
126
+ <a href="#" data-tooltip-id="tooltipProfile" data-tooltip-content={t('profiles.engineer.desc')} className="profile-link" onClick={() => handleProfile('engineer')}>
127
+ <img src="/profilEngineer.jpg" alt={"Engineer profile"} width={256} height={256}/>
128
+ </a>
129
+ <Tooltip id="tooltipProfile" render={({content, activeAnchor}) => {
130
+ const pr = activeAnchor?.querySelector('img').getAttribute('alt');
131
+ if (pr)
132
+ gtag('event', 'info '+ pr);
133
+ else
134
+ gtag('event', 'info');
135
+ return <span dangerouslySetInnerHTML={{__html:content}} />
136
+ }} afterShow={() => {
137
+
138
+ }} />
139
+ </div></div>}
140
+ <div className="models">
141
+ <h2><Trans i18nKey="models">Modèles</Trans></h2>
142
+ <div className="model-list-container">
143
+ <div className="model-list-search-bar-container">
144
+ <input
145
+ type="search"
146
+ placeholder={t('models.searchPlaceholder', 'Rechercher un modèle...')}
147
+ value={searchTerm}
148
+ onChange={(e) => setSearchTerm(e.target.value)}
149
+ className="model-list-search-input"
150
+ />
151
+ </div>
152
+ {filteredModels.length > 0 ? (
153
+ <div className="model-list">
154
+ <Tooltip id="tooltipML" />
155
+ <ul>
156
+ {filteredModels.sort((a, b) => t('model_' + a.name, a.name).localeCompare(t('model_' + b.name, b.name))).map((model) => {
157
+ if (model._user !== me.username)
158
+ return <></>
159
+ return (
160
+ <li data-testid={'model_'+model.name} className={`${model.name === selectedModel?.name ? 'active' : ''}`}
161
+ key={'modelist' + model.name} onClick={() => generatedModels.some(g => g.name === model.name) ? onEditModel(model) : onModelSelect(model)}>
162
+ <div className="flex flex-center flex-fw">
163
+ <div
164
+ className="flex-1 break-word">{t(`model_${model.name}`, model.name)} {generatedModels.some(f => f.name === model.name) ? '(tmp)' : (mods.some(f => f.mod === model.name) ? `(${mods.find(f => f.mod === model.name).count})` : '')}</div>
165
+ <div className="btns">
166
+ {!generatedModels.some(g => g.name === model.name) && (<button data-tooltip-id="tooltipML" data-tooltip-content={t('btns.addData')} onClick={(e) => {
167
+ e.stopPropagation();
168
+ e.preventDefault();
169
+ onNewData?.(model);
170
+ }}><FaPlus/></button>)}
171
+ {!model.locked && (<button data-tooltip-id="tooltipML" data-tooltip-content={t('btns.editModel')} onClick={(e) => {
172
+ e.stopPropagation();
173
+ e.preventDefault();
174
+ onEditModel(model);
175
+ }}><FaEdit/>
176
+ </button>)}
177
+ {(<button data-tooltip-id="tooltipML" data-tooltip-content={t('doc.api.title')} onClick={(e) => {
178
+ e.stopPropagation();
179
+ e.preventDefault();
180
+ onAPIInfo?.(model);
181
+ }}><FaBook/>
182
+ </button>)}
183
+ </div>
184
+ </div>
185
+ </li>)
186
+ })}
187
+ </ul>
188
+
189
+ </div>) : (
190
+ <p className="no-models-found">
191
+ {searchTerm ? t('models.noMatch', 'Aucun modèle ne correspond à votre recherche.') : t('models.noModels', 'Aucun modèle disponible.')}
192
+ </p>
193
+ )}
194
+ <div className="flex actions">
195
+ <Button onClick={onCreateModel} className="btn tourStep-create-model"><FaPlus/><Trans
196
+ i18nKey="btns.createModel">Créer un modèle</Trans></Button>
197
+ <Button onClick={onImportModel}
198
+ className="btn tourStep-import-model btn-primary"><FaFileImport/><Trans
199
+ i18nKey="btns.importModels">Importer un modèle</Trans></Button>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </>
204
+ }
205
+
@@ -0,0 +1,137 @@
1
+ import React, {useState, useEffect, useRef, useCallback, forwardRef} from 'react';
2
+ import { FaCheck, FaSpinner, FaExclamationTriangle, FaBell, FaTimes, FaTrash } from 'react-icons/fa'; // Example icons
3
+ import { useNotificationContext } from './NotificationProvider.jsx'; // Import the context
4
+ import './Notification.scss';
5
+
6
+ /*
7
+ const { addNotification } = useNotificationContext();
8
+ const notificationData = {
9
+ title: 'New Notification',
10
+ message: 'This is a new notification message.',
11
+ icon: <FaBell />,
12
+ status: ['ongoing','completed','error'][Math.floor(Math.random()*3)]
13
+ };
14
+ */
15
+ // Helper function to generate a unique ID
16
+ const generateNotificationId = () => {
17
+ return Math.random().toString(36).substring(2, 9);
18
+ };
19
+
20
+ const Notification = ({ notification, onClick }) => {
21
+ const { markAsRead, removeNotification } = useNotificationContext();
22
+ const [dismissTimer, setDismissTimer] = useState(null);
23
+ const notificationRef = useRef(null);
24
+ const isMounted = useRef(false)
25
+
26
+ const handleMarkAsRead = useCallback(() => {
27
+ markAsRead(notification.id);
28
+ }, [markAsRead, notification.id]);
29
+
30
+ useEffect(() => {
31
+ const t = setTimeout(()=>{
32
+ markAsRead(notification.id);
33
+ }, notification.timeout || 12000);
34
+ return () => {
35
+ clearTimeout(t);
36
+ }
37
+ }, [notification]);
38
+
39
+ useEffect(() => {
40
+ if (notification.isRead) {
41
+ const timer = setTimeout(() => {
42
+ removeNotification(notification.id);
43
+ }, 30000);
44
+
45
+ return () => {
46
+ clearTimeout(timer);
47
+ };
48
+ }
49
+ }, [notification.isRead, notification.id, removeNotification]);
50
+
51
+ let statusIcon;
52
+ switch (notification.status) {
53
+ case 'ongoing':
54
+ statusIcon = <FaSpinner className="spinner" />;
55
+ break;
56
+ case 'completed':
57
+ statusIcon = <FaCheck />;
58
+ break;
59
+ case 'error':
60
+ statusIcon = <FaExclamationTriangle />;
61
+ break;
62
+ default:
63
+ statusIcon = null;
64
+ }
65
+
66
+ const removeBtn = notification.isRead ? <button onClick={() => {
67
+ removeNotification(notification.id);
68
+ }}><FaTrash /></button> : <></>
69
+ return (
70
+ <div ref={notificationRef} onClick={onClick} className={`notification ${notification.isExpanded ? 'expanded' : 'collapsed'} ${notification.status}`}>
71
+ <div className="notification-header">
72
+ {notification.icon && <span className="notification-icon">{notification.icon}</span>}
73
+ <span className="notification-title">{notification.title}</span>
74
+ {statusIcon && <span className="notification-status">{statusIcon}</span>}
75
+ {removeBtn}
76
+ </div>
77
+ {<div className="notification-content">
78
+ <p className="notification-message">{notification.message}</p>
79
+ </div>}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ const NotificationList = forwardRef((props, ref) => {
85
+ const { notifications, markAsRead, removeNotification, addNotification, unreadCount } = useNotificationContext();
86
+ const [showBubble, setShowBubble] = useState(false);
87
+
88
+
89
+ const toggleBubble = useCallback(() => {
90
+ setShowBubble(!showBubble)
91
+ }, [showBubble]);
92
+
93
+ useEffect(() => {
94
+ if( unreadCount > 0 ){
95
+ if (!showBubble)
96
+ setShowBubble(true);
97
+ }else{
98
+ if (showBubble)
99
+ setShowBubble(false);
100
+ }
101
+ }, [unreadCount]);
102
+
103
+ const readNotifications = notifications.filter(n => n.isRead) || [];
104
+ const unreadNotifications = notifications.filter(n => !n.isRead) || [];
105
+ return (
106
+ <div className="notification-container">
107
+ <div className="notifications-right">
108
+ {unreadNotifications.map(notification => (
109
+ <Notification
110
+ onClick={() => {
111
+ markAsRead(notification.id)
112
+ }}
113
+ key={notification.id}
114
+ notification={notification}
115
+ />
116
+ ))}
117
+ {unreadCount > 0 && <div className={"notification-bubble-count"}>{unreadCount}</div>}
118
+ </div>
119
+ <div className={`notification-bubble ${showBubble ? 'visible' : ''}`}>
120
+ <button onClick={toggleBubble} className="notification-bubble-btn">
121
+ <FaBell/>
122
+ {unreadCount > 0 && <span className="notification-bubble-count">{unreadCount}</span>}
123
+ </button>
124
+ {showBubble && <div className='notifications-bubble-content'>
125
+ {readNotifications.map(notification => (
126
+ <Notification
127
+ key={notification.id}
128
+ notification={notification}
129
+ />
130
+ ))}
131
+ </div>}
132
+ </div>
133
+ </div>
134
+ );
135
+ });
136
+ NotificationList.displayName = "NotificationList";
137
+ export {NotificationList, generateNotificationId}; // Export for use elsewherex
@@ -0,0 +1,126 @@
1
+ .notification-container {
2
+ position: fixed;
3
+ top: 52px;
4
+ right: 8px;
5
+ width: 280px;
6
+ z-index: 10000000;
7
+ pointer-events: auto;
8
+ }
9
+
10
+ .notifications-right, .notifications-bubble-content {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 10px;
14
+ }
15
+
16
+ .notification {
17
+ background-color: #fff;
18
+ border: 1px solid #ddd;
19
+ padding: 10px;
20
+ border-radius: 5px;
21
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
22
+ opacity: 1;
23
+ transition: opacity 0.3s ease, height 0.3s ease;
24
+ pointer-events: all;
25
+ }
26
+
27
+ .notification.ongoing {
28
+ border-color: #007bff;
29
+ }
30
+
31
+ .notification.completed {
32
+ border-color: #28a745;
33
+ }
34
+
35
+ .notification.error {
36
+ border-color: #dc3545;
37
+ }
38
+
39
+ .notification-header {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 10px;
43
+ margin-bottom: 10px;
44
+ font-weight: bold;
45
+ }
46
+
47
+ .notification-icon {
48
+ font-size: 1.2em;
49
+ color: #6c757d;
50
+ }
51
+
52
+ .notification-status {
53
+ font-size: 1.2em;
54
+ }
55
+
56
+ .notification-message {
57
+ margin: 0;
58
+ }
59
+
60
+ .notification.expanded {
61
+ height: auto;
62
+ }
63
+
64
+ .spinner {
65
+ animation: spin 1s linear infinite;
66
+ }
67
+
68
+ @keyframes spin {
69
+ from {
70
+ transform: rotate(0deg);
71
+ }
72
+ to {
73
+ transform: rotate(360deg);
74
+ }
75
+ }
76
+
77
+ .notification-bubble {
78
+ position: fixed;
79
+ bottom: 20px;
80
+ right: 20px;
81
+ background-color: #007bff;
82
+ color: #fff;
83
+ border-radius: 50%;
84
+ width: 50px;
85
+ height: 50px;
86
+ justify-content: center;
87
+ align-items: center;
88
+ cursor: pointer;
89
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
90
+ z-index: 1000;
91
+ transition: all 0.3s ease;
92
+ pointer-events: all;
93
+ display: none;
94
+ }
95
+
96
+ .notification-bubble.visible {
97
+ display: flex;
98
+ }
99
+ .notifications-bubble-content {
100
+ display: none;
101
+ }
102
+ .notification-bubble.expanded .notifications-bubble-content {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 10px;
106
+ }
107
+
108
+ .notification-bubble-count {
109
+ position: absolute;
110
+ top: -5px;
111
+ right: -5px;
112
+ background-color: red;
113
+ color: white;
114
+ border-radius: 50%;
115
+ padding: 2px 6px;
116
+ font-size: 0.8em;
117
+ pointer-events: none;
118
+ }
119
+
120
+ .notification button {
121
+ cursor: pointer;
122
+ background: none;
123
+ border: none;
124
+ padding: 0;
125
+ margin: 0;
126
+ }
@@ -0,0 +1,73 @@
1
+ import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
2
+ import { generateNotificationId } from './Notification.jsx'; // Adjust path if needed
3
+
4
+ const NotificationContext = createContext();
5
+
6
+ export const useNotificationContext = () => useContext(NotificationContext);
7
+
8
+ export const NotificationProvider = ({ children }) => {
9
+ const [notifications, setNotifications] = useState([]);
10
+ const [unreadCount, setUnreadCount] = useState(0);
11
+
12
+ useEffect(() => {
13
+ setUnreadCount(notifications.filter(n => !n.isRead).length);
14
+ }, [notifications]);
15
+
16
+ const getNotification = (notificationId) => {
17
+ return notifications.find(notification => notification.id === notificationId);
18
+ }
19
+
20
+ const addNotification = useCallback((newNotification) => {
21
+ setNotifications((prevNotifications) => {
22
+ if( newNotification.id && prevNotifications.find(m => m.id === newNotification.id)){
23
+ return prevNotifications.map(m => m.id === newNotification.id ? { ...m, ...newNotification } : m);
24
+ }
25
+ const id = newNotification.id ?? generateNotificationId();
26
+ const timestamp = new Date().getTime();
27
+ const updatedNotifications = [{ id, ...newNotification, isExpanded: false, isRead: false, timestamp }, ...prevNotifications];
28
+ return updatedNotifications;
29
+ });
30
+ }, []); // Remove notifications from the dependency array
31
+
32
+ const markAsRead = useCallback((notificationId) => {
33
+ setNotifications(prevNotifications =>
34
+ prevNotifications.map(notification => {
35
+ if (notification.id === notificationId) {
36
+ return { ...notification, isExpanded: false, isRead: true };
37
+ }
38
+ return notification;
39
+ })
40
+ );
41
+ }, []);
42
+
43
+ const removeNotification = useCallback((notificationId) => {
44
+ setNotifications((prevNotifications) => prevNotifications.filter((notification) => notification.id !== notificationId));
45
+ }, []);
46
+
47
+ const toggleExpand = useCallback((notificationId) => {
48
+ setNotifications(prevNotifications =>
49
+ prevNotifications.map(notification => {
50
+ if (notification.id === notificationId) {
51
+ return { ...notification, isExpanded: !notification.isExpanded };
52
+ }
53
+ return notification;
54
+ })
55
+ );
56
+ }, []);
57
+
58
+ const contextValue = {
59
+ notifications,
60
+ addNotification,
61
+ getNotification,
62
+ markAsRead,
63
+ removeNotification,
64
+ toggleExpand,
65
+ unreadCount
66
+ };
67
+
68
+ return (
69
+ <NotificationContext.Provider value={contextValue}>
70
+ {children}
71
+ </NotificationContext.Provider>
72
+ );
73
+ };