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,274 @@
1
+ // C:/Dev/hackersonline-engine/client/src/pages/ContentView.jsx
2
+ import React, {useEffect, useMemo, useRef, useState} from 'react';
3
+ import {useParams, Link, useLocation } from 'react-router-dom'; // NavLink supprimé car non utilisé
4
+ import { useQuery } from 'react-query';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { urlData } from './core/data.js'; // urlData ajouté et correction espacement
7
+ // import Loading from '../components/Loading.jsx'; // Pour un meilleur affichage du chargement
8
+ // import NotFoundPage from './NotFoundPage'; // Si vous avez une page 404 dédiée
9
+
10
+ import './DocumentationPageLayout.scss';
11
+ import {FaChevronDown, FaChevronRight} from "react-icons/fa";
12
+ import APIInfo from "./APIInfo.jsx";
13
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
14
+ import {useModelContext} from "./contexts/ModelContext.jsx";
15
+ import {renderToString} from "react-dom/server";
16
+ import {CodeField} from "./Field.jsx";
17
+ import uniqid from "uniqid";
18
+
19
+ // Styles SCSS (à créer ou adapter)
20
+ // import './ContentView.scss'; // Si vous voulez des styles spécifiques
21
+
22
+ // Ce composant est bien défini ici, au niveau supérieur du module.
23
+ const DocMenuGroup = ({ title, children, initialOpen = true }) => {
24
+ const [isOpen, setIsOpen] = React.useState(initialOpen);
25
+
26
+ return (
27
+ <div className="doc-menu-group">
28
+ <button onClick={() => setIsOpen(!isOpen)} className="doc-menu-group-title">
29
+ {isOpen ? <FaChevronDown /> : <FaChevronRight />}
30
+ <span>{title}</span>
31
+ </button>
32
+ {isOpen && <ul className="doc-menu-sublist">{children}</ul>}
33
+ </div>
34
+ );
35
+ };
36
+
37
+ const ContentView = () => { // Le prop 'menu' a été retiré car non passé par App.jsx pour cette route
38
+
39
+ const [root, setRoot] = useState();
40
+
41
+ const { me} = useAuthContext();
42
+ const { models, setSelectedModel } = useModelContext();
43
+ const wildcard = useParams()['*'];
44
+ const [cat, ...slugArr] = wildcard.split("/"); // 'cat' est l'identifiant de la catégorie parente, 'slug' celui du contenu
45
+ const slug = slugArr.join("/");
46
+ const {t, i18n} = useTranslation();
47
+ const location = useLocation();
48
+ const langCode = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
49
+
50
+ // 2. Récupérer le contenu principal basé sur le slug et la langue
51
+ const {data: contentData, isLoading: isLoadingContent, error: errorContent} = useQuery(
52
+ ['contentPage', slug, langCode, cat], // Ajout de cat pour la spécificité de la clé
53
+ () => fetch('/api/website/content?lang='+langCode+'&slug='+slug+'&limit=1&sort=_id:DESC', { headers: { 'Content-Type': 'application/json'}}).then(e => e.json())
54
+ );
55
+ const pageContent = contentData?.data?.[0];
56
+
57
+ console.log({contentData})
58
+ // 4. Récupérer les sous-catégories de la catégorie parente 'cat'
59
+ const {data: childCategoriesData, isLoading: isLoadingChildCategories} = useQuery(
60
+ ['childDocCategories', cat, langCode], // Clé de query mise à jour
61
+ () => fetch('/api/website/taxonomies?lang='+langCode+'&identifier='+cat, { headers: { 'Content-Type': 'application/json' }}).then(e => e.json()), // Tri par order, puis par nom
62
+ { enabled: !!cat && !!langCode } // Activer si cat et langCode sont présents
63
+ );
64
+ const childCategories = childCategoriesData?.data || [];
65
+
66
+ // 5. Récupérer les contenus pour la table des matières (TOC)
67
+ const childCategoryIds = useMemo(() => childCategories.map(c => c._id), [childCategories]);
68
+
69
+ const {data: tocContentQuery, isLoading: isLoadingTocContent} = useQuery(
70
+ ['tocContent', childCategoryIds, langCode], // Clé de query mise à jour
71
+ () => fetch( '/api/website/content?cats='+childCategoryIds.join(',')+'&lang='+langCode+'&sort=order:ASC,title:ASC', { headers: { 'Content-Type': 'application/json' }}).then(e => e.json()), // Tri par order, puis par titre
72
+ {enabled: !!langCode && childCategoryIds.length > 0} // Activer si langCode et des IDs de catégories enfants sont présents
73
+ );
74
+
75
+ const tocItemsByCategories = useMemo(() => {
76
+ if (!tocContentQuery?.data || !childCategories.length) return {};
77
+
78
+ const grouped = {};
79
+ // Initialiser les groupes en respectant l'ordre des childCategories (déjà triées)
80
+ childCategories.forEach(c => {
81
+ const categoryName = t(c.name.key, c.name.value); // Utiliser name_translated si disponible
82
+ grouped[categoryName] = [];
83
+ });
84
+
85
+ tocContentQuery.data.forEach(item => {
86
+ // Assumant que item.category est un tableau de relations et on prend la première
87
+ const itemCategoryId = item.category?._id;
88
+ const parentCategory = childCategories.find(c => c._id === itemCategoryId);
89
+
90
+ if (parentCategory) {
91
+ console.log({pt:parentCategory.name});
92
+ const categoryName = parentCategory.name.value || t('toc.unknownCategory', 'Autres');
93
+ if (grouped[categoryName]) { // La catégorie doit exister car initialisée plus haut
94
+ grouped[categoryName].push(item); // Les items sont déjà triés par la query
95
+ }
96
+ } else {
97
+ // Gérer les items sans catégorie ou avec catégorie non listée (optionnel)
98
+ const fallbackCategoryName = t('toc.unknownCategory', 'Autres');
99
+ if (!grouped[fallbackCategoryName]) {
100
+ grouped[fallbackCategoryName] = [];
101
+ }
102
+ grouped[fallbackCategoryName].push(item);
103
+ }
104
+ });
105
+ // Filtrer les catégories vides
106
+ return Object.fromEntries(Object.entries(grouped).filter(([_, v]) => v.length > 0));
107
+ }, [tocContentQuery, childCategories, langCode, t]);
108
+
109
+ const pageTitle = pageContent?.title.value;
110
+ const pageHtml = pageContent?.html[langCode] || pageContent?.html['en'] || pageContent?.html['fr'];
111
+
112
+ const refContent = useRef();
113
+
114
+ useEffect(() => {
115
+ if( contentData && refContent.current){
116
+ setTimeout(() => {
117
+ refContent.current.scrollIntoView({behavior:'smooth'});
118
+ }, 50);
119
+ }
120
+ }, [contentData])
121
+
122
+ const [codeFields, setCodeFields] = useState([]);
123
+ useEffect(() => {
124
+
125
+ if (pageContent)
126
+ gtag('event', '['+langCode+'] read ' + pageContent?.slug);
127
+
128
+ const r= document.createElement("div");
129
+ setRoot(r);
130
+
131
+ r.innerHTML = pageHtml || '';
132
+ const cf = [];
133
+ document.body.appendChild(r);
134
+ r.querySelectorAll('pre > code').forEach(cc =>{
135
+ const rawLanguage = (cc.getAttribute('class')||'language-json').split('-');
136
+ const language = rawLanguage[rawLanguage.length-1];
137
+ cf.push({c:cc.innerText,a: language});
138
+ cc.parentNode.innerHTML = '<i>[@codeField]</i>';
139
+ })
140
+ document.body.removeChild(r);
141
+ setCodeFields(cf);
142
+
143
+
144
+ }, [pageHtml, pageContent]);
145
+
146
+
147
+ useEffect(() => {
148
+ if( slug && slug.startsWith('api/models/') ){
149
+ let v = slug.split("/")[2];
150
+ const mod = models.find(f => f.name === v && f._user === me?.username);
151
+ setSelectedModel(mod);
152
+ }
153
+ }, [slug, models, me]);
154
+
155
+ useEffect(() => {
156
+ const mainTitle = pageContent?.title_translated?.[langCode] || pageContent?.title.value;
157
+ if (mainTitle) {
158
+ document.title = `${t(mainTitle, mainTitle)} - ${t('siteName', 'data-primals-engine')}`;
159
+ } else if (!isLoadingContent && !pageContent) {
160
+ document.title = `${t('pageNotFound.title', 'Page non trouvée')} - ${t('siteName', 'data-primals-engine')}`;
161
+ }
162
+ }, [pageContent, isLoadingContent, t, langCode]);
163
+
164
+ const isLoading = isLoadingContent || isLoadingChildCategories || isLoadingTocContent;
165
+
166
+ if (isLoading) {
167
+ // return <Loading />; // Idéalement, utiliser un composant de chargement
168
+ return '...';
169
+ }
170
+
171
+ if (errorContent) {
172
+ return <div>{t('error.fetchContent', "Erreur lors de la récupération du contenu.")} ({errorContent.message})</div>;
173
+ }
174
+
175
+ // La définition de DocMenuGroup à l'intérieur de ContentView a été supprimée car elle existe déjà au niveau module.
176
+
177
+ const navMenu = <nav className="documentation-menu">
178
+ {Object.entries(tocItemsByCategories).map(([categoryName, items]) => (
179
+ // Utilisation du composant DocMenuGroup défini au niveau module
180
+ <DocMenuGroup key={categoryName} title={categoryName} initialOpen={true}> {/* Ouvre tous les groupes par défaut, ou ajustez la logique */}
181
+ {items.map(item => {
182
+ const itemTitle = item.title.value;
183
+ return (
184
+ <li key={item._id}>
185
+ <Link className={`doc-menu-item ${location.pathname === `/${langCode}/${cat}${item.slug}` ? 'active' : ''}`} to={`/${langCode}/${cat}${item.slug}`}> {/* Assurez-vous que item.slug commence par "/" si nssaire */}
186
+ {t(itemTitle, itemTitle)}
187
+ </Link>
188
+ </li>
189
+ );
190
+ })}
191
+ </DocMenuGroup>
192
+ ))}
193
+ {Object.keys(tocItemsByCategories).length === 0 && !isLoading && (
194
+ <p>{t('toc.noContent', 'Aucun contenu disponible pour la table des matières.')}</p>
195
+ )}
196
+ </nav>;
197
+
198
+ if (!pageContent && cat === 'documentation' && !isLoadingContent) {
199
+ return (
200
+ <div className="flex flex-no-wrap documentation-layout">
201
+ <aside className="documentation-sidebar">
202
+ <h3>{t('toc.title', 'Documentation')}</h3>
203
+ {navMenu}
204
+ </aside>
205
+ <main className="documentation-content">
206
+
207
+ {slug.startsWith('api/') || slug==='api' ? (
208
+ <APIInfo/>
209
+ ) : <>
210
+ <h1>{t('pageNotFound.title', 'Page non trouvée')}</h1>
211
+ <p>{t('pageNotFound.message', 'Le contenu que vous recherchez n\'existe pas ou n\'est plus disponible.')}</p>
212
+ <Link to={`/`}>{t('pageNotFound.goHome', `Retourner à l'accueil`)}</Link></>}
213
+
214
+ </main>
215
+ </div>
216
+ );
217
+ }
218
+
219
+ return (
220
+ <div className="flex flex-no-wrap documentation-layout content-view-layout">
221
+
222
+ {cat === 'documentation' && (<aside className="documentation-sidebar">
223
+ <h3>{t('toc.title', 'Documentation')}</h3>
224
+ {navMenu}
225
+ </aside>)}
226
+
227
+ {pageContent && (
228
+ <main className="documentation-content" ref={refContent}>
229
+ {pageContent.image && (
230
+ <img
231
+ src={typeof pageContent.image === 'string' ? pageContent.image : (pageContent.image.guid ? `${urlData}resources/${pageContent.image.guid}` : '')}
232
+ alt={pageTitle}
233
+ className="content-main-image"
234
+ style={{maxWidth: '100%', height: 'auto', marginBottom: '1em'}}
235
+ />
236
+ )}
237
+ <h1>{t(pageTitle, pageTitle)}</h1>
238
+ <div className="content-meta">
239
+ {pageContent.publishedAt && (
240
+ <p>
241
+ {t('content.publishedOn', 'Publié le')}: {new Date(pageContent.publishedAt).toLocaleDateString(i18n.language)}
242
+ </p>
243
+ )}
244
+ {/* Vous pouvez ajouter l'auteur ici si besoin, en utilisant getDataAsString */}
245
+ {/* pageContent.author && (
246
+ <p>
247
+ {t('content.author', 'Auteur')}: {getDataAsString( modèleAuteur , pageContent.author)}
248
+ </p>
249
+ )*/}
250
+ </div>
251
+
252
+ {slug.startsWith('api/') || slug==='api' ? (
253
+ <APIInfo/>
254
+ ) : root?.innerHTML.split("<pre><i>[@codeField]</i></pre>").map((m,index)=>{
255
+ const cc = codeFields[index];
256
+ console.log({cc, index});
257
+ return <div><div dangerouslySetInnerHTML={{__html: m}}></div>{cc && <div className={"fs-regular"}>
258
+ <CodeField
259
+ key={uniqid()}
260
+ value={cc.c}
261
+ language={cc.a}
262
+ name={"code" + uniqid()}
263
+ disabled={true}/>
264
+ </div>}
265
+ </div>;
266
+ })}
267
+
268
+ </main>
269
+ )}
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default ContentView;
@@ -0,0 +1,126 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { CronPartBuilder } from './CronPartBuilder.jsx';
3
+ import cronstrue from 'cronstrue/i18n';
4
+ import { useTranslation } from 'react-i18next';
5
+ import './CronBuilder.scss';
6
+
7
+ const CronBuilder = ({ cronExpression, cronMask, defaultCronExpression, onCronChange }) => {
8
+ const { t, i18n } = useTranslation();
9
+ const [parts, setParts] = useState({
10
+ second: '*',
11
+ minute: '*',
12
+ hour: '*',
13
+ dayOfMonth: '*',
14
+ month: '*',
15
+ dayOfWeek: '*',
16
+ });
17
+ const [defaultParts, setDefaultParts] = useState({
18
+ second: '*',
19
+ minute: '*',
20
+ hour: '*',
21
+ dayOfMonth: '*',
22
+ month: '*',
23
+ dayOfWeek: '*',
24
+ });
25
+ const [humanReadable, setHumanReadable] = useState('');
26
+ // Nouvel état pour suivre l'initialisation et éviter les boucles de rendu.
27
+ const [isReady, setIsReady] = useState(false);
28
+
29
+ useEffect(() => {
30
+ setIsReady(false);
31
+
32
+ const cronParts = (cronExpression || '* * * * * *').split(' ');
33
+ if (cronParts.length === 5) {
34
+ cronParts.unshift('*');
35
+ }
36
+
37
+ const defaultParts = (defaultCronExpression || '* * * * * *').split(' ');
38
+ if (defaultParts.length === 5) {
39
+ defaultParts.unshift('*');
40
+ }
41
+ console.log({cronMask})
42
+ setParts({
43
+ second: cronMask && !cronMask[0] ? '0' : cronParts[0] || defaultParts[0] || '*',
44
+ minute: cronMask && !cronMask[1] ? '0' : cronParts[1] || defaultParts[1] || '*',
45
+ hour: cronMask && !cronMask[2] ? '0' : cronParts[2] || defaultParts[2] || '*',
46
+ dayOfMonth: cronMask && !cronMask[3] ? '0' : cronParts[3] || defaultParts[3] || '*',
47
+ month: cronMask && !cronMask[4] ? '0' : cronParts[4] || defaultParts[4] || '*',
48
+ dayOfWeek: cronMask && !cronMask[5] ? '0' : cronParts[5] || defaultParts[5] || '*',
49
+ });
50
+ setIsReady(true);
51
+ }, [cronExpression, defaultCronExpression]); // Ajouter defaultCronExpression aux dépendances
52
+
53
+ // Met à jour la chaîne CRON complète et sa version lisible.
54
+ // Ce `useEffect` se déclenche lorsque les `parts` ou la langue changent.
55
+ useEffect(() => {
56
+ // **CORRECTION CLÉ :** On ne fait rien tant que le composant n'est pas prêt.
57
+ // Cela empêche l'appel à `onCronChange` avec l'état par défaut lors du montage initial.
58
+ if (!isReady) {
59
+ return;
60
+ }
61
+
62
+ const newCronString = `${parts.second} ${parts.minute} ${parts.hour} ${parts.dayOfMonth} ${parts.month} ${parts.dayOfWeek}`;
63
+
64
+ // Met à jour le parent si la chaîne a changé.
65
+ // Cette condition est maintenant sûre car elle ne s'exécute qu'après l'initialisation.
66
+ if (newCronString !== cronExpression) {
67
+ onCronChange(newCronString);
68
+ }
69
+
70
+ // Met à jour la version lisible (logique inchangée).
71
+ try {
72
+ const lang = (i18n.language || 'en').split('-')[0];
73
+ setHumanReadable(cronstrue.toString(newCronString, { locale: lang }));
74
+ } catch (e) {
75
+ setHumanReadable(t('cron.invalid', 'Expression CRON invalide'));
76
+ }
77
+ }, [parts, i18n.language, isReady]); // `isReady` est ajouté aux dépendances.
78
+
79
+ const handlePartChange = (partName, newValue) => {
80
+ // Si une partie devient invalide, CronPartBuilder renvoie `*`.
81
+ // Ce changement est géré normalement.
82
+ setParts(p => ({ ...p, [partName]: newValue || '*' }));
83
+ };
84
+
85
+ const cronPartsConfig = [
86
+ { name: 'second', label: t('cron.second', 'Seconde'), min: 0, max: 59 },
87
+ { name: 'minute', label: t('cron.minute', 'Minute'), min: 0, max: 59 },
88
+ { name: 'hour', label: t('cron.hour', 'Heure'), min: 0, max: 23 },
89
+ { name: 'dayOfMonth', label: t('cron.dayOfMonth', 'Jour du mois'), min: 1, max: 31 },
90
+ { name: 'month', label: t('cron.month', 'Mois'), min: 1, max: 12 },
91
+ { name: 'dayOfWeek', label: t('cron.dayOfWeek', 'Jour de la semaine'), min: 0, max: 6 },
92
+ ];
93
+
94
+ return (
95
+ <div className="cron-builder-wrapper">
96
+ <div className="cron-builder-inputs">
97
+ {cronPartsConfig.map((part,ind)=>
98
+ {
99
+ return <div className="cron-part" key={part.name}>
100
+ <label htmlFor={`cron-${part.name}`}>{part.label}</label>
101
+ <CronPartBuilder
102
+ label={part.label}
103
+ id={`cron-${part.name}`}
104
+ value={parts[part.name]}
105
+ masked={!(!cronMask || cronMask[ind])}
106
+ onChange={(val) => handlePartChange(part.name, val)}
107
+ min={part.min}
108
+ max={part.max}
109
+ /></div>
110
+ })}
111
+ </div>
112
+ <div className="cron-builder-output">
113
+ <div className="cron-string-preview">
114
+ <span className="label">{t('cron.expression', 'Expression CRON :')}</span>
115
+ <code>{cronExpression}</code>
116
+ </div>
117
+ <div className="cron-human-readable">
118
+ <span className="label">{t('cron.preview', 'Aperçu :')}</span>
119
+ <span className="preview-text">{humanReadable}</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export default CronBuilder;
@@ -0,0 +1,128 @@
1
+ // C:/Dev/hackersonline-engine/client/src/CronBuilder.scss
2
+
3
+ // J'utilise ici des variables pour les couleurs et espacements.
4
+ // Si votre projet a déjà un fichier de variables SCSS, il serait bon de les utiliser
5
+ // pour une cohérence parfaite.
6
+ $primary-border-color: #e0e0e0;
7
+ $secondary-text-color: #555;
8
+ $code-bg-color: #f0f0f0;
9
+ $code-text-color: #c7254e;
10
+ $input-bg-color: #fff;
11
+ $input-border-color: #ccc;
12
+ $input-focus-border-color: #337ab7;
13
+ $label-color: #333;
14
+ $preview-bg-color: #f9f9f9;
15
+ $gap-small: 8px;
16
+ $gap-medium: 16px;
17
+ $border-radius: 4px;
18
+
19
+ .cron-builder-wrapper {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: $gap-medium;
23
+ padding: $gap-medium;
24
+ border: 1px solid $primary-border-color;
25
+ border-radius: $border-radius;
26
+ background-color: #fff; // Fond clair
27
+ width: 100%;
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ .cron-builder-inputs {
32
+ display: flex;
33
+ flex-wrap: wrap; // Permet aux éléments de passer à la ligne sur mobile
34
+ gap: $gap-medium;
35
+ align-items: flex-end; // Aligne les champs sur le bas pour un rendu net
36
+ }
37
+
38
+ .cron-part {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: $gap-small / 2;
42
+ flex: 1 1 120px; // Permet aux champs de s'adapter en largeur
43
+
44
+ label {
45
+ font-size: 0.875rem;
46
+ color: $label-color;
47
+ font-weight: 500;
48
+ margin-bottom: 2px;
49
+ }
50
+
51
+ // En supposant que CronPartBuilder génère un input ou une div stylisable.
52
+ // Le style s'appliquera à l'input à l'intérieur.
53
+ input {
54
+ width: 100%;
55
+ padding: $gap-small;
56
+ border: 1px solid $input-border-color;
57
+ border-radius: $border-radius;
58
+ background-color: $input-bg-color;
59
+ font-family: monospace;
60
+ font-size: 1rem;
61
+ line-height: 1.5;
62
+ box-sizing: border-box;
63
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
64
+
65
+ &:focus {
66
+ outline: none;
67
+ border-color: $input-focus-border-color;
68
+ box-shadow: 0 0 0 2px rgba($input-focus-border-color, 0.25);
69
+ }
70
+ }
71
+ }
72
+
73
+ .cron-builder-output {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: $gap-small;
77
+ padding: $gap-small $gap-medium;
78
+ background-color: $preview-bg-color;
79
+ border-radius: $border-radius;
80
+ border-top: 1px solid $primary-border-color;
81
+ font-size: 0.9rem;
82
+
83
+ .cron-string-preview, .cron-human-readable {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: $gap-small;
87
+ flex-wrap: wrap;
88
+ }
89
+
90
+ .label {
91
+ font-weight: 600;
92
+ color: $secondary-text-color;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ code {
97
+ background-color: $code-bg-color;
98
+ color: $code-text-color;
99
+ padding: 3px 6px;
100
+ border-radius: $border-radius;
101
+ font-family: monospace;
102
+ }
103
+
104
+ .preview-text {
105
+ color: #000;
106
+ font-style: italic;
107
+ }
108
+ }
109
+
110
+ // Ajustements pour les {
111
+ .cron-builder-inputs {
112
+ gap: $gap-small;
113
+ }
114
+
115
+ .cron-part {
116
+ // Sur tablette, 3 champs par ligne
117
+ flex-basis: calc(33.333% - #{$gap-small});
118
+ label {
119
+ font-weight: bold;
120
+ }
121
+ }
122
+
123
+ @media (max-width: 480px) {
124
+ .cron-part {
125
+ // Sur mobile, 2 champs par ligne pour plus de lisibilité
126
+ flex-basis: calc(50% - #{$gap-small});
127
+ }
128
+ }
@@ -0,0 +1,150 @@
1
+ // C:/Dev/hackersonline-engine/client/src/CronPartBuilder.jsx
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { SelectField, TextField, NumberField } from './Field.jsx';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ /**
8
+ * Ce composant gère une seule partie de l'expression CRON.
9
+ * Il est "contrôlé" : son affichage est entièrement dérivé de la prop `value`
10
+ * et les interactions utilisateur appellent directement `onChange` avec la nouvelle valeur.
11
+ * Cette version renforce la logique de parsing pour éviter les interprétations incorrectes.
12
+ */
13
+ export const CronPartBuilder = ({ label, masked, value, defaultValue, onChange, min, max, id }) => {
14
+ const { t } = useTranslation();
15
+
16
+
17
+ const { mode, specificValue, intervalValue, rangeStart, rangeEnd } = useMemo(() => {
18
+ const initialState = {
19
+ mode: 'every',
20
+ specificValue: '',
21
+ intervalValue: 5,
22
+ rangeStart: min,
23
+ rangeEnd: max,
24
+ };
25
+
26
+ // --- CORRECTION ICI ---
27
+ // On utilise la prop `defaultValue` quand le champ est masqué.
28
+ const valStr = String(value || '');
29
+ // --- FIN DE LA CORRECTION ---
30
+
31
+ if (valStr === '*') {
32
+ return { ...initialState, mode: 'every' };
33
+ }
34
+ // ... (le reste du useMemo est correct)
35
+ if (valStr.includes('/')) {
36
+ const parts = valStr.split('/');
37
+ // Assurez-vous que la valeur par défaut est bien parsée si elle est de type intervalle
38
+ return { ...initialState, mode: 'interval', intervalValue: parseInt(parts[1], 10) || 5 };
39
+ }
40
+ if (valStr.includes('-')) {
41
+ const parts = valStr.split('-');
42
+ const start = parseInt(parts[0], 10);
43
+ const end = parseInt(parts[1], 10);
44
+ return { ...initialState, mode: 'range', rangeStart: isNaN(start) ? min : start, rangeEnd: isNaN(end) ? max : end };
45
+ }
46
+ if (valStr.includes(',')) {
47
+ return { ...initialState, mode: 'specific_multiple', specificValue: valStr };
48
+ }
49
+ if (/^\d+$/.test(valStr)) {
50
+ return { ...initialState, mode: 'specific_single', specificValue: valStr };
51
+ }
52
+
53
+ return initialState;
54
+
55
+ }, [value, min, max, masked, defaultValue]);
56
+
57
+ // GESTIONNAIRES D'ÉVNEMENTS : Calculent la nouvelle chaîne et appellent `onChange` directement.
58
+
59
+ const handleModeChange = (item) => {
60
+ const newMode = item.value;
61
+ let newValue = defaultValue;
62
+ switch (newMode) {
63
+ case 'every':
64
+ newValue = '*';
65
+ break;
66
+ case 'specific_single':
67
+ newValue = String(min);
68
+ break;
69
+ case 'specific_multiple':
70
+ newValue = `${min},${min + 1 > max ? min : min + 1}`;
71
+ break;
72
+ case 'interval':
73
+ newValue = '*/5';
74
+ break;
75
+ case 'range':
76
+ newValue = `${min}-${max}`;
77
+ break;
78
+ default:
79
+ newValue = defaultValue;
80
+ }
81
+ if (newValue !== value) {
82
+ onChange(newValue);
83
+ }
84
+ };
85
+
86
+ const handleSpecificValueChange = (e) => {
87
+ const cleanedValue = e.target.value.replace(/[^0-9,]/g, '');
88
+ onChange(cleanedValue || '*'); // Retour à '*' si le champ est vide
89
+ };
90
+
91
+ const handleIntervalChange = (e) => {
92
+ const newInterval = parseInt(e.target.value, 10) || 1;
93
+ onChange(`*/${newInterval}`);
94
+ };
95
+
96
+ const handleRangeChange = (part, e) => {
97
+ const newRangeValue = parseInt(e.target.value, 10);
98
+ if (isNaN(newRangeValue)) return; // Ne pas mettre à jour si l'entrée n'est pas un nombre
99
+
100
+ if (part === 'start') {
101
+ onChange(`${newRangeValue}-${rangeEnd}`);
102
+ } else {
103
+ onChange(`${rangeStart}-${newRangeValue}`);
104
+ }
105
+ };
106
+
107
+ const modeOptions = [
108
+ { value: 'every', label: t('cron.every', `Chaque {{label}}`, {label:label.toLowerCase()}) },
109
+ { value: 'specific_single', label: t('cron.specific_single', `Spécifique (ex: 15)`) },
110
+ { value: 'specific_multiple', label: t('cron.specific_multiple', `Spécifiques (ex: 0,15,30)`) },
111
+ { value: 'interval', label: t('cron.interval', `Toutes les X {{label}}s`, {label:label.toLowerCase()}) },
112
+ { value: 'range', label: t('cron.range', `Plage (ex: 9-17)`) },
113
+ ];
114
+
115
+ console.log({label, masked})
116
+
117
+ return (
118
+ <div className="cron-part-controls">
119
+ <SelectField disabled={masked} items={modeOptions} value={mode} onChange={handleModeChange} />
120
+
121
+ <div className="cron-part-inputs">
122
+ {['specific_single', 'specific_multiple'].includes(mode) && (
123
+ <TextField
124
+ id={id}
125
+ disabled={masked}
126
+ value={specificValue}
127
+ onChange={handleSpecificValueChange}
128
+ placeholder={mode === 'specific_single' ? '15' : '0,15,30,45'}
129
+ />
130
+ )}
131
+ {mode === 'interval' && (
132
+ <NumberField
133
+ id={id}
134
+ value={intervalValue}
135
+ onChange={handleIntervalChange}
136
+ min={1}
137
+ max={max}
138
+ />
139
+ )}
140
+ {mode === 'range' && (
141
+ <div className="range-inputs">
142
+ <NumberField id={id} value={rangeStart} onChange={(e) => handleRangeChange('start', e)} min={min} max={max} />
143
+ <span>-</span>
144
+ <NumberField value={rangeEnd} onChange={(e) => handleRangeChange('end', e)} min={min} max={max} />
145
+ </div>
146
+ )}
147
+ </div>
148
+ </div>
149
+ );
150
+ };