Версия 1.0.6 Исправлена ошибка, когда при выходе из редактирования статьи не сохранялись состояния фильтров и пагинации.
This commit is contained in:
parent
758e9fe821
commit
103705c59a
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { Pencil, Trash2, ImagePlus, ToggleLeft, ToggleRight, Plus } from 'lucide
|
|||||||
import MinutesWord from './Words/MinutesWord';
|
import MinutesWord from './Words/MinutesWord';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import {Pagination} from "./Pagination.tsx";
|
import { Pagination } from "./Pagination";
|
||||||
import {useSearchParams} from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
interface ArticleListProps {
|
interface ArticleListProps {
|
||||||
articles: Article[];
|
articles: Article[];
|
||||||
@ -32,15 +32,16 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
}: ArticleListProps) {
|
}: ArticleListProps) {
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const { availableCategoryIds, availableCityIds, isAdmin } = usePermissions();
|
const { availableCategoryIds, availableCityIds, isAdmin } = usePermissions();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Получаем значения фильтров из URL
|
||||||
|
const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
|
||||||
|
const filterCategoryId = parseInt(searchParams.get('categoryId') || '0', 10);
|
||||||
|
const filterCityId = parseInt(searchParams.get('cityId') || '0', 10);
|
||||||
|
const showDraftOnly = searchParams.get('draftOnly') === 'true';
|
||||||
|
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [totalArticles, setTotalArticles] = useState(0);
|
const [totalArticles, setTotalArticles] = useState(0);
|
||||||
const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
|
|
||||||
const [filterCategoryId, setFilterCategoryId] = useState(0);
|
|
||||||
const [filterCityId, setFilterCityId] = useState(0);
|
|
||||||
const [showDraftOnly, setShowDraftOnly] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@ -49,7 +50,14 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/articles/', {
|
const response = await axios.get('/api/articles/', {
|
||||||
params: { page: currentPage, categoryId: filterCategoryId, cityId: filterCityId, isDraft: showDraftOnly, userId: user?.id, isAdmin },
|
params: {
|
||||||
|
page: currentPage,
|
||||||
|
categoryId: filterCategoryId,
|
||||||
|
cityId: filterCityId,
|
||||||
|
isDraft: showDraftOnly,
|
||||||
|
userId: user?.id,
|
||||||
|
isAdmin
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setArticles(response.data.articles);
|
setArticles(response.data.articles);
|
||||||
setTotalPages(response.data.totalPages);
|
setTotalPages(response.data.totalPages);
|
||||||
@ -74,9 +82,17 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
|
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
|
||||||
);
|
);
|
||||||
setArticles(prev => prev.map(a => (a.id === id ? { ...a, isActive: !a.isActive } : a)));
|
setArticles(prev => prev.map(a => (a.id === id ? { ...a, isActive: !a.isActive } : a)));
|
||||||
|
|
||||||
// Обновляем список с сервера после изменения статуса
|
// Обновляем список с сервера после изменения статуса
|
||||||
const response = await axios.get('/api/articles/', {
|
const response = await axios.get('/api/articles/', {
|
||||||
params: { page: currentPage, categoryId: filterCategoryId, cityId: filterCityId, isDraft: showDraftOnly, userId: user?.id, isAdmin },
|
params: {
|
||||||
|
page: currentPage,
|
||||||
|
categoryId: filterCategoryId,
|
||||||
|
cityId: filterCityId,
|
||||||
|
isDraft: showDraftOnly,
|
||||||
|
userId: user?.id,
|
||||||
|
isAdmin
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setArticles(response.data.articles);
|
setArticles(response.data.articles);
|
||||||
setTotalPages(response.data.totalPages);
|
setTotalPages(response.data.totalPages);
|
||||||
@ -88,11 +104,46 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
searchParams.set('page', page.toString());
|
const newParams = new URLSearchParams(searchParams);
|
||||||
setSearchParams(searchParams);
|
newParams.set('page', page.toString());
|
||||||
|
setSearchParams(newParams);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчики изменения фильтров
|
||||||
|
const handleCategoryChange = (categoryId: number) => {
|
||||||
|
const newParams = new URLSearchParams(searchParams);
|
||||||
|
if (categoryId === 0) {
|
||||||
|
newParams.delete('categoryId');
|
||||||
|
} else {
|
||||||
|
newParams.set('categoryId', categoryId.toString());
|
||||||
|
}
|
||||||
|
newParams.set('page', '1');
|
||||||
|
setSearchParams(newParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCityChange = (cityId: number) => {
|
||||||
|
const newParams = new URLSearchParams(searchParams);
|
||||||
|
if (cityId === 0) {
|
||||||
|
newParams.delete('cityId');
|
||||||
|
} else {
|
||||||
|
newParams.set('cityId', cityId.toString());
|
||||||
|
}
|
||||||
|
newParams.set('page', '1');
|
||||||
|
setSearchParams(newParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDraftOnlyChange = (checked: boolean) => {
|
||||||
|
const newParams = new URLSearchParams(searchParams);
|
||||||
|
if (checked) {
|
||||||
|
newParams.set('draftOnly', 'true');
|
||||||
|
} else {
|
||||||
|
newParams.delete('draftOnly');
|
||||||
|
}
|
||||||
|
newParams.set('page', '1');
|
||||||
|
setSearchParams(newParams);
|
||||||
|
};
|
||||||
|
|
||||||
const hasNoPermissions = availableCategoryIds.length === 0 || availableCityIds.length === 0;
|
const hasNoPermissions = availableCategoryIds.length === 0 || availableCityIds.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -103,67 +154,58 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<select
|
<select
|
||||||
value={filterCategoryId}
|
value={filterCategoryId}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleCategoryChange(Number(e.target.value))}
|
||||||
setFilterCategoryId(Number(e.target.value));
|
|
||||||
searchParams.set('page', '1');
|
|
||||||
setSearchParams(searchParams);
|
|
||||||
}}
|
|
||||||
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Все категории</option>
|
<option value="0">Все категории</option>
|
||||||
{availableCategoryIds.map(cat => (
|
{availableCategoryIds.map(cat => (
|
||||||
<option key={cat} value={cat}>
|
<option key={cat} value={cat}>
|
||||||
{CategoryTitles[cat]}
|
{CategoryTitles[cat]}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={filterCityId}
|
value={filterCityId}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleCityChange(Number(e.target.value))}
|
||||||
setFilterCityId(Number(e.target.value));
|
|
||||||
searchParams.set('page', '1');
|
|
||||||
setSearchParams(searchParams);
|
|
||||||
}}
|
|
||||||
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Все столицы</option>
|
<option value="0">Все столицы</option>
|
||||||
{availableCityIds.map(c => (
|
{availableCityIds.map(c => (
|
||||||
<option key={c} value={c}>
|
<option key={c} value={c}>
|
||||||
{CityTitles[c]}
|
{CityTitles[c]}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label className="inline-flex items-center">
|
<label className="inline-flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showDraftOnly}
|
checked={showDraftOnly}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleDraftOnlyChange(e.target.checked)}
|
||||||
setShowDraftOnly(e.target.checked)
|
|
||||||
searchParams.set('page', '1');
|
|
||||||
setSearchParams(searchParams);
|
|
||||||
}}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-sm text-gray-700">Только черновики</span>
|
<span className="ml-2 text-sm text-gray-700">Только черновики</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{!hasNoPermissions && (
|
{!hasNoPermissions && (
|
||||||
<button
|
<button
|
||||||
onClick={onNewArticle}
|
onClick={onNewArticle}
|
||||||
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
<Plus size={16} className="mr-2" /> Новая статья
|
<Plus size={16} className="mr-2" />
|
||||||
|
Новая статья
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<p className="font-bold text-gray-600">
|
<p className="font-bold text-gray-600">
|
||||||
Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)}
|
Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)} - {Math.min(currentPage * ARTICLES_PER_PAGE, totalArticles)} из {totalArticles}
|
||||||
-
|
|
||||||
{Math.min(currentPage * ARTICLES_PER_PAGE, totalArticles)} из {totalArticles}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center p-6">
|
<div className="flex justify-center p-6">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
@ -190,10 +232,8 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
.filter(a => a.role === 'WRITER')
|
.filter(a => a.role === 'WRITER')
|
||||||
.sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0));
|
.sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0));
|
||||||
if (!writerAuthors) return null;
|
if (!writerAuthors) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center text-xs text-gray-500 mt-1">
|
<div className="flex items-center text-xs text-gray-500 mt-1">
|
||||||
|
|
||||||
{writerAuthors.map((authorLink) => (
|
{writerAuthors.map((authorLink) => (
|
||||||
<img
|
<img
|
||||||
key={authorLink.author.id}
|
key={authorLink.author.id}
|
||||||
@ -202,7 +242,6 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
className="h-6 w-6 rounded-full mr-1"
|
className="h-6 w-6 rounded-full mr-1"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900">
|
||||||
{writerAuthors.map((a, i) => (
|
{writerAuthors.map((a, i) => (
|
||||||
@ -248,6 +287,7 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@ -258,6 +298,7 @@ export const ArticleList = React.memo(function ArticleList({
|
|||||||
<span className="ml-2 text-sm text-gray-700"></span>
|
<span className="ml-2 text-sm text-gray-700"></span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && isAdmin && (
|
{error && isAdmin && (
|
||||||
<div className="p-4 text-red-700 bg-red-50 rounded-md">{error}</div>
|
<div className="p-4 text-red-700 bg-red-50 rounded-md">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user