feat(registry): массовое выделение задач через Ctrl/Cmd и Shift + клик

This commit is contained in:
2026-07-08 13:55:30 +03:00
parent 6826d82850
commit eec350c6da
2 changed files with 52 additions and 2 deletions

View File

@@ -116,6 +116,7 @@ interface TaskRegistryProps {
selectedTaskIds?: Set<number>;
onToggleSelected?: (taskId: number) => void;
onToggleSelectAll?: () => void;
onSelectRange?: (taskIds: number[]) => void;
}
// Отображает значение поля с учётом типа
@@ -253,11 +254,13 @@ const REGISTRY_INLINE_TYPES = new Set([
// Типы полей, которые переключаются по одиночному клику
const REGISTRY_QUICK_TOGGLE_TYPES = new Set(['checkbox', 'toggle']);
const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = [], roles = [], formId, onFilteredCountChange, onTaskClick, filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters, searches, onAddSearch, onRemoveSearch, isAdmin, selectedTaskIds, onToggleSelected, onToggleSelectAll }: TaskRegistryProps) => {
const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = [], roles = [], formId, onFilteredCountChange, onTaskClick, filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters, searches, onAddSearch, onRemoveSearch, isAdmin, selectedTaskIds, onToggleSelected, onToggleSelectAll, onSelectRange }: TaskRegistryProps) => {
const { toast } = useToast();
// Текущая редактируемая ячейка: { taskId, columnKey }
const [editingCell, setEditingCell] = useState<{ taskId: number; columnKey: string } | null>(null);
// Последняя выделенная задача — якорь для Shift+клика
const [lastSelectedTaskId, setLastSelectedTaskId] = useState<number | null>(null);
// Функция для получения имени пользователя по ID
const getUserName = (userId: number): string => {
@@ -615,6 +618,39 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
}
};
const handleRowClick = (task: TaskData, e: React.MouseEvent) => {
if (!isAdmin || !onToggleSelected) {
onTaskClick?.(task);
return;
}
const isCtrl = e.ctrlKey || e.metaKey;
const isShift = e.shiftKey;
if (isShift && lastSelectedTaskId != null) {
e.preventDefault();
const ids = filteredTasks.map(t => t.id);
const start = ids.indexOf(lastSelectedTaskId);
const end = ids.indexOf(task.id);
if (start !== -1 && end !== -1) {
const range = ids.slice(Math.min(start, end), Math.max(start, end) + 1);
onSelectRange?.(range);
}
setLastSelectedTaskId(task.id);
return;
}
if (isCtrl) {
e.preventDefault();
onToggleSelected(task.id);
setLastSelectedTaskId(task.id);
return;
}
// Обычный клик по строке открывает задачу
onTaskClick?.(task);
};
const isEditing = (taskId: number, columnKey: string) =>
editingCell?.taskId === taskId && editingCell?.columnKey === columnKey;
@@ -855,7 +891,12 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
const isOverdue = !isCompleted && !!task.dueDate && new Date(task.dueDate) < new Date();
return (
<TableRow key={task.id} data-testid={`row-task-${task.id}`} className={`${isCompleted ? 'bg-gray-100 dark:bg-gray-900 opacity-70' : ''} ${isOverdue ? 'border-l-2 border-l-red-500 bg-red-50 dark:bg-red-950/20' : ''}`}>
<TableRow
key={task.id}
data-testid={`row-task-${task.id}`}
className={`${isCompleted ? 'bg-gray-100 dark:bg-gray-900 opacity-70' : ''} ${isOverdue ? 'border-l-2 border-l-red-500 bg-red-50 dark:bg-red-950/20' : ''} ${isAdmin ? 'cursor-pointer' : ''}`}
onClick={(e) => handleRowClick(task, e)}
>
{isAdmin && (
<TableCell className="py-1 px-2 w-8" onClick={(e) => e.stopPropagation()}>
<Checkbox