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.
- package/client/README.md +8 -0
- package/client/index.html +31 -0
- package/client/index.js +25 -0
- package/client/package-lock.json +11664 -0
- package/client/package.json +70 -0
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
- package/client/public/doc/API.postman_collection.json +214 -0
- package/client/public/github.svg +1 -0
- package/client/public/profilCompany.jpg +0 -0
- package/client/public/profilDeveloper.jpg +0 -0
- package/client/public/profilEngineer.jpg +0 -0
- package/client/public/profilPersonal.jpg +0 -0
- package/client/public/profils .psd +0 -0
- package/client/public/react.svg +9 -0
- package/client/src/APIInfo.jsx +328 -0
- package/client/src/AddWidgetTypeModal.jsx +44 -0
- package/client/src/AddWidgetTypeModal.scss +87 -0
- package/client/src/App.css +42 -0
- package/client/src/App.jsx +586 -0
- package/client/src/App.scss +1466 -0
- package/client/src/AssistantChat.jsx +280 -0
- package/client/src/AssistantChat.scss +229 -0
- package/client/src/Button.jsx +12 -0
- package/client/src/Button.scss +197 -0
- package/client/src/CalculationBuilder.jsx +338 -0
- package/client/src/CalculationStepRow.jsx +199 -0
- package/client/src/CalendarConfigModal.jsx +49 -0
- package/client/src/ChartConfigModal.jsx +402 -0
- package/client/src/ConditionBuilder.jsx +622 -0
- package/client/src/ConditionBuilder.scss +672 -0
- package/client/src/ConditionBuilder2.jsx +757 -0
- package/client/src/ContentView.jsx +274 -0
- package/client/src/CronBuilder.jsx +126 -0
- package/client/src/CronBuilder.scss +128 -0
- package/client/src/CronPartBuilder.jsx +150 -0
- package/client/src/Dashboard.jsx +210 -0
- package/client/src/Dashboard.scss +445 -0
- package/client/src/DashboardChart.jsx +362 -0
- package/client/src/DashboardFlexViewItem.jsx +129 -0
- package/client/src/DashboardView.jsx +554 -0
- package/client/src/DataChart.jsx +349 -0
- package/client/src/DataEditor.jsx +556 -0
- package/client/src/DataLayout.jsx +709 -0
- package/client/src/DataTable.jsx +996 -0
- package/client/src/DataTable.scss +73 -0
- package/client/src/Dialog.jsx +90 -0
- package/client/src/Dialog.scss +117 -0
- package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
- package/client/src/DocumentationPageLayout.jsx +93 -0
- package/client/src/DocumentationPageLayout.scss +262 -0
- package/client/src/Draggable.jsx +222 -0
- package/client/src/Draggable.scss +49 -0
- package/client/src/ExportDialog.jsx +172 -0
- package/client/src/Field.jsx +1558 -0
- package/client/src/FlexBuilder.jsx +269 -0
- package/client/src/FlexBuilder.scss +226 -0
- package/client/src/FlexBuilderControls.jsx +170 -0
- package/client/src/FlexBuilderModal.jsx +75 -0
- package/client/src/FlexBuilderModal.scss +109 -0
- package/client/src/FlexBuilderPreview.jsx +48 -0
- package/client/src/FlexBuilderUtils.js +84 -0
- package/client/src/FlexDataRenderer.jsx +170 -0
- package/client/src/FlexDataRenderer.scss +80 -0
- package/client/src/FlexNode.jsx +290 -0
- package/client/src/FlexTreeUtils.js +129 -0
- package/client/src/HiddenableCell.jsx +46 -0
- package/client/src/HiddenableCell.scss +36 -0
- package/client/src/KPIDialog.jsx +39 -0
- package/client/src/KPIWidget.jsx +139 -0
- package/client/src/KanbanCard.jsx +92 -0
- package/client/src/KanbanColumn.jsx +29 -0
- package/client/src/KanbanConfigModal.jsx +115 -0
- package/client/src/KanbanView.jsx +113 -0
- package/client/src/KanbanView.scss +107 -0
- package/client/src/MessageRotator.jsx +77 -0
- package/client/src/MessageRotator.scss +14 -0
- package/client/src/ModelCreator.jsx +640 -0
- package/client/src/ModelCreator.scss +154 -0
- package/client/src/ModelCreatorField.jsx +810 -0
- package/client/src/ModelImporter.jsx +120 -0
- package/client/src/ModelList.jsx +205 -0
- package/client/src/Notification.jsx +137 -0
- package/client/src/Notification.scss +126 -0
- package/client/src/NotificationProvider.jsx +73 -0
- package/client/src/PackGallery.jsx +211 -0
- package/client/src/PackGallery.scss +156 -0
- package/client/src/Pagination.jsx +141 -0
- package/client/src/RTE.jsx +415 -0
- package/client/src/RTETrans.jsx +103 -0
- package/client/src/RelationField.jsx +247 -0
- package/client/src/RelationValue.jsx +230 -0
- package/client/src/RestoreConfirmationModal.jsx +183 -0
- package/client/src/RestoreDialog.jsx +134 -0
- package/client/src/RestoreDialog.scss +67 -0
- package/client/src/RichTextEditorModal.jsx +34 -0
- package/client/src/RichTextEditorModal.scss +54 -0
- package/client/src/TourSpotlight.jsx +234 -0
- package/client/src/TourSpotlight.scss +100 -0
- package/client/src/TutorialsMenu.jsx +83 -0
- package/client/src/TutorialsMenu.scss +217 -0
- package/client/src/TutorialsRewardModal.jsx +65 -0
- package/client/src/Utils.jsx +78 -0
- package/client/src/ViewSwitcher.jsx +54 -0
- package/client/src/ViewSwitcher.scss +74 -0
- package/client/src/Webpage.jsx +69 -0
- package/client/src/_variables.scss +21 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/constants.js +68 -0
- package/client/src/contexts/AuthContext.jsx +28 -0
- package/client/src/contexts/ModelContext.jsx +328 -0
- package/client/src/contexts/UIContext.jsx +52 -0
- package/client/src/core/data.js +35 -0
- package/client/src/entry-client.jsx +15 -0
- package/client/src/entry-server.jsx +13 -0
- package/client/src/filter.js +222 -0
- package/client/src/hooks/data.js +101 -0
- package/client/src/hooks/useDebounce.js +20 -0
- package/client/src/hooks/useDragAndDrop.js +59 -0
- package/client/src/hooks/useLocalStorage.js +100 -0
- package/client/src/hooks/useMount.js +42 -0
- package/client/src/hooks/useTutorials.jsx +227 -0
- package/client/src/hooks/useWindowSize.js +24 -0
- package/client/src/i18n.js +47 -0
- package/client/src/index.css +90 -0
- package/client/src/index.js +10 -0
- package/client/src/main.jsx +11 -0
- package/client/src/translations.js +15940 -0
- package/client/src/tutorials.js +112 -0
- package/client/vite.config.js +30 -0
- package/eslint.config.js +50 -0
- package/package.json +48 -27
- package/server.js +9 -0
- package/src/constants.js +12 -23
- package/src/core.js +9 -8
- package/src/data.js +81 -80
- package/src/defaultModels.js +114 -115
- package/src/email.js +8 -9
- package/src/engine.js +5 -5
- package/src/events.js +1 -2
- package/src/i18n.js +212 -211
- package/src/middlewares/middleware-mongodb.js +94 -93
- package/src/migrate.js +24 -24
- package/src/modules/assistant.js +55 -55
- package/src/modules/bucket.js +10 -9
- package/src/modules/data.js +387 -357
- package/src/modules/file.js +3 -6
- package/src/modules/mongodb.js +6 -7
- package/src/modules/user.js +24 -47
- package/src/modules/workflow.js +68 -67
- package/src/openai.jobs.js +2 -2
- package/src/packs.js +30 -31
- package/src/providers.js +101 -3
- package/src/setenv.js +1 -1
- package/src/user.js +0 -17
- package/src/workers/crypto-worker.js +21 -20
- package/src/workers/import-export-worker.js +18 -18
- package/test/data.backup.integration.test.js +7 -12
- package/test/data.integration.test.js +27 -25
- package/test/globalSetup.js +2 -2
- package/test/import_export.integration.test.js +6 -6
- package/test/model.integration.test.js +21 -22
- package/test/workflow.integration.test.js +2 -5
- package/test/workflow.robustness.test.js +6 -13
- 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
|
+
};
|