data-primals-engine 1.5.0 → 1.5.1

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 (52) hide show
  1. package/README.md +35 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +12 -0
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +805 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +47 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +92 -53
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +262 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +2 -1
  38. package/src/constants.js +1 -1
  39. package/src/defaultModels.js +1596 -1544
  40. package/src/i18n.js +710 -10
  41. package/src/modules/assistant/assistant.js +148 -18
  42. package/src/modules/bucket.js +2 -1
  43. package/src/modules/data/data.core.js +118 -92
  44. package/src/modules/data/data.history.js +531 -492
  45. package/src/modules/data/data.js +3 -53
  46. package/src/modules/data/data.operations.js +77 -26
  47. package/src/modules/data/data.relations.js +686 -686
  48. package/src/modules/data/data.routes.js +1879 -1821
  49. package/src/modules/data/data.validation.js +81 -2
  50. package/src/modules/file.js +247 -238
  51. package/src/packs.js +5482 -5478
  52. package/test/data.integration.test.js +1115 -1060
@@ -3,7 +3,7 @@ import {Trans, useTranslation} from "react-i18next";
3
3
  import {useAuthContext} from "./contexts/AuthContext.jsx";
4
4
  import {useMutation, useQueryClient} from "react-query";
5
5
  import React, {useEffect, useMemo, useState} from "react";
6
- import {FaBook, FaEdit, FaFileImport, FaPlus} from "react-icons/fa";
6
+ import {FaBook, FaBoxOpen, FaEdit, FaFileImport, FaPlus} from "react-icons/fa";
7
7
  import Button from "./Button.jsx";
8
8
  import useLocalStorage from "./hooks/useLocalStorage.js";
9
9
  import {Tooltip} from "react-tooltip";
@@ -12,7 +12,7 @@ import {profiles} from "./constants.js";
12
12
  import * as FaIcons from "react-icons/fa";
13
13
  import * as Fa6Icons from "react-icons/fa6";
14
14
 
15
-
15
+ // --- SUGGESTION: Extraire cette logique dans un composant dédié si elle devient plus complexe ---
16
16
  // Fonction pour obtenir le composant icône par son nom
17
17
  const getIconComponent = (iconName) => {
18
18
  if (!iconName) return null;
@@ -20,8 +20,22 @@ const getIconComponent = (iconName) => {
20
20
  return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
21
21
  };
22
22
 
23
+ // --- SUGGESTION: Créer un petit composant pour la lisibilité de l'affichage du nom du modèle ---
24
+ const ModelListItemLabel = ({ model, count, isGenerated, t }) => {
25
+ const modelName = t(`model_${model.name}`, model.name);
26
+
27
+ let suffix = '';
28
+ if (isGenerated) {
29
+ suffix = ` (${t('models.status.tmp', 'brouillon')})`; // Utiliser i18n pour 'tmp'
30
+ } else if (count > 0) {
31
+ suffix = ` (${count})`;
32
+ }
33
+
34
+ return <>{modelName}{suffix}</>;
35
+ };
36
+
23
37
 
24
- export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditModel, onAPIInfo, onNewData }) {
38
+ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditModel, onAPIInfo, onNewData, onImportPack }) {
25
39
  const {allTourSteps, setIsTourOpen,setCurrentTourSteps, setTourStepIndex, currentTour, setCurrentTour} = useUI();
26
40
 
27
41
  const {models, setSelectedModel, selectedModel, countByModel, generatedModels} = useModelContext();
@@ -29,6 +43,7 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
29
43
  const {me} = useAuthContext();
30
44
  const queryClient = useQueryClient()
31
45
  const [searchTerm, setSearchTerm] = useState('');
46
+ const [selectedTag, setSelectedTag] = useLocalStorage('modelList-selectedTag', 'all');
32
47
 
33
48
  const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
34
49
  useEffect(() =>{
@@ -37,20 +52,38 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
37
52
  }
38
53
  }, [me])
39
54
 
40
- const filteredModels = useMemo(() => {
55
+ const allTags = useMemo(() => {
41
56
  if (!models) return [];
42
- if (!searchTerm.trim()) return models; // Si la recherche est vide, retourne tous les modèles
43
-
44
- const lowerSearchTerm = searchTerm.toLowerCase();
45
- return models.filter(model => {
46
- return (model.name && t('model_' + model.name, model.name).toLowerCase().includes(lowerSearchTerm)) ||
47
- (model.fields.some(f =>
48
- (model.icon && model.icon.toLowerCase().includes(lowerSearchTerm)) ||
49
- f.name?.toLowerCase().includes(lowerSearchTerm) ||
50
- f.hint?.toLowerCase().includes(lowerSearchTerm)) ||
51
- (model.description && model.description.toLowerCase().includes(lowerSearchTerm)))
57
+ const tagsSet = new Set();
58
+ models
59
+ .filter(model => model._user === me?.username) // On ne prend que les tags des modèles de l'utilisateur
60
+ .forEach(model => {
61
+ if (model.tags && Array.isArray(model.tags)) {
62
+ model.tags.forEach(tag => tagsSet.add(tag));
63
+ }
52
64
  });
53
- }, [models, searchTerm]);
65
+ return Array.from(tagsSet).sort();
66
+ }, [models, me?.username]);
67
+
68
+ console.log(allTags,"t");
69
+ const filteredModels = useMemo(() => {
70
+ if (!models) return [];
71
+ let results = models.filter(model => model._user === me?.username);
72
+
73
+ // Filtrage par tag
74
+ if (selectedTag && selectedTag !== 'all') {
75
+ results = results.filter(model => model.tags?.includes(selectedTag));
76
+ }
77
+
78
+ // Filtrage par terme de recherche
79
+ if (searchTerm.trim()) {
80
+ const lowerSearchTerm = searchTerm.toLowerCase();
81
+ results = results.filter(model =>
82
+ (t('model_' + model.name, model.name).toLowerCase().includes(lowerSearchTerm)) ||
83
+ (model.description && model.description.toLowerCase().includes(lowerSearchTerm)));
84
+ }
85
+ return results;
86
+ }, [models, searchTerm, selectedTag, me?.username, t]);
54
87
 
55
88
  const handleSelectModel = (model) => {
56
89
  setSelectedModel(model);
@@ -96,28 +129,6 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
96
129
  });
97
130
  };
98
131
 
99
- const [mods, setMods] = useState([]);
100
- useEffect(() => {
101
- if( countByModel && selectedModel)
102
- {
103
- setMods((m) => {
104
- const counts = [...m];
105
- const mod = counts.find(f => f.mod === selectedModel.name);
106
- if( mod ){
107
- mod.count = countByModel[selectedModel.name];
108
- return counts.map((c => {
109
- if( c.mod === mod.mod) {
110
- return {...mod};
111
- }
112
- return c;
113
- }));
114
- }else{
115
- return [...m, { mod: selectedModel.name, count: countByModel[selectedModel.name]}];
116
- }
117
- })
118
- }
119
- }, [countByModel, selectedModel]);
120
-
121
132
  // --- NEW: Effect to update tour steps when a profile is already selected on mount ---
122
133
  useEffect(() => {
123
134
  if (currentProfile && currentTour && /^demo[0-9]{1,2}$/.test(me?.username)) { // Only run on demo user for simplicity
@@ -163,9 +174,9 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
163
174
  }} />
164
175
  </div></div>}
165
176
  <div className="models">
166
- <h2><Trans i18nKey="models">Modèles</Trans></h2>
177
+ <h2 className={"field-bg p-2"}><Trans i18nKey="models">Modèles</Trans></h2>
167
178
  <div className="model-list-container">
168
- <div className="model-list-search-bar-container">
179
+ <div className="flex flex-no-wrap model-list-search-bar-container">
169
180
  <input
170
181
  type="search"
171
182
  placeholder={t('models.searchPlaceholder', 'Rechercher un modèle...')}
@@ -173,6 +184,18 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
173
184
  onChange={(e) => setSearchTerm(e.target.value)}
174
185
  className="model-list-search-input"
175
186
  />
187
+ {allTags.length > 0 && (
188
+ <div className="model-list-tag-filter">
189
+ <select value={selectedTag} onChange={(e) => setSelectedTag(e.target.value)}>
190
+ <option value="all">{t('models.tags.all', 'Tous les tags')}</option>
191
+ {allTags.map(tag => (
192
+ <option key={tag} value={tag}>
193
+ {t(`tags.${tag}`, tag.charAt(0).toUpperCase() + tag.slice(1))}
194
+ </option>
195
+ ))}
196
+ </select>
197
+ </div>
198
+ )}
176
199
  </div>
177
200
  {filteredModels.length > 0 ? (
178
201
  <div className="model-list">
@@ -185,15 +208,23 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
185
208
 
186
209
  const IconComponent = getIconComponent(model.icon);
187
210
 
211
+ // --- SUGGESTION: Rendre le comportement du clic prévisible ---
212
+ // Le clic principal sélectionne toujours le modèle. L'édition est une action secondaire via son bouton.
213
+ const handleItemClick = () => {
214
+ onModelSelect(model);
215
+ };
216
+
188
217
  return (
189
218
  <li data-testid={'model_'+model.name} className={`${model.name === selectedModel?.name ? 'active' : ''}`}
190
- key={'modelist' + model.name} onClick={() => generatedModels.some(g => g.name === model.name) ? onEditModel(model) : onModelSelect(model)}>
219
+ key={'modellist' + model.name} onClick={handleItemClick}>
191
220
  <div className="flex flex-center flex-fw">
192
221
  <div
193
222
  className="flex flex-1 flex-no-wrap break-word gap-2">
194
223
  <div className={"icon"}>{IconComponent ? IconComponent : <></>}</div>
195
- <div>{t(`model_${model.name}`, model.name)} {generatedModels.some(f => f.name === model.name) ? '(tmp)' : (mods.some(f => f.mod === model.name) ? `(${mods.find(f => f.mod === model.name).count})` : '')}</div></div>
224
+ {/* --- SUGGESTION: Utiliser le composant pour la clarté --- */}
225
+ <div><ModelListItemLabel model={model} count={countByModel?.[model.name]} isGenerated={generatedModels.some(f => f.name === model.name)} t={t} /></div></div>
196
226
  <div className="btns">
227
+ {/* Le bouton "Ajouter" est conditionnel, ce qui est bien */}
197
228
  {!generatedModels.some(g => g.name === model.name) && (<button data-tooltip-id="tooltipML" data-tooltip-content={t('btns.addData')} onClick={(e) => {
198
229
  e.stopPropagation();
199
230
  e.preventDefault();
@@ -216,21 +247,29 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
216
247
  </li>)
217
248
  })}
218
249
  </ul>
219
-
220
250
  </div>) : (
221
- <p className="no-models-found">
222
- {searchTerm ? t('models.noMatch', 'Aucun modèle ne correspond à votre recherche.') : t('models.noModels', 'Aucun modèle disponible.')}
223
- </p>
251
+ <div className="empty-state-container">
252
+ <div className="empty-state-content">
253
+ <div className="empty-state-icon">
254
+ {/* Une icône SVG simple pour représenter des "blocs de construction" ou des "données" */}
255
+ <svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
256
+ <path d="M14 10L14 4L20 4L20 10L14 10Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
257
+ <path d="M4 20L4 14L10 14L10 20L4 20Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
258
+ <path d="M4 10L4 4L10 4L10 10L4 10Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
259
+ <path d="M14 20L14 14L20 14L20 20L14 20Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
260
+ </svg>
261
+ </div>
262
+ <h3>{t('models.empty.title', 'Commencez à structurer vos données')}</h3>
263
+ <p>
264
+ {searchTerm ?
265
+ t('models.noMatch', 'Aucun modèle ne correspond à votre recherche.') :
266
+ t('models.empty.description', 'Créez votre premier modèle de A à Z ou importez une structure existante pour démarrer rapidement.')
267
+ }
268
+ </p>
269
+ </div>
270
+ </div>
224
271
  )}
225
- <div className="flex actions">
226
- <Button onClick={onCreateModel} className="btn tourStep-create-model"><FaPlus/><Trans
227
- i18nKey="btns.createModel">Créer un modèle</Trans></Button>
228
- <Button onClick={onImportModel}
229
- className="btn tourStep-import-model btn-primary"><FaFileImport/><Trans
230
- i18nKey="btns.importModels">Importer un modèle</Trans></Button>
231
- </div>
232
272
  </div>
233
273
  </div>
234
274
  </>
235
275
  }
236
-
@@ -1,137 +1,137 @@
1
- import React, {useState, useEffect, useRef, useCallback, forwardRef} from 'react';
2
- import { FaCheck, FaSpinner, FaExclamationTriangle, FaBell, FaTimes, FaTrash } from 'react-icons/fa'; // Example icons
3
- import { useNotificationContext } from './NotificationProvider.jsx'; // Import the context
4
- import './Notification.scss';
5
-
6
- /*
7
- const { addNotification } = useNotificationContext();
8
- const notificationData = {
9
- title: 'New Notification',
10
- message: 'This is a new notification message.',
11
- icon: <FaBell />,
12
- status: ['ongoing','completed','error'][Math.floor(Math.random()*3)]
13
- };
14
- */
15
- // Helper function to generate a unique ID
16
- const generateNotificationId = () => {
17
- return Math.random().toString(36).substring(2, 9);
18
- };
19
-
20
- const Notification = ({ notification, onClick }) => {
21
- const { markAsRead, removeNotification } = useNotificationContext();
22
- const [dismissTimer, setDismissTimer] = useState(null);
23
- const notificationRef = useRef(null);
24
- const isMounted = useRef(false)
25
-
26
- const handleMarkAsRead = useCallback(() => {
27
- markAsRead(notification.id);
28
- }, [markAsRead, notification.id]);
29
-
30
- useEffect(() => {
31
- const t = setTimeout(()=>{
32
- markAsRead(notification.id);
33
- }, notification.timeout || 12000);
34
- return () => {
35
- clearTimeout(t);
36
- }
37
- }, [notification]);
38
-
39
- useEffect(() => {
40
- if (notification.isRead) {
41
- const timer = setTimeout(() => {
42
- removeNotification(notification.id);
43
- }, 30000);
44
-
45
- return () => {
46
- clearTimeout(timer);
47
- };
48
- }
49
- }, [notification.isRead, notification.id, removeNotification]);
50
-
51
- let statusIcon;
52
- switch (notification.status) {
53
- case 'ongoing':
54
- statusIcon = <FaSpinner className="spinner" />;
55
- break;
56
- case 'completed':
57
- statusIcon = <FaCheck />;
58
- break;
59
- case 'error':
60
- statusIcon = <FaExclamationTriangle />;
61
- break;
62
- default:
63
- statusIcon = null;
64
- }
65
-
66
- const removeBtn = notification.isRead ? <button onClick={() => {
67
- removeNotification(notification.id);
68
- }}><FaTrash /></button> : <></>
69
- return (
70
- <div ref={notificationRef} onClick={onClick} className={`notification ${notification.isExpanded ? 'expanded' : 'collapsed'} ${notification.status}`}>
71
- <div className="notification-header">
72
- {notification.icon && <span className="notification-icon">{notification.icon}</span>}
73
- <span className="notification-title">{notification.title}</span>
74
- {statusIcon && <span className="notification-status">{statusIcon}</span>}
75
- {removeBtn}
76
- </div>
77
- {<div className="notification-content">
78
- <p className="notification-message">{notification.message}</p>
79
- </div>}
80
- </div>
81
- );
82
- };
83
-
84
- const NotificationList = forwardRef((props, ref) => {
85
- const { notifications, markAsRead, removeNotification, addNotification, unreadCount } = useNotificationContext();
86
- const [showBubble, setShowBubble] = useState(false);
87
-
88
-
89
- const toggleBubble = useCallback(() => {
90
- setShowBubble(!showBubble)
91
- }, [showBubble]);
92
-
93
- useEffect(() => {
94
- if( unreadCount > 0 ){
95
- if (!showBubble)
96
- setShowBubble(true);
97
- }else{
98
- if (showBubble)
99
- setShowBubble(false);
100
- }
101
- }, [unreadCount]);
102
-
103
- const readNotifications = notifications.filter(n => n.isRead) || [];
104
- const unreadNotifications = notifications.filter(n => !n.isRead) || [];
105
- return (
106
- <div className="notification-container">
107
- <div className="notifications-right">
108
- {unreadNotifications.map(notification => (
109
- <Notification
110
- onClick={() => {
111
- markAsRead(notification.id)
112
- }}
113
- key={notification.id}
114
- notification={notification}
115
- />
116
- ))}
117
- {unreadCount > 0 && <div className={"notification-bubble-count"}>{unreadCount}</div>}
118
- </div>
119
- <div className={`notification-bubble ${showBubble ? 'visible' : ''}`}>
120
- <button onClick={toggleBubble} className="notification-bubble-btn">
121
- <FaBell/>
122
- {unreadCount > 0 && <span className="notification-bubble-count">{unreadCount}</span>}
123
- </button>
124
- {showBubble && <div className='notifications-bubble-content'>
125
- {readNotifications.map(notification => (
126
- <Notification
127
- key={notification.id}
128
- notification={notification}
129
- />
130
- ))}
131
- </div>}
132
- </div>
133
- </div>
134
- );
135
- });
136
- NotificationList.displayName = "NotificationList";
1
+ import React, {useState, useEffect, useRef, useCallback, forwardRef} from 'react';
2
+ import { FaCheck, FaSpinner, FaExclamationTriangle, FaBell, FaTimes, FaTrash } from 'react-icons/fa'; // Example icons
3
+ import { useNotificationContext } from './NotificationProvider.jsx'; // Import the context
4
+ import './Notification.scss';
5
+
6
+ /*
7
+ const { addNotification } = useNotificationContext();
8
+ const notificationData = {
9
+ title: 'New Notification',
10
+ message: 'This is a new notification message.',
11
+ icon: <FaBell />,
12
+ status: ['ongoing','completed','error'][Math.floor(Math.random()*3)]
13
+ };
14
+ */
15
+ // Helper function to generate a unique ID
16
+ const generateNotificationId = () => {
17
+ return Math.random().toString(36).substring(2, 9);
18
+ };
19
+
20
+ const Notification = ({ notification, onClick }) => {
21
+ const { markAsRead, removeNotification } = useNotificationContext();
22
+ const [dismissTimer, setDismissTimer] = useState(null);
23
+ const notificationRef = useRef(null);
24
+ const isMounted = useRef(false)
25
+
26
+ const handleMarkAsRead = useCallback(() => {
27
+ markAsRead(notification.id);
28
+ }, [markAsRead, notification.id]);
29
+
30
+ useEffect(() => {
31
+ const t = setTimeout(()=>{
32
+ markAsRead(notification.id);
33
+ }, notification.timeout || 12000);
34
+ return () => {
35
+ clearTimeout(t);
36
+ }
37
+ }, [notification]);
38
+
39
+ useEffect(() => {
40
+ if (notification.isRead) {
41
+ const timer = setTimeout(() => {
42
+ removeNotification(notification.id);
43
+ }, 30000);
44
+
45
+ return () => {
46
+ clearTimeout(timer);
47
+ };
48
+ }
49
+ }, [notification.isRead, notification.id, removeNotification]);
50
+
51
+ let statusIcon;
52
+ switch (notification.status) {
53
+ case 'ongoing':
54
+ statusIcon = <FaSpinner className="spinner" />;
55
+ break;
56
+ case 'completed':
57
+ statusIcon = <FaCheck />;
58
+ break;
59
+ case 'error':
60
+ statusIcon = <FaExclamationTriangle />;
61
+ break;
62
+ default:
63
+ statusIcon = null;
64
+ }
65
+
66
+ const removeBtn = notification.isRead ? <button onClick={() => {
67
+ removeNotification(notification.id);
68
+ }}><FaTrash /></button> : <></>
69
+ return (
70
+ <div ref={notificationRef} onClick={onClick} className={`notification ${notification.isExpanded ? 'expanded' : 'collapsed'} ${notification.status}`}>
71
+ <div className="notification-header">
72
+ {notification.icon && <span className="notification-icon">{notification.icon}</span>}
73
+ <span className="notification-title">{notification.title}</span>
74
+ {statusIcon && <span className="notification-status">{statusIcon}</span>}
75
+ {removeBtn}
76
+ </div>
77
+ {<div className="notification-content">
78
+ <p className="notification-message">{notification.message}</p>
79
+ </div>}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ const NotificationList = forwardRef((props, ref) => {
85
+ const { notifications, markAsRead, removeNotification, addNotification, unreadCount } = useNotificationContext();
86
+ const [showBubble, setShowBubble] = useState(false);
87
+
88
+ const toggleBubble = useCallback(() => {
89
+ // Lorsque l'on ouvre la bulle, on marque toutes les notifications comme lues.
90
+ if (!showBubble) {
91
+ notifications.forEach(n => {
92
+ if (!n.isRead) {
93
+ markAsRead(n.id);
94
+ }
95
+ });
96
+ }
97
+ // Ensuite, on bascule la visibilité du contenu de la bulle.
98
+ setShowBubble(prev => !prev);
99
+ }, [showBubble, notifications, markAsRead]);
100
+
101
+ const readNotifications = notifications.filter(n => n.isRead) || [];
102
+ const unreadNotifications = notifications.filter(n => !n.isRead) || [];
103
+ return (
104
+ <><div className="notification-container">
105
+ <div className="notifications-right">
106
+ {unreadNotifications.map(notification => (
107
+ <Notification
108
+ onClick={() => {
109
+ markAsRead(notification.id)
110
+ }}
111
+ key={notification.id}
112
+ notification={notification}
113
+ />
114
+ ))}
115
+ {unreadCount > 0 && <div className={"notification-bubble-count"}>{unreadCount}</div>}
116
+ </div>
117
+ </div>
118
+
119
+ {unreadCount > 0 && <button onClick={toggleBubble} className={`notification-bubble fab ${showBubble ? 'visible' : ''}`}>
120
+ <div>
121
+ <FaBell/>
122
+ {unreadCount > 0 && <span className="notification-bubble-count">{unreadCount}</span>}
123
+ </div>
124
+ {showBubble && <div className='notifications-bubble-content'>
125
+ {readNotifications.map(notification => (
126
+ <Notification
127
+ key={notification.id}
128
+ notification={notification}
129
+ />
130
+ ))}
131
+ </div>}
132
+ </button>}
133
+ </>
134
+ );
135
+ });
136
+ NotificationList.displayName = "NotificationList";
137
137
  export {NotificationList, generateNotificationId}; // Export for use elsewherex
@@ -74,24 +74,6 @@
74
74
  }
75
75
  }
76
76
 
77
- .notification-bubble {
78
- position: fixed;
79
- bottom: 20px;
80
- right: 20px;
81
- background-color: #007bff;
82
- color: #fff;
83
- border-radius: 50%;
84
- width: 50px;
85
- height: 50px;
86
- justify-content: center;
87
- align-items: center;
88
- cursor: pointer;
89
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
90
- z-index: 1000;
91
- transition: all 0.3s ease;
92
- pointer-events: all;
93
- display: none;
94
- }
95
77
 
96
78
  .notification-bubble.visible {
97
79
  display: flex;
@@ -14,9 +14,11 @@ export const Pagination = ({
14
14
  page,
15
15
  setPage,
16
16
  useParam = false,
17
- showElementsPerPage=false
17
+ showElementsPerPage=false,
18
+ elementsPerPage: propElementsPerPage
18
19
  }) => {
19
- const { elementsPerPage, setElementsPerPage } = useModelContext();
20
+ const { elementsPerPage: contextElementsPerPage, setElementsPerPage } = useModelContext();
21
+ const elementsPerPage = propElementsPerPage || contextElementsPerPage;
20
22
  let pageCount = Math.ceil(totalCount / elementsPerPage);
21
23
  const [searchParams, setSearchParams] = useSearchParams();
22
24
 
@@ -109,7 +111,7 @@ export const Pagination = ({
109
111
  (_, i) => page + i - parseInt(visibleItemsCount / 2, 10),
110
112
  ).map((p, i) => {
111
113
  return p >= 1 && p <= pageCount ? (
112
- <Button disabled={p === page} onClick={() => handleChange(p)}>
114
+ <Button key={"paginate-"+i} disabled={p === page} onClick={() => handleChange(p)}>
113
115
  {p}
114
116
  </Button>
115
117
  ) : (