update tanstack-table

This commit is contained in:
smanylov
2026-03-20 19:45:18 +07:00
parent 38bfc94125
commit 9d90c81f64
3 changed files with 290 additions and 235 deletions
+30 -6
View File
@@ -4,8 +4,25 @@ import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL } from '@/app/actions/definitions';
export async function getUserFilesData(page: number, pageSize: number) { export async function getUserFilesData({
page,
pageSize,
sortBy,
sortOrder,
type,
date,
query = ''
}: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc' | string;
type?: string;
date?: string;
query?: string;
}) {
const token = await getSessionData('token'); const token = await getSessionData('token');
/* console.log('getUserFilesData', { page, pageSize, sortBy, sortOrder, type, date, query }); */
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -14,10 +31,15 @@ export async function getUserFilesData(page: number, pageSize: number) {
version: 1, version: 1,
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'user_files', action: 'search_files',
token: token,
page: page, page: page,
page_size: pageSize, page_size: pageSize,
token: token query: query,
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
type: type || '', // image, video, document, audio
date: date || '', // today, week, month
sort_order: sortOrder
} }
}), }),
headers: { headers: {
@@ -28,17 +50,19 @@ export async function getUserFilesData(page: number, pageSize: number) {
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
} else { } else {
throw parsed; throw new Error(parsed.message_text || 'API error');
} }
} else { } else {
throw (`${response.status}`); throw new Error(`${response.status}`);
} }
} catch (error) { } catch (error) {
return error console.error('getUserFilesData error:', error);
return error;
} }
} }
+237 -229
View File
@@ -10,8 +10,9 @@ import {
ColumnDef, ColumnDef,
SortingState, SortingState,
ColumnFiltersState, ColumnFiltersState,
PaginationState
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconInfo } from '@/app/ui/icons/icons'; import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconInfo } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl'; import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/DropDownList'; import DropDownList from '@/app/components/DropDownList';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
@@ -27,12 +28,8 @@ import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { MonitoringDropDown } from '@/app/components/tanstak-table/MonitoringDropDown'; import { MonitoringDropDown } from '@/app/components/tanstak-table/MonitoringDropDown';
import { formatDate, formatDateTime } from '@/app/lib/formatDate'; import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import { useViewport } from '@/app/hooks/useViewport'; import { useViewport } from '@/app/hooks/useViewport';
import { API_BASE_URL } from '@/app/actions/definitions'; import { downloadFile } from '@/app/actions/fileEntity';
import { getSessionData } from '@/app/actions/session'; import { useDebouncedCallback } from 'use-debounce';
import { downloadFile } from '@/app/actions/fileEntity'
type FileType = 'image' | 'video' | 'audio';
export type FileItem = { export type FileItem = {
id: string; id: string;
@@ -64,6 +61,9 @@ type ApiFile = {
type ApiResponse = { type ApiResponse = {
files?: ApiFile[]; files?: ApiFile[];
total_count: number;
page: number;
page_size: number;
}; };
const cutFileExtension = (fileName: string) => { const cutFileExtension = (fileName: string) => {
@@ -73,37 +73,107 @@ const cutFileExtension = (fileName: string) => {
} }
export default function TanstakFilesTable({ fileType }: { fileType: string }) { export default function TanstakFilesTable({ fileType }: { fileType: string }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [dateFilter, setDateFilter] = useState<string>('month');
const [typeFilter, setTypeFilter] = useState<string>(fileType);
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [query, setQuery] = useState<string>('');
const getSortParams = useMemo(() => {
if (sorting.length === 0) {
return { sortBy: '', sortOrder: undefined };
}
const sort = sorting[0];
const sortByMap: Record<string, string> = {
'id': 'supportId',
'fileName': 'fileName',
'size': 'fileSize',
'uploadDate': 'updatedAt',
'monitoring': 'monitoring'
};
return {
sortBy: sortByMap[sort.id] || sort.id,
sortOrder: sort.desc ? 'desc' : 'asc'
};
}, [sorting]);
// Преобразование фильтра типа
const getTypeParam = useMemo(() => {
return typeFilter !== 'all' ? typeFilter : '';
}, [typeFilter]);
// Преобразование фильтра даты
const getDateParam = useMemo(() => {
return dateFilter !== 'all' ? dateFilter : '';
}, [dateFilter]);
const { const {
data: tableData, data: apiResponse,
isLoading, isLoading,
isError, isError,
error, error,
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({ refetch
queryKey: ['userFilesData', 1, 10000], } = useQuery({
queryFn: () => getUserFilesData(1, 10000), queryKey: [
'userFilesData',
pagination.pageIndex,
pagination.pageSize,
getSortParams.sortBy,
getSortParams.sortOrder,
getTypeParam,
getDateParam,
query
],
queryFn: () => getUserFilesData({
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
sortBy: getSortParams.sortBy,
sortOrder: getSortParams?.sortOrder,
type: getTypeParam,
date: getDateParam,
query: query
}),
select: (data: any): {
items: FileItem[];
total_count: number;
page: number;
pageSize: number;
} => {
if (!data?.files) {
return { items: [], total_count: 0, page: 1, pageSize: pagination.pageSize };
}
select: (data: ApiResponse): FileItem[] => { const items = data.files.map((item: ApiFile) => ({
if (!data?.files) return []; id: item.id,
fileName: item.originalFileName,
fileType: item.mimeType.toLowerCase(),
violations: item.violations,
size: item.fileSize,
uploadDate: new Date(item.updatedAt).getTime(),
status: item.status,
monitoring: item.monitoring,
protectStatus: item.protectStatus,
supportId: item.supportId,
_original: item
}));
return data.files.map((item: ApiFile) => { return {
const newDate = new Date(item.updatedAt).getTime(); items,
total_count: data.total_count || data.files.length, // предполагаем, что сервер возвращает total
return { page: data.page || pagination.pageIndex + 1,
id: item.id, pageSize: data.page_size || pagination.pageSize
fileName: item.originalFileName, };
fileType: item.mimeType.toLocaleLowerCase(),
violations: item.violations,
size: item.fileSize,
uploadDate: newDate,
status: item.status,
monitoring: item.monitoring,
protectStatus: item.protectStatus,
supportId: item.supportId,
_original: item
};
});
}, },
refetchInterval: 30000 refetchInterval: 30000,
placeholderData: (previousData) => previousData // для оптимистичных обновлений
}); });
const { const {
@@ -124,15 +194,12 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Состояния // Данные для таблицы
const [sorting, setSorting] = useState<SortingState>([]); const tableData = apiResponse?.items || [];
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const totalItems = apiResponse?.total_count || 0;
const [dateFilter, setDateFilter] = useState<string>('all'); const totalPages = Math.ceil(totalItems / pagination.pageSize);
const [typeFilter, setTypeFilter] = useState<string>(fileType);
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [openWindow, setOpenWindow] = useState<boolean>(false); const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null); const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
@@ -256,48 +323,6 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
), ),
enableColumnFilter: false, enableColumnFilter: false,
}, },
/* {
accessorKey: 'violations',
header: ({ column }) => (
<div className="column">
<span>
{t('violations')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
let classCollor = () => {
let result = ''
if (row.original.violations !== undefined) {
result = row.original.violations > 0 ? 'text-red-600' : 'text-green-600'
}
return result;
}
return (
<div className={`text-center font-semibold ${classCollor()}`}>
{row.original.violations !== undefined ? row.original.violations : '-'}
</div>
)
},
}, */
{ {
accessorKey: 'size', accessorKey: 'size',
header: ({ column }) => ( header: ({ column }) => (
@@ -420,12 +445,6 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
</div> </div>
) )
}, },
/* sortingFn: (rowA, rowB) => {
console.log(rowA.original.monitoring, rowB.original.monitoring)
const monitoringA = rowA.original.monitoring;
const monitoringB = rowA.original.monitoring;
return monitoringA - monitoringB;
}, */
}, },
{ {
id: 'actions', id: 'actions',
@@ -439,22 +458,6 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
cell: ({ row }) => ( cell: ({ row }) => (
<div className="actions"> <div className="actions">
<div className="actions-group"> <div className="actions-group">
{/* <button
onClick={() => handleMonitoring(row.original)}
className="bg-violet-500 hover:bg-violet-600"
title={t('monitoring')}
>
<IconEye />
</button> */}
{/* <button
onClick={() => handleOpenWindowForRemove(row.original)}
className="table-action-delete"
>
<IconDelete />
<span>
{t('delete')}
</span>
</button> */}
</div> </div>
<div className="actions-group"> <div className="actions-group">
{row.original.protectStatus === 'PROTECTED' && ( {row.original.protectStatus === 'PROTECTED' && (
@@ -603,7 +606,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
} }
return result; return result;
}, [tableData, typeFilter, dateFilter]); }, [query, typeFilter, dateFilter]);
useEffect(() => { useEffect(() => {
const currentPageRows = table.getRowModel().rows; const currentPageRows = table.getRowModel().rows;
@@ -616,33 +619,53 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
// Создание таблицы // Создание таблицы
const table = useReactTable({ const table = useReactTable({
data: filteredData, data: tableData,
columns, columns,
state: { state: {
sorting, sorting,
columnFilters, pagination,
pagination
}, },
autoResetPageIndex: false, pageCount: totalPages,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
onPaginationChange: setPagination, onPaginationChange: setPagination,
onSortingChange: setSorting, onSortingChange: (updater) => {
onColumnFiltersChange: setColumnFilters, const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
setSorting(newSorting);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
},
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
}); });
const handleDateFilterChange = (newDateFilter: string) => {
setDateFilter(newDateFilter);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
};
const handleTypeFilterChange = (newTypeFilter: string) => {
setTypeFilter(newTypeFilter);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
};
const handleQueryChange = (newQuery: string) => {
setQuery(newQuery);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
};
const pluralizeFiles = (number: number) => { const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')]; const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale); return pluralize(number, translate[0], translate[1], translate[2], locale);
}; };
useEffect(() => {
if (tableData.length === 0 && pagination.pageIndex > 0 && totalPages > 0) {
setPagination(prev => ({ ...prev, pageIndex: Math.max(0, totalPages - 1) }));
}
}, [tableData, pagination.pageIndex, totalPages]);
return ( return (
<div className="tanstak-table-wrapper"> <div className="tanstak-table-wrapper">
<h3 <h3
@@ -674,34 +697,23 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
translatedValue={(() => { translatedValue={(() => {
switch (dateFilter) { switch (dateFilter) {
case 'all': case 'all':
return t('all-dates') return t('all-dates');
case 'today':
return t('today');
case 'week': case 'week':
return t('for-a-week') return t('for-a-week');
case 'month': case 'month':
return t('for-a-month') return t('for-a-month');
case 'older':
return t('older-than-a-month')
default: default:
return t('today') return t('all-dates');
} }
})()} })()}
callBack={setDateFilter} callBack={handleDateFilterChange}
> >
<li value="all"> <li value="all">{t('all-dates')}</li>
{t('all-dates')} <li value="today">{t('today')}</li>
</li> <li value="week">{t('for-a-week')}</li>
<li value="today"> <li value="month">{t('for-a-month')}</li>
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList> </DropDownList>
</div> </div>
@@ -710,57 +722,55 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
<div className="table-filtres-label">{t('type-filter')}:</div> <div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList <DropDownList
value={typeFilter} value={typeFilter}
translatedValue={ translatedValue={(() => {
(() => { switch (typeFilter) {
switch (typeFilter) { case 'image':
case 'image': return t('images');
return t('images') case 'video':
case 'video': return t('videos');
return t('videos') case 'audio':
case 'audio': return t('audios');
return t('audios') case 'document':
case 'document': return t('documents');
return t('documents') default:
default: return t('all-types');
return t('all-types') }
} })()}
})() callBack={handleTypeFilterChange}
}
callBack={setTypeFilter}
> >
<li value="all"> <li value="all">{t('all-types')}</li>
{t('all-types')} <li value="image">{t('images-few')}</li>
</li> <li value="video">{t('videos')}</li>
<li value="image"> <li value="audio">{t('audios')}</li>
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
{!referralLink && ( {!referralLink && (
<li value="document"> <li value="document">{t('documents')}</li>
{t('documents')}
</li>
)} )}
</DropDownList> </DropDownList>
</div> </div>
)} )}
<div className="table-filtres-item">
<div className="table-filtres-label">{t('search')}:</div>
<input
type="text"
className="table-filtres-text-filter"
value={query}
onChange={(e) => handleQueryChange(e.target.value)}
placeholder={t('search')}
/>
</div>
</div> </div>
<div <div className="table-filtres-wrapper">
className="table-filtres-wrapper"
>
<div className="table-filtres-item"> <div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div> <div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList <DropDownList
value={table.getState().pagination.pageSize} value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра callBack={(newSize: string) => {
callBack={table.setPageSize} table.setPageSize(Number(newSize));
setPagination(prev => ({ ...prev, pageIndex: 0 }));
}}
> >
{[5, 10, 20, 50, 100].map(pageSize => ( {['5', '10', '20', '50', '100'].map(pageSize => (
<li key={pageSize} value={pageSize}> <li key={pageSize} value={pageSize}>
{t('show')} {pageSize} {t('show')} {pageSize}
</li> </li>
@@ -768,9 +778,14 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
</DropDownList> </DropDownList>
</div> </div>
</div> </div>
</div> </div>
{/* {isLoading && (
<div className="loading-indicator">
loading...
</div>
)} */}
{/* Таблица */} {/* Таблица */}
<div className="tanstak-table-block"> <div className="tanstak-table-block">
<table className="tanstak-table"> <table className="tanstak-table">
@@ -821,53 +836,50 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
<span className="pagination-info-pages"> <span className="pagination-info-pages">
{t('page')}{' '} {t('page')}{' '}
<strong> <strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1} {pagination.pageIndex + 1} {t('out-of')} {totalPages || 1}
</strong> </strong>
</span> </span>
<span className="pagination-info-files"> <span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)} | {t('shown')} {tableData.length} {t('out-of')} {totalItems} {pluralizeFiles(totalItems)}
</span> </span>
</div> </div>
<div className="pagination-controls"> <div className="pagination-controls">
{table.getCanPreviousPage() && ( <button
<button className="arrow"
className="arrow" onClick={() => table.firstPage()}
onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage() || isLoading}
disabled={!table.getCanPreviousPage()} >
> <IconDoubleArrowLeft />
<IconDoubleArrowLeft /> </button>
</button> <button
)} className="arrow"
{table.getCanPreviousPage() && ( onClick={() => table.previousPage()}
<button disabled={!table.getCanPreviousPage() || isLoading}
className="arrow" >
onClick={() => table.previousPage()} <IconArrowLeft />
disabled={!table.getCanPreviousPage()} </button>
>
<IconArrowLeft />
</button>
)}
<div className="pagination-controls-pages"> <div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => { {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageIndex = Math.max( let pageIndex;
0, if (totalPages <= 5) {
Math.min( pageIndex = i;
table.getPageCount() - 5, } else if (pagination.pageIndex <= 2) {
table.getState().pagination.pageIndex - 2 pageIndex = i;
) } else if (pagination.pageIndex >= totalPages - 3) {
) + i; pageIndex = totalPages - 5 + i;
} else {
pageIndex = pagination.pageIndex - 2 + i;
}
if (pageIndex < table.getPageCount()) { if (pageIndex >= 0 && pageIndex < totalPages) {
return ( return (
<button <button
key={pageIndex} key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex className={pagination.pageIndex === pageIndex ? 'current' : 'other'}
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)} onClick={() => table.setPageIndex(pageIndex)}
disabled={isLoading}
> >
{pageIndex + 1} {pageIndex + 1}
</button> </button>
@@ -877,24 +889,20 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
})} })}
</div> </div>
{table.getCanNextPage() && ( <button
<button className="arrow"
className="arrow" onClick={() => table.nextPage()}
onClick={() => table.nextPage()} disabled={!table.getCanNextPage() || isLoading}
disabled={!table.getCanNextPage()} >
> <IconArrowRight />
<IconArrowRight /> </button>
</button> <button
)} className="arrow"
{table.getCanNextPage() && ( onClick={() => table.lastPage()}
<button disabled={!table.getCanNextPage() || isLoading}
className="arrow" >
onClick={() => table.lastPage()} <IconDoubleArrowRight />
disabled={!table.getCanNextPage()} </button>
>
<IconDoubleArrowRight />
</button>
)}
</div> </div>
</div> </div>
</div > </div >
+23
View File
@@ -624,6 +624,29 @@
font-weight: 500; font-weight: 500;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.table-filtres-text-filter {
background-color: #fff;
border: 2px solid #e2e8f0;
border-radius: 12px;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
line-height: 1.25;
transition: background-color .2s ease-in-out;
display: flex;
box-shadow: 0 1px 2px #0000000d;
&:focus {
outline-offset: 2px;
outline: 2px solid #0000;
box-shadow: 0 0 0 2px #6365f187;
}
}
} }
} }