data-primals-engine 1.0.10 → 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 +5390 -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,211 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {useMutation, useQuery, useQueryClient} from 'react-query';
|
|
3
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
4
|
+
import { FaBoxOpen, FaChevronLeft, FaDownload, FaStar, FaTag, FaUser } from 'react-icons/fa';
|
|
5
|
+
import Button from './Button.jsx';
|
|
6
|
+
import { useNotificationContext } from './NotificationProvider.jsx';
|
|
7
|
+
import { useModelContext } from './contexts/ModelContext.jsx';
|
|
8
|
+
|
|
9
|
+
import './PackGallery.scss';
|
|
10
|
+
import {elementsPerPage} from "data-primals-engine/constants";
|
|
11
|
+
|
|
12
|
+
// --- API Fetching Functions ---
|
|
13
|
+
const fetchPacks = async (sortBy, lang) => {
|
|
14
|
+
const response = await fetch(`/api/packs?lang=${lang}&sortBy=${sortBy.field}&order=${sortBy.order}`);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error('Network response was not ok');
|
|
17
|
+
}
|
|
18
|
+
return response.json();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const fetchPackDetails = async (packId, lang) => {
|
|
22
|
+
if (!packId) return null;
|
|
23
|
+
const response = await fetch(`/api/packs/${packId}?lang=${lang}`);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error('Failed to fetch pack details');
|
|
26
|
+
}
|
|
27
|
+
return response.json();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// --- Sub-components ---
|
|
31
|
+
|
|
32
|
+
// Carte pour afficher un pack dans la galerie
|
|
33
|
+
const PackCard = ({ pack, onSelect }) => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
return (
|
|
36
|
+
<div className="pack-card" onClick={() => onSelect(pack._id)}>
|
|
37
|
+
<div className="pack-card-header">
|
|
38
|
+
<h3><FaBoxOpen /> {pack.name}</h3>
|
|
39
|
+
<span className="pack-stars"><FaStar /> {pack.stars || 0}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<p className="pack-description">{pack.description?.substring(0, 120) || t('packs.noDescription', 'Aucune description.')}... </p>
|
|
42
|
+
<div className="pack-card-footer">
|
|
43
|
+
<span className="pack-author"><FaUser /> {pack._user}</span>
|
|
44
|
+
<div className="pack-tags">
|
|
45
|
+
{(pack.tags || []).map(tag => <span key={tag} className="tag"><FaTag /> {tag}</span>)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Vue détaillée d'un pack
|
|
53
|
+
const PackDetail = ({ packId, onBack }) => {
|
|
54
|
+
const { t,i18n } = useTranslation();
|
|
55
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
56
|
+
const { addNotification } = useNotificationContext();
|
|
57
|
+
const queryClient = useQueryClient();
|
|
58
|
+
const { selectedModel, page, pagedFilters, pagedSort } = useModelContext();
|
|
59
|
+
const { data: pack, isLoading, isError } = useQuery(['packDetails', packId], () => fetchPackDetails(packId, lang));
|
|
60
|
+
|
|
61
|
+
const { mutate: installPack, isLoading: isInstalling } = useMutation(
|
|
62
|
+
installPackMutationFn,
|
|
63
|
+
{
|
|
64
|
+
onSuccess: (data) => {
|
|
65
|
+
addNotification({
|
|
66
|
+
status: 'completed',
|
|
67
|
+
title: t('datatable.pack.install.success', `Pack '${pack.name}' installé avec succès !`)
|
|
68
|
+
});
|
|
69
|
+
gtag('event', 'pack install '+pack.name);
|
|
70
|
+
queryClient.invalidateQueries('api/models');
|
|
71
|
+
queryClient.invalidateQueries(['api/data', selectedModel?.name, 'page', page, elementsPerPage, pagedFilters[selectedModel?.name], pagedSort[selectedModel?.name]]);
|
|
72
|
+
|
|
73
|
+
onBack(); // Retourner à la galerie
|
|
74
|
+
},
|
|
75
|
+
onError: (error) => {
|
|
76
|
+
addNotification({
|
|
77
|
+
status: 'error',
|
|
78
|
+
title: t('datatable.pack.install.error', `Échec de l'installation du pack`),
|
|
79
|
+
message: error.message
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleInstall = async () => {
|
|
86
|
+
addNotification({ status: 'info', title: t('packs.install.started', `Installation du pack '${pack.name}'...`) });
|
|
87
|
+
installPack({ packId: pack._id, lang });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (isLoading) return <div className="spinner-loader"></div>;
|
|
91
|
+
if (isError) return <p className="error-text">{t('packs.error.details', 'Erreur lors du chargement des détails du pack.')}</p>;
|
|
92
|
+
if (!pack) return null;
|
|
93
|
+
|
|
94
|
+
const renderModelsList = (models) => {
|
|
95
|
+
if (!models || models.length === 0) return t('packs.noModelsIncluded', 'Aucun modèle spécifié.');
|
|
96
|
+
|
|
97
|
+
return models.map(model => {
|
|
98
|
+
// Si l'élément est un objet, on prend sa propriété 'name', sinon on prend la chaîne de caractères
|
|
99
|
+
const modelName = typeof model === 'object' && model !== null ? model.name : model;
|
|
100
|
+
return modelName;
|
|
101
|
+
}).join(', ');
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="pack-detail">
|
|
106
|
+
<Button onClick={onBack} className="back-button"><FaChevronLeft /> <Trans i18nKey="btns.back">Retour</Trans></Button>
|
|
107
|
+
<div className="pack-detail-header">
|
|
108
|
+
<h1>{pack.name}</h1>
|
|
109
|
+
<Button onClick={handleInstall} disabled={isInstalling}>
|
|
110
|
+
<FaDownload /> {isInstalling ? t('packs.installing', 'Installation...') : t('packs.install', 'Installer')}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="pack-meta">
|
|
114
|
+
<span><FaUser /> {pack._user}</span>
|
|
115
|
+
<span><FaStar /> {pack.stars || 0}</span>
|
|
116
|
+
<span><FaTag /> {(pack.tags || []).join(', ')}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="pack-content">
|
|
119
|
+
<h2><Trans i18nKey="packs.description">Description</Trans></h2>
|
|
120
|
+
<div className="description-content" dangerouslySetInnerHTML={{__html: pack.description || ''}}/>
|
|
121
|
+
|
|
122
|
+
<h2><Trans i18nKey="packs.modelsIncluded">Modèles inclus</Trans></h2>
|
|
123
|
+
<p>{renderModelsList(pack.models)}</p>
|
|
124
|
+
|
|
125
|
+
<h2><Trans i18nKey="packs.dataPreview">Aperçu des données à installer</Trans></h2>
|
|
126
|
+
<div className="data-preview">
|
|
127
|
+
<pre>{JSON.stringify(pack.data, null, 2)}</pre>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// --- NOUVELLE FONCTION POUR LA MUTATION D'INSTALLATION ---
|
|
136
|
+
const installPackMutationFn = async ({packId, lang}) => {
|
|
137
|
+
const response = await fetch(`/api/packs/${packId}/install?lang=${lang}`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const errorData = await response.json();
|
|
145
|
+
throw new Error(errorData.error || 'Failed to install pack');
|
|
146
|
+
}
|
|
147
|
+
return response.json();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// --- Main Gallery Component ---
|
|
151
|
+
const PackGallery = () => {
|
|
152
|
+
const { t, i18n } = useTranslation();
|
|
153
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
154
|
+
|
|
155
|
+
const [view, setView] = useState('list'); // 'list' ou 'detail'
|
|
156
|
+
const [selectedPackId, setSelectedPackId] = useState(null);
|
|
157
|
+
const [sortBy, setSortBy] = useState({ field: '_updatedAt', order: -1 });
|
|
158
|
+
|
|
159
|
+
const { data: packs, isLoading, isError } = useQuery(['packs', sortBy], () => fetchPacks(sortBy, lang));
|
|
160
|
+
|
|
161
|
+
const handleSelectPack = (packId) => {
|
|
162
|
+
setSelectedPackId(packId);
|
|
163
|
+
setView('detail');
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleBackToList = () => {
|
|
167
|
+
setSelectedPackId(null);
|
|
168
|
+
setView('list');
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const renderContent = () => {
|
|
172
|
+
if (view === 'detail') {
|
|
173
|
+
return <PackDetail packId={selectedPackId} onBack={handleBackToList} />;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Vue 'list'
|
|
177
|
+
if (isLoading) return <div className="spinner-loader"></div>;
|
|
178
|
+
if (isError) return <p className="error-text">{t('packs.error.loading', 'Erreur lors du chargement des packs.')}</p>;
|
|
179
|
+
if (!packs || packs.length === 0) return <p>{t('packs.noPacks', 'Aucun pack n\'est disponible pour le moment.')}</p>;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<div className="gallery-header">
|
|
184
|
+
<h1><Trans i18nKey="packs.galleryTitle">Galerie de Packs</Trans></h1>
|
|
185
|
+
<div className="sort-options">
|
|
186
|
+
<span><Trans i18nKey="packs.sortBy">Trier par :</Trans></span>
|
|
187
|
+
<Button onClick={() => setSortBy({ field: '_updatedAt', order: -1 })} className={sortBy.field === '_updatedAt' ? 'active' : ''}>
|
|
188
|
+
<Trans i18nKey="packs.sort.lastUpdated">Derniers ajouts</Trans>
|
|
189
|
+
</Button>
|
|
190
|
+
<Button onClick={() => setSortBy({ field: 'stars', order: -1 })} className={sortBy.field === 'stars' ? 'active' : ''}>
|
|
191
|
+
<Trans i18nKey="packs.sort.mostStarred">Plus populaires</Trans>
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="pack-list">
|
|
196
|
+
{packs.map(pack => (
|
|
197
|
+
<PackCard key={pack._id} pack={pack} onSelect={handleSelectPack} />
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="pack-gallery-container">
|
|
206
|
+
{renderContent()}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default PackGallery;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
.pack-gallery-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 90vw;
|
|
4
|
+
height: 80vh;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
|
|
8
|
+
.gallery-header {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
align-items: center;
|
|
12
|
+
padding-bottom: 1rem;
|
|
13
|
+
border-bottom: 1px solid #e0e0e0;
|
|
14
|
+
margin-bottom: 1rem;
|
|
15
|
+
h1 {
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
.sort-options {
|
|
19
|
+
display: flex;
|
|
20
|
+
gap: 0.5rem;
|
|
21
|
+
align-items: center;
|
|
22
|
+
.btn.active {
|
|
23
|
+
color: white;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.pack-list {
|
|
29
|
+
display: grid;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
padding: 0.5rem;
|
|
32
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
33
|
+
@media only screen and (min-width: 480px) {
|
|
34
|
+
overflow-y: auto;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.pack-card {
|
|
39
|
+
border: 1px solid #ddd;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 1rem;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: box-shadow 0.2s, transform 0.2s;
|
|
44
|
+
background-color: #fff;
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
48
|
+
transform: translateY(-3px);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pack-card-header {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
align-items: center;
|
|
55
|
+
margin-bottom: 0.5rem;
|
|
56
|
+
h3 {
|
|
57
|
+
margin: 0;
|
|
58
|
+
font-size: 1.2em;
|
|
59
|
+
color: var(--primary-color);
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.5rem;
|
|
63
|
+
}
|
|
64
|
+
.pack-stars {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.3rem;
|
|
68
|
+
color: #f0ad4e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.pack-description {
|
|
73
|
+
font-size: 0.9em;
|
|
74
|
+
color: #666;
|
|
75
|
+
min-height: 50px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.pack-card-footer {
|
|
79
|
+
margin-top: 1rem;
|
|
80
|
+
padding-top: 0.5rem;
|
|
81
|
+
border-top: 1px solid #eee;
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
align-items: center;
|
|
85
|
+
font-size: 0.8em;
|
|
86
|
+
color: #888;
|
|
87
|
+
.pack-author {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 0.3rem;
|
|
91
|
+
}
|
|
92
|
+
.pack-tags {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-wrap: wrap;
|
|
95
|
+
gap: 0.5rem;
|
|
96
|
+
.tag {
|
|
97
|
+
background-color: #f0f0f0;
|
|
98
|
+
padding: 2px 6px;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 0.2rem;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.pack-detail {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
height: 100%;
|
|
112
|
+
.back-button {
|
|
113
|
+
align-self: flex-start;
|
|
114
|
+
margin-bottom: 1rem;
|
|
115
|
+
}
|
|
116
|
+
.pack-detail-header {
|
|
117
|
+
display: flex;
|
|
118
|
+
justify-content: space-between;
|
|
119
|
+
align-items: center;
|
|
120
|
+
h1 { margin: 0; }
|
|
121
|
+
}
|
|
122
|
+
.pack-meta {
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 1rem;
|
|
125
|
+
color: #777;
|
|
126
|
+
margin: 0.5rem 0 1rem 0;
|
|
127
|
+
span {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 0.4rem;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
.pack-content {
|
|
134
|
+
flex-grow: 1;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
padding-right: 1rem;
|
|
137
|
+
h2 {
|
|
138
|
+
border-bottom: 1px solid #eee;
|
|
139
|
+
padding-bottom: 0.3rem;
|
|
140
|
+
margin-top: 1.5rem;
|
|
141
|
+
}
|
|
142
|
+
.data-preview {
|
|
143
|
+
background-color: #2d2d2d;
|
|
144
|
+
color: #f1f1f1;
|
|
145
|
+
padding: 1rem;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
max-height: 300px;
|
|
148
|
+
overflow: auto;
|
|
149
|
+
pre {
|
|
150
|
+
margin: 0;
|
|
151
|
+
font-size: 0.85em;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import Button from "./Button.jsx";
|
|
3
|
+
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
4
|
+
import {useQueryClient} from "react-query";
|
|
5
|
+
import {Trans} from "react-i18next";
|
|
6
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
7
|
+
|
|
8
|
+
export const Pagination = ({
|
|
9
|
+
hasPreviousNext,
|
|
10
|
+
visibleItemsCount,
|
|
11
|
+
onRenderItem,
|
|
12
|
+
totalCount,
|
|
13
|
+
onChange,
|
|
14
|
+
page,
|
|
15
|
+
setPage,
|
|
16
|
+
useParam = false,
|
|
17
|
+
showElementsPerPage=false
|
|
18
|
+
}) => {
|
|
19
|
+
const { elementsPerPage, setElementsPerPage } = useModelContext();
|
|
20
|
+
let pageCount = Math.ceil(totalCount / elementsPerPage);
|
|
21
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
22
|
+
|
|
23
|
+
const handleFirst = (e) => {
|
|
24
|
+
setPage(1);
|
|
25
|
+
onChange?.(1);
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
};
|
|
28
|
+
const handleLast = (e) => {
|
|
29
|
+
setPage(pageCount);
|
|
30
|
+
onChange?.(pageCount);
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
};
|
|
33
|
+
const handlePrevious = (e) => {
|
|
34
|
+
if (page > 1) {
|
|
35
|
+
setPage(page - 1);
|
|
36
|
+
if (useParam) {
|
|
37
|
+
searchParams.set("page", page - 1 + "");
|
|
38
|
+
setSearchParams(
|
|
39
|
+
(params) => {
|
|
40
|
+
params.set("page", page - 1 + "");
|
|
41
|
+
return params;
|
|
42
|
+
},
|
|
43
|
+
{ replace: true },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
onChange?.(page - 1);
|
|
47
|
+
}
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
};
|
|
50
|
+
const handleNext = (e) => {
|
|
51
|
+
if (page < pageCount) {
|
|
52
|
+
setPage(page + 1);
|
|
53
|
+
if (useParam) {
|
|
54
|
+
searchParams.set("page", page + 1 + "");
|
|
55
|
+
setSearchParams(
|
|
56
|
+
(params) => {
|
|
57
|
+
params.set("page", page + 1 + "");
|
|
58
|
+
return params;
|
|
59
|
+
},
|
|
60
|
+
{ replace: true },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
onChange?.(page + 1);
|
|
64
|
+
}
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleChange = (p) => {
|
|
69
|
+
if (p !== page) {
|
|
70
|
+
setPage(p);
|
|
71
|
+
if (useParam) {
|
|
72
|
+
searchParams.set("page", p + "");
|
|
73
|
+
setSearchParams(
|
|
74
|
+
(params) => {
|
|
75
|
+
params.set("page", p + "");
|
|
76
|
+
return params;
|
|
77
|
+
},
|
|
78
|
+
{ replace: true },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
onChange?.(p);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleElementsPerPageChange = (event) => {
|
|
86
|
+
const newPerPage = parseInt(event.target.value, 10);
|
|
87
|
+
if (!isNaN(newPerPage) && newPerPage > 0 && newPerPage <= 100) {
|
|
88
|
+
setElementsPerPage(newPerPage);
|
|
89
|
+
setPage(1);
|
|
90
|
+
onChange?.(1);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return (
|
|
94
|
+
<div className="pagination-wrapper flex">
|
|
95
|
+
{page >= 1 && page <= pageCount && (
|
|
96
|
+
<div className="pagination">
|
|
97
|
+
{hasPreviousNext && page > 1 && (
|
|
98
|
+
<>
|
|
99
|
+
<Button onClick={handleFirst}><<</Button>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
{hasPreviousNext && page > 1 && (
|
|
103
|
+
<>
|
|
104
|
+
<Button onClick={handlePrevious}><</Button>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
{Array.from(
|
|
108
|
+
{length: parseInt(visibleItemsCount, 10)},
|
|
109
|
+
(_, i) => page + i - parseInt(visibleItemsCount / 2, 10),
|
|
110
|
+
).map((p, i) => {
|
|
111
|
+
return p >= 1 && p <= pageCount ? (
|
|
112
|
+
<Button disabled={p === page} onClick={() => handleChange(p)}>
|
|
113
|
+
{p}
|
|
114
|
+
</Button>
|
|
115
|
+
) : (
|
|
116
|
+
<></>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
{hasPreviousNext && page < pageCount && (
|
|
120
|
+
<>
|
|
121
|
+
<Button onClick={handleNext}>></Button>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
{hasPreviousNext && page < pageCount && (
|
|
125
|
+
<>
|
|
126
|
+
<Button onClick={handleLast}>>></Button>
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
{showElementsPerPage&& (<div className="form flex flex-no-wrap elementsPerPage">
|
|
132
|
+
<label htmlFor="elementsPerPage"><Trans i18nKey="pagination.elementsPerPage">Éléments par page:</Trans></label>
|
|
133
|
+
<select id="elementsPerPage" value={elementsPerPage} onChange={handleElementsPerPageChange}>
|
|
134
|
+
<option value="20">20</option>
|
|
135
|
+
<option value="50">50</option>
|
|
136
|
+
<option value="100">100</option>
|
|
137
|
+
</select>
|
|
138
|
+
</div>)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|