Compare commits

...

4 Commits

Author SHA1 Message Date
Vladislav Drozdov 5e9e40aad2 Merge pull request 'redisign' (#46) from redisign into rc
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/46
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
2025-06-11 15:01:46 +03:00
DmitriyA 405bda3df9 added ranages editor
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-06-11 07:30:34 -04:00
DmitriyA 328018edfa created modal window for settings 2025-06-11 04:49:48 -04:00
DmitriyA 69a5e4ade1 sidebar menu improvement 2025-06-10 09:29:14 -04:00
6 changed files with 636 additions and 146 deletions

View File

@ -0,0 +1,258 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import axios from 'axios';
const MetricRangeEditor = ({ onSave }) => {
const [ranges, setRanges] = useState([]);
const [filter, setFilter] = useState('');
const [newMetricName, setNewMetricName] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [hasChanges, setHasChanges] = useState(false);
const [success, setSuccess] = useState(false);
// Загрузка данных
const loadRanges = useCallback(async () => {
try {
setLoading(true);
const res = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/ranges/list`);
setRanges(
Object.entries(res.data).map(([name, r]) => ({
name,
ranges: Array.isArray(r) ? r : []
}))
);
setError(null);
} catch (err) {
console.error('Ошибка при получении ranges:', err);
setError('Не удалось загрузить данные');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadRanges();
}, [loadRanges]);
// Обновление диапазона
const updateRange = (metricIndex, rangeIndex, field, value) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges[rangeIndex][field] = Number(value);
setRanges(newRanges);
setHasChanges(true);
};
// Добавление диапазона
const addRange = (metricIndex) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 });
setRanges(newRanges);
setHasChanges(true);
};
// Удаление диапазона
const deleteRange = (metricIndex, rangeIndex) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges.splice(rangeIndex, 1);
setRanges(newRanges);
setHasChanges(true);
};
const saveChanges = async () => {
try {
setLoading(true);
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges);
setHasChanges(false);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
if (onSave) {
onSave({
hasChanges: false,
saveChanges: saveChanges
});
}
return true;
} catch (err) {
console.error('Ошибка при сохранении:', err);
setError('Ошибка при сохранении');
return false;
} finally {
setLoading(false);
}
};
// Добавление новой метрики
const addNewMetric = () => {
if (!newMetricName.trim()) {
setError('Введите название метрики');
return;
}
if (ranges.some(r => r.name === newMetricName)) {
setError('Метрика с таким именем уже существует');
return;
}
setRanges([...ranges, {
name: newMetricName,
ranges: [{ min: 0, max: 100, status: 1 }]
}]);
setNewMetricName('');
setHasChanges(true);
setError(null);
};
const filtered = filter
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
: ranges;
return (
<Box sx={{ position: 'relative' }}>
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
)}
<Collapse in={!!error}>
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
</Collapse>
<Collapse in={success}>
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess(false)}>
Изменения успешно сохранены!
</Alert>
</Collapse>
{!loading && (
<>
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
<TextField
label="Поиск по метрике"
fullWidth
value={filter}
onChange={(e) => setFilter(e.target.value)}
variant="standard"
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
<TextField
label="Новая метрика"
value={newMetricName}
onChange={(e) => setNewMetricName(e.target.value)}
fullWidth
variant="standard"
/>
<Tooltip title="Добавить метрику">
<IconButton
onClick={addNewMetric}
color="primary"
disabled={!newMetricName.trim()}
>
<AddIcon />
</IconButton>
</Tooltip>
</Box>
<Divider sx={{ mb: 3 }} />
{filtered.map((metric, i) => (
<Box
key={metric.name}
sx={{
mb: 3,
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 2,
backgroundColor: 'background.paper'
}}
>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
{metric.name}
</Typography>
{metric.ranges.map((r, j) => (
<Box
key={j}
sx={{
display: 'flex',
gap: 2,
alignItems: 'center',
mt: 1,
'& > *': { flex: 1 }
}}
>
<TextField
label="Минимум"
type="number"
value={r.min}
onChange={(e) => updateRange(i, j, 'min', e.target.value)}
size="small"
/>
<TextField
label="Максимум"
type="number"
value={r.max}
onChange={(e) => updateRange(i, j, 'max', e.target.value)}
size="small"
/>
<TextField
label="Статус"
type="number"
value={r.status}
onChange={(e) => updateRange(i, j, 'status', e.target.value)}
size="small"
/>
<Tooltip title="Удалить диапазон">
<IconButton
onClick={() => deleteRange(i, j)}
size="small"
sx={{ flex: 'none' }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
))}
<Button
onClick={() => addRange(i)}
startIcon={<AddIcon />}
size="small"
sx={{ mt: 1 }}
>
Добавить диапазон
</Button>
</Box>
))}
{filtered.length === 0 && (
<Typography color="text.secondary" textAlign="center" py={3}>
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}
</Typography>
)}
</>
)}
{/* Передаем состояние изменений в родительский компонент */}
{useEffect(() => {
if (onSave) {
onSave({ hasChanges, saveChanges });
}
}, [hasChanges, onSave])}
</Box>
);
};
export default MetricRangeEditor;

View File

@ -0,0 +1,215 @@
// components/SettingsModal.jsx
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Tabs,
Tab,
Box,
Typography,
IconButton,
styled,
CircularProgress,
Slide,
Snackbar,
Alert
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
const StyledDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': {
minWidth: 600,
maxHeight: '80vh',
backgroundColor: theme.palette.background.paper,
},
}));
const TabPanel = (props) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`settings-tabpanel-${index}`}
aria-labelledby={`settings-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography component="div">{children}</Typography>
</Box>
)}
</div>
);
};
const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
const [tabValue, setTabValue] = useState(0);
const [isSaving, setIsSaving] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [metricEditorState, setMetricEditorState] = useState({
hasChanges: false,
save: () => { }
});
const handleTabChange = (event, newValue) => {
if (hasChanges) {
setShowConfirmClose(true);
} else {
setTabValue(newValue);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
let success = true;
if (tabValue === 1 && metricEditorState.hasChanges) {
success = await metricEditorState.save();
}
if (success) {
setShowSuccess(true);
setHasChanges(false);
if (onMenuUpdate) {
onMenuUpdate();
}
}
} finally {
setIsSaving(false);
}
};
const handleMetricEditorChange = ({ hasChanges, saveChanges }) => {
setMetricEditorState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleClose = () => {
if (hasChanges) {
setShowConfirmClose(true);
} else {
onClose();
}
};
const handleConfirmClose = (shouldClose) => {
setShowConfirmClose(false);
if (shouldClose) {
onClose();
}
};
// Пример обработчика изменений
const handleSettingChange = () => {
setHasChanges(true);
};
return (
<>
<StyledDialog
open={open}
onClose={handleClose}
aria-labelledby="settings-dialog-title"
maxWidth="md"
fullWidth
TransitionComponent={Transition}
>
<DialogTitle id="settings-dialog-title">
Настройки
<IconButton
aria-label="close"
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: (theme) => theme.palette.grey[500],
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
{/* Добавляйте новые вкладки здесь */}
</Tabs>
</Box>
<DialogContent dividers>
<TabPanel value={tabValue} index={0}>
<Typography variant="h6">Настройки меню</Typography>
{/* Добавьте содержимое для вкладки меню */}
</TabPanel>
<TabPanel value={tabValue} index={1}>
<MetricRangeEditor onSave={handleMetricEditorChange} />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Закрыть</Button>
<Button
onClick={handleSave}
variant="contained"
color="primary"
disabled={isSaving || !hasChanges}
startIcon={isSaving ? <CircularProgress size={20} /> : <SaveIcon />}
>
{isSaving ? 'Сохранение...' : 'Сохранить'}
</Button>
</DialogActions>
</StyledDialog>
{/* Уведомление об успешном сохранении */}
<Snackbar
open={showSuccess}
autoHideDuration={3000}
onClose={() => setShowSuccess(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<Alert onClose={() => setShowSuccess(false)} severity="success" sx={{ width: '100%' }}>
Настройки успешно сохранены!
</Alert>
</Snackbar>
{/* Диалог подтверждения закрытия */}
<Dialog
open={showConfirmClose}
onClose={() => handleConfirmClose(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Есть несохраненные изменения</DialogTitle>
<DialogContent>
<Typography>Вы уверены, что хотите закрыть без сохранения изменений?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => handleConfirmClose(false)}>Отмена</Button>
<Button onClick={() => handleConfirmClose(true)} autoFocus color="error">
Закрыть без сохранения
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default SettingsModal;

View File

@ -3,17 +3,10 @@ import React, { useState, useEffect } from "react";
import { import {
Drawer, Drawer,
List, List,
Typography,
styled, styled,
IconButton, IconButton,
Tooltip, Tooltip,
Box, Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField
} from "@mui/material"; } from "@mui/material";
import MenuItem from "./SidebarMenuComponents/MenuItem"; import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter"; import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
@ -27,12 +20,8 @@ const SidebarMenu = ({
data, data,
isDarkMode, isDarkMode,
setIsDarkMode, setIsDarkMode,
onEditItem,
onSelectItem, onSelectItem,
editModalOpen, forceRefreshMenu
editingItem,
onCloseEditModal,
onSaveChanges
}) => { }) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const { sidebarWidth, startResizing } = useSidebarResize(290); const { sidebarWidth, startResizing } = useSidebarResize(290);
@ -148,7 +137,7 @@ const SidebarMenu = ({
item={data} item={data}
collapsed={collapsed} collapsed={collapsed}
level={0} level={0}
onEdit={onEditItem}
onSelectItem={onSelectItem} onSelectItem={onSelectItem}
/> />
)} )}
@ -158,76 +147,15 @@ const SidebarMenu = ({
collapsed={collapsed} collapsed={collapsed}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode} setIsDarkMode={setIsDarkMode}
forceRefreshMenu={forceRefreshMenu}
/> />
</Box> </Box>
{!collapsed && ( {!collapsed && (
<SidebarResizer onMouseDown={startResizing} /> <SidebarResizer onMouseDown={startResizing} />
)} )}
</Drawer> </Drawer>
{/* Модальное окно редактирования */}
<EditMenuItemDialog
open={editModalOpen}
item={editingItem}
onClose={onCloseEditModal}
onSave={onSaveChanges}
/>
</Box> </Box>
); );
}; };
const EditMenuItemDialog = ({ open, item, onClose, onSave }) => {
const [formData, setFormData] = useState(item || {});
useEffect(() => {
setFormData(item || {});
}, [item]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = () => {
onSave(formData);
};
if (!item) return null;
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Редактирование элемента меню</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
<TextField
fullWidth
label="Название"
name="title"
value={formData.title || ''}
onChange={handleChange}
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="ID"
name="id"
value={formData.id || ''}
onChange={handleChange}
disabled
sx={{ mb: 2 }}
/>
{/* Дополнительные поля для редактирования */}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Отмена</Button>
<Button onClick={handleSubmit} variant="contained" color="primary">
Сохранить
</Button>
</DialogActions>
</Dialog>
);
};
export default SidebarMenu; export default SidebarMenu;

View File

@ -7,11 +7,10 @@ import {
Collapse, Collapse,
List, List,
styled, styled,
IconButton,
Menu, Menu,
MenuItem as MuiMenuItem MenuItem as MuiMenuItem
} from "@mui/material"; } from "@mui/material";
import { ExpandLess, ExpandMore, Folder, FolderOpen, Edit } from "@mui/icons-material"; import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
import StatusIndicator from "./StatusIndicator"; import StatusIndicator from "./StatusIndicator";
const StyledListItem = styled(ListItem)(({ theme, level }) => ({ const StyledListItem = styled(ListItem)(({ theme, level }) => ({
@ -44,11 +43,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
setContextMenu(null); setContextMenu(null);
}; };
const handleEditClick = () => {
onEdit(item);
handleCloseContextMenu();
};
const handleToggle = (e) => { const handleToggle = (e) => {
e.stopPropagation(); e.stopPropagation();
setIsOpen(!isOpen); setIsOpen(!isOpen);
@ -87,19 +81,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
}} }}
/> />
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)} {hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
{level > 0 && (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleEditClick();
}}
sx={{ ml: 1 }}
>
<Edit fontSize="small" />
</IconButton>
)}
</> </>
)} )}
</StyledListItem> </StyledListItem>
@ -114,9 +95,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
: undefined : undefined
} }
> >
<MuiMenuItem onClick={handleEditClick}>
<Edit fontSize="small" sx={{ mr: 1 }} /> Редактировать
</MuiMenuItem>
</Menu> </Menu>
{hasChildren && !collapsed && ( {hasChildren && !collapsed && (

View File

@ -1,4 +1,5 @@
import React from "react"; // components/SidebarMenuComponents/SidebarFooter.jsx
import React, { useState } from "react";
import { Brightness4, Brightness7 } from "@mui/icons-material"; import { Brightness4, Brightness7 } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
import { import {
@ -7,8 +8,10 @@ import {
ListItemText, ListItemText,
styled, styled,
Switch, Switch,
Box Box,
Button
} from "@mui/material"; } from "@mui/material";
import SettingsModal from "../SettingsModal";
const FooterList = styled(List)(({ theme }) => ({ const FooterList = styled(List)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebar, backgroundColor: theme.palette.custom.sidebar,
@ -27,8 +30,19 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
alignItems: 'center' alignItems: 'center'
})); }));
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => { const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => {
const [settingsOpen, setSettingsOpen] = useState(false);
const handleSettingsOpen = () => {
setSettingsOpen(true);
};
const handleSettingsClose = () => {
setSettingsOpen(false);
};
return ( return (
<>
<FooterList> <FooterList>
{!collapsed && ( {!collapsed && (
<FooterListItem button> <FooterListItem button>
@ -43,13 +57,24 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
)} )}
<FooterListItem> <FooterListItem>
{!collapsed && ( {!collapsed && (
<Button
onClick={handleSettingsOpen}
sx={{
color: 'custom.sidebarText',
textTransform: 'none',
minWidth: 0,
padding: 0,
marginRight: 'auto'
}}
>
<ListItemText <ListItemText
primary="Настройка" primary="Настройки"
primaryTypographyProps={{ primaryTypographyProps={{
color: 'custom.sidebarText', color: 'custom.sidebarText',
variant: 'body2' variant: 'body2'
}} }}
/> />
</Button>
)} )}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@ -72,6 +97,11 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
</Box> </Box>
</FooterListItem> </FooterListItem>
</FooterList> </FooterList>
<SettingsModal open={settingsOpen}
onClose={handleSettingsClose}
onMenuUpdate={forceRefreshMenu} />
</>
); );
}; };

View File

@ -5,16 +5,61 @@ import axios from 'axios';
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
const [menuData, setMenuData] = useState(null); const [menuData, setMenuData] = useState(null);
const [lastModified, setLastModified] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [editingItem, setEditingItem] = useState(null); const [editingItem, setEditingItem] = useState(null);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [backgroundLoading, setBackgroundLoading] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const forceRefreshMenu = () => {
setRefreshTrigger(prev => prev + 1);
localStorage.removeItem('menuCache'); // Очищаем кэш
};
// Загружаем меню из localStorage при инициализации
useEffect(() => {
const loadCachedMenu = () => {
try {
const cached = localStorage.getItem('menuCache');
if (cached) {
const { data, timestamp } = JSON.parse(cached);
setMenuData(data);
setLastModified(timestamp);
}
} catch (e) {
console.warn('Failed to load menu from cache', e);
}
};
loadCachedMenu();
}, []);
// Основная загрузка меню
useEffect(() => { useEffect(() => {
const fetchMenuData = async () => { const fetchMenuData = async () => {
try { try {
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`); setLoading(true);
setMenuData(response.data); // axios хранит данные в response.data const headers = lastModified ? { 'If-Modified-Since': lastModified } : {};
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`, {
headers,
validateStatus: status => status === 200 || status === 304
});
if (response.status === 200) {
const newLastModified = response.headers['last-modified'];
setMenuData(response.data);
setLastModified(newLastModified);
// Сохраняем в кэш
localStorage.setItem('menuCache', JSON.stringify({
data: response.data,
timestamp: newLastModified
}));
}
} catch (err) { } catch (err) {
console.error('Error fetching menu data:', err); console.error('Error fetching menu data:', err);
setError(err.message || 'Failed to fetch menu data'); setError(err.message || 'Failed to fetch menu data');
@ -24,7 +69,41 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
}; };
fetchMenuData(); fetchMenuData();
}, []); }, [refreshTrigger]);
// Фоновая проверка обновлений
useEffect(() => {
if (!lastModified) return;
const checkForUpdates = async () => {
try {
setBackgroundLoading(true);
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/check-updates`, {
headers: { 'If-Modified-Since': lastModified }
});
if (response.data.hasUpdates) {
// Если есть обновления, загружаем их в фоне
const updateResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`);
setMenuData(updateResponse.data);
setLastModified(updateResponse.headers['last-modified']);
localStorage.setItem('menuCache', JSON.stringify({
data: updateResponse.data,
timestamp: updateResponse.headers['last-modified']
}));
}
} catch (err) {
console.warn('Background update check failed', err);
} finally {
setBackgroundLoading(false);
}
};
// Проверяем обновления каждые 5 минут
const interval = setInterval(checkForUpdates, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [lastModified]);
const handleSaveChanges = async (updatedItem) => { const handleSaveChanges = async (updatedItem) => {
try { try {
@ -97,6 +176,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
editingItem={editingItem} editingItem={editingItem}
onCloseEditModal={() => setEditModalOpen(false)} onCloseEditModal={() => setEditModalOpen(false)}
onSaveChanges={handleSaveChanges} onSaveChanges={handleSaveChanges}
forceRefreshMenu={forceRefreshMenu}
/> />
); );
}; };