feat: Enhance error handling in server responses by including request IDs and detailed error codes; improve frontend modal components for better accessibility and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m59s

This commit is contained in:
2025-08-11 21:32:32 +07:00
parent f44acbff98
commit 3b1a59b03f
6 changed files with 137 additions and 77 deletions
+43 -15
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import api from '../lib/api.js';
import { useQuery } from '@tanstack/react-query';
import ConfirmDialog from './ConfirmDialog.jsx';
import { useNotify } from './NotifyProvider.jsx';
import { IconHistory, IconRefresh, IconDeviceFloppy } from '@tabler/icons-react';
@@ -22,30 +23,49 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack })
useEffect(() => { if (show) refetch().catch(() => notify.error('Не удалось загрузить историю версий')); }, [show, resource]);
const [confirmState, setConfirmState] = useState({ open: false, onConfirm: null });
const rollback = async (versionId) => {
if (!versionId) return;
if (!confirm('Откатить к выбранной версии? Текущее содержимое будет перезаписано.')) return;
setLoading(true);
try {
const res = await api.post(`/history/${resource}/rollback`, { versionId });
notify.success('Откат выполнен');
onRolledBack && onRolledBack(res.data || {});
onClose && onClose();
} catch (e) {
notify.error('Не удалось выполнить откат');
} finally {
setLoading(false);
}
await new Promise((resolve) => {
setConfirmState({
open: true,
onConfirm: async () => {
setConfirmState({ open: false, onConfirm: null });
setLoading(true);
try {
const res = await api.post(`/history/${resource}/rollback`, { versionId });
notify.success('Откат выполнен');
onRolledBack && onRolledBack(res.data || {});
onClose && onClose();
} catch (e) {
notify.error('Не удалось выполнить откат');
} finally {
setLoading(false);
}
resolve();
}
});
});
};
if (!show) return null;
return (
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal show d-block" role="dialog" aria-modal="true" aria-labelledby="history-title" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }} onKeyDown={(e) => { if (e.key === 'Escape') onClose?.() }}>
<div className="modal-dialog modal-lg modal-dialog-centered" role="document">
<div className="modal-content">
<div className="modal-content" tabIndex={-1} onKeyDown={(e) => {
if (e.key === 'Tab') {
const c = e.currentTarget
const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
if (!focusable || focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}}>
<div className="modal-header">
<h5 className="modal-title d-flex align-items-center">
<h5 id="history-title" className="modal-title d-flex align-items-center">
<IconHistory className="me-2" /> История версий
</h5>
<button type="button" className="btn-close" onClick={onClose}></button>
@@ -96,6 +116,14 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack })
</div>
</div>
</div>
<ConfirmDialog
open={confirmState.open}
title="Подтверждение"
message="Откатить к выбранной версии? Текущее содержимое будет перезаписано."
confirmText="Откатить"
onCancel={() => setConfirmState({ open: false, onConfirm: null })}
onConfirm={confirmState.onConfirm}
/>
</div>
);
}