add auth
This commit is contained in:
91
frontend/src/components/IdeasTable/DraggableRow.tsx
Normal file
91
frontend/src/components/IdeasTable/DraggableRow.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { TableRow, TableCell, Box } from '@mui/material';
|
||||
import { DragIndicator } from '@mui/icons-material';
|
||||
import { flexRender } from '@tanstack/react-table';
|
||||
import type { Row } from '@tanstack/react-table';
|
||||
import type { Idea } from '../../types/idea';
|
||||
|
||||
// Контекст для передачи информации о drag handle в ячейку
|
||||
interface DragHandleContextValue {
|
||||
attributes: ReturnType<typeof useSortable>['attributes'];
|
||||
listeners: ReturnType<typeof useSortable>['listeners'];
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
const DragHandleContext = createContext<DragHandleContextValue | null>(null);
|
||||
|
||||
// Компонент drag handle для использования в колонке
|
||||
export function DragHandle() {
|
||||
const context = useContext(DragHandleContext);
|
||||
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { attributes, listeners, isDragging } = context;
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
sx={{
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'text.secondary',
|
||||
touchAction: 'none',
|
||||
'&:hover': { color: 'text.primary' },
|
||||
}}
|
||||
>
|
||||
<DragIndicator fontSize="small" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface DraggableRowProps {
|
||||
row: Row<Idea>;
|
||||
}
|
||||
|
||||
export function DraggableRow({ row }: DraggableRowProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: row.original.id });
|
||||
|
||||
// Используем CSS.Translate вместо CSS.Transform для лучшей совместимости с таблицами
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
backgroundColor: row.original.color
|
||||
? `${row.original.color}15`
|
||||
: isDragging
|
||||
? 'action.hover'
|
||||
: undefined,
|
||||
position: isDragging ? ('relative' as const) : undefined,
|
||||
zIndex: isDragging ? 1000 : undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: row.original.color
|
||||
? `${row.original.color}25`
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<DragHandleContext.Provider value={{ attributes, listeners, isDragging }}>
|
||||
<TableRow ref={setNodeRef} hover sx={style}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</DragHandleContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -4,6 +4,7 @@ import { Delete } from '@mui/icons-material';
|
||||
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
import { EditableCell } from './EditableCell';
|
||||
import { statusOptions, priorityOptions } from './constants';
|
||||
import { DragHandle } from './DraggableRow';
|
||||
|
||||
const columnHelper = createColumnHelper<Idea>();
|
||||
|
||||
@ -29,6 +30,13 @@ const priorityColors: Record<
|
||||
};
|
||||
|
||||
export const createColumns = (onDelete: (id: string) => void) => [
|
||||
columnHelper.display({
|
||||
id: 'drag',
|
||||
header: '',
|
||||
cell: () => <DragHandle />,
|
||||
size: 40,
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor('title', {
|
||||
header: 'Название',
|
||||
cell: (info) => (
|
||||
|
||||
Reference in New Issue
Block a user