feat(registry): массовое выделение задач через Ctrl/Cmd и Shift + клик
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -289,6 +289,14 @@ const TaskView = () => {
|
||||
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user