feat: Integrate @tanstack/react-query for improved data fetching and state management in HistoryModal; update API error handling and enhance request interceptors for better error notifications
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m12s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m12s
This commit is contained in:
Generated
+27
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tabler/core": "^1.3.2",
|
"@tabler/core": "^1.3.2",
|
||||||
"@tabler/icons-react": "^3.34.0",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@xyflow/react": "^12.3.4",
|
"@xyflow/react": "^12.3.4",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
@@ -632,6 +633,32 @@
|
|||||||
"react": ">= 16"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@tabler/core": "^1.3.2",
|
"@tabler/core": "^1.3.2",
|
||||||
"@tabler/icons-react": "^3.34.0",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router-dom": "^6.30.1",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="modal show d-block" role="dialog" aria-modal="true" aria-labelledby="confirm-title" aria-describedby="confirm-message" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||||
|
<div className="modal-dialog" role="document">
|
||||||
|
<div className="modal-content" ref={ref}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 id="confirm-title" className="modal-title">{title || 'Подтверждение'}</h5>
|
||||||
|
<button type="button" className="btn-close" aria-label="Close" onClick={onCancel}></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<p id="confirm-message" className="m-0">{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onCancel}>{cancelText}</button>
|
||||||
|
<button type="button" className="btn btn-primary" data-primary onClick={onConfirm}>{confirmText}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import api from '../lib/api.js';
|
import api from '../lib/api.js';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNotify } from './NotifyProvider.jsx';
|
import { useNotify } from './NotifyProvider.jsx';
|
||||||
import { IconHistory, IconRefresh, IconDeviceFloppy } from '@tabler/icons-react';
|
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 [items, setItems] = useState([]);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
|
||||||
const fetchHistory = async () => {
|
const { refetch } = useQuery({
|
||||||
if (!resource) return;
|
queryKey: ['history', resource],
|
||||||
setLoading(true);
|
enabled: false,
|
||||||
try {
|
queryFn: async () => {
|
||||||
const res = await api.get(`/history/${resource}`);
|
const res = await api.get(`/history/${resource}`)
|
||||||
setItems(Array.isArray(res.data?.items) ? res.data.items : []);
|
const data = Array.isArray(res.data?.items) ? res.data.items : []
|
||||||
} catch (e) {
|
setItems(data)
|
||||||
notify.error('Не удалось загрузить историю версий');
|
return data
|
||||||
} finally {
|
},
|
||||||
setLoading(false);
|
})
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => { if (show) fetchHistory(); }, [show, resource]);
|
useEffect(() => { if (show) refetch().catch(() => notify.error('Не удалось загрузить историю версий')); }, [show, resource]);
|
||||||
|
|
||||||
const rollback = async (versionId) => {
|
const rollback = async (versionId) => {
|
||||||
if (!versionId) return;
|
if (!versionId) return;
|
||||||
@@ -54,7 +53,7 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack })
|
|||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
<div className="text-muted small">Ресурс: <code>{resource}</code></div>
|
<div className="text-muted small">Ресурс: <code>{resource}</code></div>
|
||||||
<button className="btn btn-outline-secondary btn-sm" onClick={fetchHistory} disabled={loading}>
|
<button className="btn btn-outline-secondary btn-sm" onClick={() => refetch()} disabled={loading}>
|
||||||
<IconRefresh className={loading ? 'spin' : ''} />
|
<IconRefresh className={loading ? 'spin' : ''} />
|
||||||
<span className="ms-1">Обновить</span>
|
<span className="ms-1">Обновить</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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;
|
export default api;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import '@tabler/core/dist/css/tabler.min.css'
|
import '@tabler/core/dist/css/tabler.min.css'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
|
||||||
// Динамический импорт Tabler JS и присваивание в window
|
// Динамический импорт Tabler JS и присваивание в window
|
||||||
import('@tabler/core/dist/js/tabler.min.js').then((mod) => {
|
import('@tabler/core/dist/js/tabler.min.js').then((mod) => {
|
||||||
window.Tabler = window.Tabler || window.globalThis.Tabler || mod;
|
window.Tabler = window.Tabler || window.globalThis.Tabler || mod;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user