diff --git a/client/src/components/TaskRegistry.tsx b/client/src/components/TaskRegistry.tsx index 7408497..abe6454 100644 --- a/client/src/components/TaskRegistry.tsx +++ b/client/src/components/TaskRegistry.tsx @@ -116,6 +116,7 @@ interface TaskRegistryProps { selectedTaskIds?: Set; 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(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 ( - + handleRowClick(task, e)} + > {isAdmin && ( e.stopPropagation()}> { const clearSelection = () => setSelectedTaskIds(new Set()); + const selectRange = (taskIds: number[]) => { + setSelectedTaskIds(prev => { + const next = new Set(prev); + taskIds.forEach(id => next.add(id)); + return next; + }); + }; + // Автооткрытие диалога создания задачи при предзаполнении из URL (QR-коды). // Сначала запускаем автоматизации task.before_create через check-create endpoint. // Диалог открывается только если автоматизации не блокируют создание. @@ -478,6 +486,7 @@ const TaskView = () => { selectedTaskIds={selectedTaskIds} onToggleSelected={toggleTask} onToggleSelectAll={selectAllVisible} + onSelectRange={selectRange} />