end fase 2
This commit is contained in:
118
frontend/src/components/IdeasTable/ColorPickerCell.tsx
Normal file
118
frontend/src/components/IdeasTable/ColorPickerCell.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, Popover, IconButton, Tooltip } from '@mui/material';
|
||||
import { Circle, Clear } from '@mui/icons-material';
|
||||
import type { Idea } from '../../types/idea';
|
||||
import { useUpdateIdea } from '../../hooks/useIdeas';
|
||||
|
||||
// Предустановленные цвета
|
||||
const COLORS = [
|
||||
'#ef5350', // красный
|
||||
'#ff7043', // оранжевый
|
||||
'#ffca28', // жёлтый
|
||||
'#66bb6a', // зелёный
|
||||
'#42a5f5', // синий
|
||||
'#ab47bc', // фиолетовый
|
||||
'#8d6e63', // коричневый
|
||||
'#78909c', // серый
|
||||
];
|
||||
|
||||
interface ColorPickerCellProps {
|
||||
idea: Idea;
|
||||
}
|
||||
|
||||
export function ColorPickerCell({ idea }: ColorPickerCellProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const updateIdea = useUpdateIdea();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleColorSelect = (color: string | null) => {
|
||||
updateIdea.mutate({
|
||||
id: idea.id,
|
||||
dto: { color },
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Выбрать цвет">
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
data-testid="color-picker-trigger"
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: idea.color ?? 'transparent',
|
||||
border: idea.color ? 'none' : '2px dashed',
|
||||
borderColor: 'divider',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
'data-testid': 'color-picker-popover',
|
||||
} as React.HTMLAttributes<HTMLDivElement>,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 1, display: 'flex', gap: 0.5, flexWrap: 'wrap', maxWidth: 180 }}>
|
||||
{COLORS.map((color) => (
|
||||
<IconButton
|
||||
key={color}
|
||||
size="small"
|
||||
onClick={() => handleColorSelect(color)}
|
||||
data-testid={`color-option-${color.replace('#', '')}`}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
border: idea.color === color ? '2px solid' : 'none',
|
||||
borderColor: 'primary.main',
|
||||
}}
|
||||
>
|
||||
<Circle sx={{ color, fontSize: 24 }} />
|
||||
</IconButton>
|
||||
))}
|
||||
<Tooltip title="Убрать цвет">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleColorSelect(null)}
|
||||
data-testid="color-clear-button"
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<Clear sx={{ fontSize: 24, color: 'text.secondary' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -30,6 +30,7 @@ export function DragHandle() {
|
||||
<Box
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
data-testid="drag-handle"
|
||||
sx={{
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
display: 'flex',
|
||||
@ -79,7 +80,7 @@ export function DraggableRow({ row }: DraggableRowProps) {
|
||||
|
||||
return (
|
||||
<DragHandleContext.Provider value={{ attributes, listeners, isDragging }}>
|
||||
<TableRow ref={setNodeRef} hover sx={style}>
|
||||
<TableRow ref={setNodeRef} hover sx={style} data-testid={`idea-row-${row.original.id}`}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useState, Fragment } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
@ -33,6 +33,7 @@ import {
|
||||
Box,
|
||||
Typography,
|
||||
TablePagination,
|
||||
Collapse,
|
||||
} from '@mui/material';
|
||||
import { Inbox } from '@mui/icons-material';
|
||||
import {
|
||||
@ -43,8 +44,9 @@ import {
|
||||
import { useIdeasStore } from '../../store/ideas';
|
||||
import { createColumns } from './columns';
|
||||
import { DraggableRow } from './DraggableRow';
|
||||
import { CommentsPanel } from '../CommentsPanel';
|
||||
|
||||
const SKELETON_COLUMNS_COUNT = 8;
|
||||
const SKELETON_COLUMNS_COUNT = 9;
|
||||
|
||||
export function IdeasTable() {
|
||||
const { data, isLoading, isError } = useIdeasQuery();
|
||||
@ -55,10 +57,21 @@ export function IdeasTable() {
|
||||
|
||||
// ID активно перетаскиваемого элемента
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
// ID идеи с раскрытыми комментариями
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
const handleToggleComments = (id: string) => {
|
||||
setExpandedId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => createColumns((id) => deleteIdea.mutate(id)),
|
||||
[deleteIdea],
|
||||
() =>
|
||||
createColumns({
|
||||
onDelete: (id) => deleteIdea.mutate(id),
|
||||
onToggleComments: handleToggleComments,
|
||||
expandedId,
|
||||
}),
|
||||
[deleteIdea, expandedId],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
@ -140,7 +153,7 @@ export function IdeasTable() {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
|
||||
<Paper sx={{ width: '100%', overflow: 'hidden' }} data-testid="ideas-table-container">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@ -149,7 +162,7 @@ export function IdeasTable() {
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table stickyHeader size="small">
|
||||
<Table stickyHeader size="small" data-testid="ideas-table">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
@ -214,6 +227,7 @@ export function IdeasTable() {
|
||||
alignItems: 'center',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
data-testid="ideas-empty-state"
|
||||
>
|
||||
<Inbox sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
|
||||
<Typography variant="h6">Идей пока нет</Typography>
|
||||
@ -229,7 +243,19 @@ export function IdeasTable() {
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{rows.map((row) => (
|
||||
<DraggableRow key={row.id} row={row} />
|
||||
<Fragment key={row.id}>
|
||||
<DraggableRow row={row} />
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={SKELETON_COLUMNS_COUNT}
|
||||
sx={{ p: 0, borderBottom: expandedId === row.original.id ? 1 : 0, borderColor: 'divider' }}
|
||||
>
|
||||
<Collapse in={expandedId === row.original.id} unmountOnExit>
|
||||
<CommentsPanel ideaId={row.original.id} />
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
))}
|
||||
</SortableContext>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { Chip, Box, IconButton } from '@mui/material';
|
||||
import { Delete } from '@mui/icons-material';
|
||||
import { Chip, Box, IconButton, Tooltip } from '@mui/material';
|
||||
import { Delete, Comment, ExpandLess } from '@mui/icons-material';
|
||||
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
import { EditableCell } from './EditableCell';
|
||||
import { ColorPickerCell } from './ColorPickerCell';
|
||||
import { statusOptions, priorityOptions } from './constants';
|
||||
import { DragHandle } from './DraggableRow';
|
||||
|
||||
@ -29,7 +30,13 @@ const priorityColors: Record<
|
||||
critical: 'error',
|
||||
};
|
||||
|
||||
export const createColumns = (onDelete: (id: string) => void) => [
|
||||
interface ColumnsConfig {
|
||||
onDelete: (id: string) => void;
|
||||
onToggleComments: (id: string) => void;
|
||||
expandedId: string | null;
|
||||
}
|
||||
|
||||
export const createColumns = ({ onDelete, onToggleComments, expandedId }: ColumnsConfig) => [
|
||||
columnHelper.display({
|
||||
id: 'drag',
|
||||
header: '',
|
||||
@ -37,6 +44,12 @@ export const createColumns = (onDelete: (id: string) => void) => [
|
||||
size: 40,
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor('color', {
|
||||
header: 'Цвет',
|
||||
cell: (info) => <ColorPickerCell idea={info.row.original} />,
|
||||
size: 60,
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor('title', {
|
||||
header: 'Название',
|
||||
cell: (info) => (
|
||||
@ -143,15 +156,33 @@ export const createColumns = (onDelete: (id: string) => void) => [
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: (info) => (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onDelete(info.row.original.id)}
|
||||
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
size: 50,
|
||||
cell: (info) => {
|
||||
const ideaId = info.row.original.id;
|
||||
const isExpanded = expandedId === ideaId;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title="Комментарии">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onToggleComments(ideaId)}
|
||||
color={isExpanded ? 'primary' : 'default'}
|
||||
data-testid="toggle-comments-button"
|
||||
sx={{ opacity: isExpanded ? 1 : 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
{isExpanded ? <ExpandLess fontSize="small" /> : <Comment fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onDelete(ideaId)}
|
||||
data-testid="delete-idea-button"
|
||||
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
size: 90,
|
||||
}),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user