This commit is contained in:
2026-01-14 01:10:01 +03:00
parent 24c5581d7b
commit 2ce092aa59
40 changed files with 2001 additions and 297 deletions

View File

@ -1,9 +1,25 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import {
useReactTable,
getCoreRowModel,
flexRender,
} from '@tanstack/react-table';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
Table,
TableBody,
@ -19,18 +35,27 @@ import {
TablePagination,
} from '@mui/material';
import { Inbox } from '@mui/icons-material';
import { useIdeasQuery, useDeleteIdea } from '../../hooks/useIdeas';
import {
useIdeasQuery,
useDeleteIdea,
useReorderIdeas,
} from '../../hooks/useIdeas';
import { useIdeasStore } from '../../store/ideas';
import { createColumns } from './columns';
import { DraggableRow } from './DraggableRow';
const SKELETON_COLUMNS_COUNT = 7;
const SKELETON_COLUMNS_COUNT = 8;
export function IdeasTable() {
const { data, isLoading, isError } = useIdeasQuery();
const deleteIdea = useDeleteIdea();
const reorderIdeas = useReorderIdeas();
const { sorting, setSorting, pagination, setPage, setLimit } =
useIdeasStore();
// ID активно перетаскиваемого элемента
const [activeId, setActiveId] = useState<string | null>(null);
const columns = useMemo(
() => createColumns((id) => deleteIdea.mutate(id)),
[deleteIdea],
@ -43,8 +68,49 @@ export function IdeasTable() {
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
manualPagination: true,
getRowId: (row) => row.id,
});
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
if (over && active.id !== over.id && data?.data) {
const oldIndex = data.data.findIndex((item) => item.id === active.id);
const newIndex = data.data.findIndex((item) => item.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
// Создаём новый порядок
const items = [...data.data];
const [movedItem] = items.splice(oldIndex, 1);
items.splice(newIndex, 0, movedItem);
// Отправляем на сервер новый порядок
const reorderItems = items.map((item, index) => ({
id: item.id,
order: index,
}));
reorderIdeas.mutate(reorderItems);
}
}
};
const handleSort = (columnId: string) => {
setSorting(columnId);
};
@ -67,101 +133,131 @@ export function IdeasTable() {
);
}
const rows = table.getRowModel().rows;
const rowIds = rows.map((row) => row.original.id);
const activeRow = activeId
? rows.find((row) => row.original.id === activeId)
: null;
return (
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
<TableContainer>
<Table stickyHeader size="small">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableCell
key={header.id}
sx={{
fontWeight: 600,
backgroundColor: 'grey.100',
width: header.getSize(),
}}
>
{header.column.getCanSort() ? (
<TableSortLabel
active={sorting.sortBy === header.id}
direction={
sorting.sortBy === header.id
? (sorting.sortOrder.toLowerCase() as
| 'asc'
| 'desc')
: 'asc'
}
onClick={() => handleSort(header.id)}
>
{flexRender(
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<TableContainer>
<Table stickyHeader size="small">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableCell
key={header.id}
sx={{
fontWeight: 600,
backgroundColor: 'grey.100',
width: header.getSize(),
}}
>
{header.column.getCanSort() ? (
<TableSortLabel
active={sorting.sortBy === header.id}
direction={
sorting.sortBy === header.id
? (sorting.sortOrder.toLowerCase() as
| 'asc'
| 'desc')
: 'asc'
}
onClick={() => handleSort(header.id)}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableSortLabel>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableSortLabel>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)
)}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
Array.from({ length: 5 }).map((_, index) => (
<TableRow key={index}>
{Array.from({ length: SKELETON_COLUMNS_COUNT }).map(
(_, colIndex) => (
<TableCell key={colIndex}>
<Skeleton variant="text" />
</TableCell>
),
)}
)
)}
</TableCell>
))}
</TableRow>
))
) : table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell colSpan={SKELETON_COLUMNS_COUNT}>
<Box
sx={{
py: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
color: 'text.secondary',
}}
>
<Inbox sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
<Typography variant="h6">Идей пока нет</Typography>
<Typography variant="body2">
Создайте первую идею, чтобы начать
</Typography>
</Box>
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
hover
sx={{
backgroundColor: row.original.color
? `${row.original.color}15`
: undefined,
'&:hover': {
backgroundColor: row.original.color
? `${row.original.color}25`
: undefined,
},
}}
))}
</TableHead>
<TableBody>
{isLoading ? (
Array.from({ length: 5 }).map((_, index) => (
<TableRow key={index}>
{Array.from({ length: SKELETON_COLUMNS_COUNT }).map(
(_, colIndex) => (
<TableCell key={colIndex}>
<Skeleton variant="text" />
</TableCell>
),
)}
</TableRow>
))
) : rows.length === 0 ? (
<TableRow>
<TableCell colSpan={SKELETON_COLUMNS_COUNT}>
<Box
sx={{
py: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
color: 'text.secondary',
}}
>
<Inbox sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
<Typography variant="h6">Идей пока нет</Typography>
<Typography variant="body2">
Создайте первую идею, чтобы начать
</Typography>
</Box>
</TableCell>
</TableRow>
) : (
<SortableContext
items={rowIds}
strategy={verticalListSortingStrategy}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
)}
</TableBody>
</Table>
</TableContainer>
<DragOverlay dropAnimation={null}>
{activeRow ? (
<Table
size="small"
sx={{
backgroundColor: 'background.paper',
boxShadow: 6,
borderRadius: 1,
'& td': {
backgroundColor: activeRow.original.color
? `${activeRow.original.color}30`
: 'action.selected',
},
}}
>
<TableBody>
<TableRow>
{activeRow.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
sx={{ width: cell.column.getSize() }}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
@ -169,11 +265,11 @@ export function IdeasTable() {
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</TableBody>
</Table>
) : null}
</DragOverlay>
</DndContext>
{data && (
<TablePagination
component="div"