fix(registry): Shift+клик выделяет диапазон от якоря, якорь хранится в TaskView
This commit is contained in:
@@ -114,9 +114,11 @@ interface TaskRegistryProps {
|
|||||||
// Bulk selection
|
// Bulk selection
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
selectedTaskIds?: Set<number>;
|
selectedTaskIds?: Set<number>;
|
||||||
|
anchorTaskId?: number | null;
|
||||||
onToggleSelected?: (taskId: number) => void;
|
onToggleSelected?: (taskId: number) => void;
|
||||||
onToggleSelectAll?: () => void;
|
onToggleSelectAll?: () => void;
|
||||||
onSelectRange?: (taskIds: number[]) => void;
|
onSelectRange?: (taskIds: number[]) => void;
|
||||||
|
onSetAnchorTaskId?: (taskId: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отображает значение поля с учётом типа
|
// Отображает значение поля с учётом типа
|
||||||
@@ -254,13 +256,11 @@ const REGISTRY_INLINE_TYPES = new Set([
|
|||||||
// Типы полей, которые переключаются по одиночному клику
|
// Типы полей, которые переключаются по одиночному клику
|
||||||
const REGISTRY_QUICK_TOGGLE_TYPES = new Set(['checkbox', 'toggle']);
|
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, onSelectRange }: 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, anchorTaskId, onToggleSelected, onToggleSelectAll, onSelectRange, onSetAnchorTaskId }: TaskRegistryProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Текущая редактируемая ячейка: { taskId, columnKey }
|
// Текущая редактируемая ячейка: { taskId, columnKey }
|
||||||
const [editingCell, setEditingCell] = useState<{ taskId: number; columnKey: string } | null>(null);
|
const [editingCell, setEditingCell] = useState<{ taskId: number; columnKey: string } | null>(null);
|
||||||
// Последняя выделенная задача — якорь для Shift+клика
|
|
||||||
const [lastSelectedTaskId, setLastSelectedTaskId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Функция для получения имени пользователя по ID
|
// Функция для получения имени пользователя по ID
|
||||||
const getUserName = (userId: number): string => {
|
const getUserName = (userId: number): string => {
|
||||||
@@ -635,23 +635,22 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
|
|||||||
const isCtrl = e.ctrlKey || e.metaKey || e.altKey;
|
const isCtrl = e.ctrlKey || e.metaKey || e.altKey;
|
||||||
const isShift = e.shiftKey;
|
const isShift = e.shiftKey;
|
||||||
|
|
||||||
if (isShift && lastSelectedTaskId != null) {
|
if (isShift && anchorTaskId != null) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const ids = filteredTasks.map(t => t.id);
|
const ids = filteredTasks.map(t => t.id);
|
||||||
const start = ids.indexOf(lastSelectedTaskId);
|
const start = ids.indexOf(anchorTaskId);
|
||||||
const end = ids.indexOf(task.id);
|
const end = ids.indexOf(task.id);
|
||||||
if (start !== -1 && end !== -1) {
|
if (start !== -1 && end !== -1) {
|
||||||
const range = ids.slice(Math.min(start, end), Math.max(start, end) + 1);
|
const range = ids.slice(Math.min(start, end), Math.max(start, end) + 1);
|
||||||
onSelectRange?.(range);
|
onSelectRange?.(range);
|
||||||
}
|
}
|
||||||
setLastSelectedTaskId(task.id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCtrl) {
|
if (isCtrl) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onToggleSelected(task.id);
|
onToggleSelected(task.id);
|
||||||
setLastSelectedTaskId(task.id);
|
onSetAnchorTaskId?.(task.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const TaskView = () => {
|
|||||||
const [filteredTasksCount, setFilteredTasksCount] = useState(0);
|
const [filteredTasksCount, setFilteredTasksCount] = useState(0);
|
||||||
const [createTaskDialogOpen, setCreateTaskDialogOpen] = useState(false);
|
const [createTaskDialogOpen, setCreateTaskDialogOpen] = useState(false);
|
||||||
const [selectedTaskIds, setSelectedTaskIds] = useState<Set<number>>(new Set());
|
const [selectedTaskIds, setSelectedTaskIds] = useState<Set<number>>(new Set());
|
||||||
|
const [anchorTaskId, setAnchorTaskId] = useState<number | null>(null);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deleteProgress, setDeleteProgress] = useState<{ processed: number; total: number } | null>(null);
|
const [deleteProgress, setDeleteProgress] = useState<{ processed: number; total: number } | null>(null);
|
||||||
|
|
||||||
@@ -273,6 +274,7 @@ const TaskView = () => {
|
|||||||
setDeleteProgress(null);
|
setDeleteProgress(null);
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/forms', formId, 'tasks', 'with-fields'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/forms', formId, 'tasks', 'with-fields'] });
|
||||||
setSelectedTaskIds(new Set());
|
setSelectedTaskIds(new Set());
|
||||||
|
setAnchorTaskId(null);
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
const desc = data.errors > 0
|
const desc = data.errors > 0
|
||||||
? `Удалено задач: ${data.deleted}, ошибок: ${data.errors}`
|
? `Удалено задач: ${data.deleted}, ошибок: ${data.errors}`
|
||||||
@@ -286,12 +288,23 @@ const TaskView = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggleTask = (taskId: number) => {
|
const toggleTask = (taskId: number) => {
|
||||||
|
let willBeSelected = false;
|
||||||
setSelectedTaskIds(prev => {
|
setSelectedTaskIds(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(taskId)) next.delete(taskId);
|
if (next.has(taskId)) {
|
||||||
else next.add(taskId);
|
next.delete(taskId);
|
||||||
|
willBeSelected = false;
|
||||||
|
} else {
|
||||||
|
next.add(taskId);
|
||||||
|
willBeSelected = true;
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
if (willBeSelected) {
|
||||||
|
setAnchorTaskId(taskId);
|
||||||
|
} else if (anchorTaskId === taskId) {
|
||||||
|
setAnchorTaskId(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllVisible = () => {
|
const selectAllVisible = () => {
|
||||||
@@ -310,9 +323,13 @@ const TaskView = () => {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setAnchorTaskId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSelection = () => setSelectedTaskIds(new Set());
|
const clearSelection = () => {
|
||||||
|
setSelectedTaskIds(new Set());
|
||||||
|
setAnchorTaskId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const selectRange = (taskIds: number[]) => {
|
const selectRange = (taskIds: number[]) => {
|
||||||
setSelectedTaskIds(prev => {
|
setSelectedTaskIds(prev => {
|
||||||
@@ -509,9 +526,11 @@ const TaskView = () => {
|
|||||||
onRemoveSearch={taskFilters.removeSearch}
|
onRemoveSearch={taskFilters.removeSearch}
|
||||||
isAdmin={isAdmin(currentUser)}
|
isAdmin={isAdmin(currentUser)}
|
||||||
selectedTaskIds={selectedTaskIds}
|
selectedTaskIds={selectedTaskIds}
|
||||||
|
anchorTaskId={anchorTaskId}
|
||||||
onToggleSelected={toggleTask}
|
onToggleSelected={toggleTask}
|
||||||
onToggleSelectAll={selectAllVisible}
|
onToggleSelectAll={selectAllVisible}
|
||||||
onSelectRange={selectRange}
|
onSelectRange={selectRange}
|
||||||
|
onSetAnchorTaskId={setAnchorTaskId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user