diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e3b51f..bb1bb50 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tabler/core": "^1.3.2", "@tabler/icons-react": "^3.34.0", + "@tanstack/react-query": "^5.56.2", "@xyflow/react": "^12.3.4", "axios": "^1.10.0", "react": "^19.1.0", @@ -632,6 +633,32 @@ "react": ">= 16" } }, + "node_modules/@tanstack/query-core": { + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", + "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.84.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.2.tgz", + "integrity": "sha512-cZadySzROlD2+o8zIfbD978p0IphuQzRWiiH3I2ugnTmz4jbjc0+TdibpwqxlzynEen8OulgAg+rzdNF37s7XQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index cd99cba..02a93d1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@tabler/core": "^1.3.2", "@tabler/icons-react": "^3.34.0", "axios": "^1.10.0", + "@tanstack/react-query": "^5.56.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^6.30.1", diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx new file mode 100644 index 0000000..e4ad993 --- /dev/null +++ b/frontend/src/components/ConfirmDialog.jsx @@ -0,0 +1,32 @@ +import { useEffect, useRef } from 'react' + +export default function ConfirmDialog({ open, title, message, confirmText = 'Подтвердить', cancelText = 'Отмена', onConfirm, onCancel }) { + const ref = useRef(null) + useEffect(() => { + if (open && ref.current) { + try { ref.current.querySelector('button[data-primary]')?.focus() } catch {} + } + }, [open]) + if (!open) return null + return ( +
+
+
+
+
{title || 'Подтверждение'}
+ +
+
+

{message}

+
+
+ + +
+
+
+
+ ) +} + + diff --git a/frontend/src/components/HistoryModal.jsx b/frontend/src/components/HistoryModal.jsx index fb4dd88..861cf13 100644 --- a/frontend/src/components/HistoryModal.jsx +++ b/frontend/src/components/HistoryModal.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import api from '../lib/api.js'; +import { useQuery } from '@tanstack/react-query'; import { useNotify } from './NotifyProvider.jsx'; import { IconHistory, IconRefresh, IconDeviceFloppy } from '@tabler/icons-react'; @@ -8,20 +9,18 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack }) const [items, setItems] = useState([]); const notify = useNotify(); - const fetchHistory = async () => { - if (!resource) return; - setLoading(true); - try { - const res = await api.get(`/history/${resource}`); - setItems(Array.isArray(res.data?.items) ? res.data.items : []); - } catch (e) { - notify.error('Не удалось загрузить историю версий'); - } finally { - setLoading(false); - } - }; + const { refetch } = useQuery({ + queryKey: ['history', resource], + enabled: false, + queryFn: async () => { + const res = await api.get(`/history/${resource}`) + const data = Array.isArray(res.data?.items) ? res.data.items : [] + setItems(data) + return data + }, + }) - useEffect(() => { if (show) fetchHistory(); }, [show, resource]); + useEffect(() => { if (show) refetch().catch(() => notify.error('Не удалось загрузить историю версий')); }, [show, resource]); const rollback = async (versionId) => { if (!versionId) return; @@ -54,7 +53,7 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack })
Ресурс: {resource}
- diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 66d7d7e..541c7da 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -28,6 +28,40 @@ api.interceptors.response.use( } ); +// Нормализация ошибок и уведомления по умолчанию +api.interceptors.response.use( + (res) => res, + (err) => { + try { + const status = err?.response?.status; + const message = err?.response?.data?.message || err?.message || 'Ошибка запроса'; + if (status >= 400 && typeof window !== 'undefined' && window.notify?.error) { + window.notify.error(`${message} (${status ?? '—'})`); + } + } catch {} + return Promise.reject(err); + } +); + +// Request интерсептор: If-Match из etag (если не задан явный заголовок) +api.interceptors.request.use((config) => { + try { + // Если заголовок не указан, но в теле есть etag — пробуем проставить If-Match + if (!config.headers?.['If-Match'] && config.data && typeof config.data === 'object' && config.data.etag) { + config.headers = config.headers || {}; + config.headers['If-Match'] = String(config.data.etag); + } + } catch {} + return config; +}); + +// Утилита: опциональная распаковка стандартного формата +export function unwrapStd(res) { + const data = res?.data; + if (data && typeof data === 'object' && Array.isArray(data.items)) return data.items; + return data; +} + export default api; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 2482e02..7ea9b84 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,14 +3,19 @@ import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' import '@tabler/core/dist/css/tabler.min.css' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // Динамический импорт Tabler JS и присваивание в window import('@tabler/core/dist/js/tabler.min.js').then((mod) => { window.Tabler = window.Tabler || window.globalThis.Tabler || mod; }); +const queryClient = new QueryClient() + ReactDOM.createRoot(document.getElementById('root')).render( - + + + )