From eec350c6dab2108cbb391a9e7b3eb2ead87d2c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Wed, 8 Jul 2026 13:55:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(registry):=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20Ctrl/Cmd=20=D0=B8=20Shift=20+=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/TaskRegistry.tsx | 45 ++++++++++++++++++++++++-- client/src/pages/TaskView.tsx | 9 ++++++ 2 files changed, 52 insertions(+), 2 deletions(-) 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} />