From 62faad92bb2223afe808e75a89e87af8c3ee3623 Mon Sep 17 00:00:00 2001 From: lizerk Date: Mon, 24 Aug 2026 15:08:48 +0300 Subject: [PATCH] Enable direct navigation to task attachments and comments --- frontend/src/app/tasks/page.tsx | 94 ++++++++++++++++--- .../src/components/shared/CommentControls.tsx | 5 +- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/tasks/page.tsx b/frontend/src/app/tasks/page.tsx index 4f852ad9..1ca14b50 100644 --- a/frontend/src/app/tasks/page.tsx +++ b/frontend/src/app/tasks/page.tsx @@ -656,7 +656,7 @@ function getMessageCreatedLabel(message?: Message | null) { type TaskCardViewProps = { task: TaskCard; - onOpen: (task: TaskCard) => void; + onOpen: (task: TaskCard, target?: TaskViewTarget) => void; onEdit: (task: TaskCard) => void; onDelete: (task: TaskCard) => void; currentUserId?: number; @@ -668,6 +668,8 @@ type TaskCardViewProps = { onToggleTaskMenu: (taskId: number, anchor: HTMLButtonElement) => void; }; +type TaskViewTarget = "attachments" | "comments"; + type TaskCardDragBinding = { attributes?: ReturnType["attributes"]; listeners?: ReturnType["listeners"]; @@ -845,10 +847,20 @@ const TaskCardContent = memo(function TaskCardContent({ ) : null} {(task.attachments_count || 0) > 0 ? ( - + ) : null} {(task.linked_objects_count || task.linked_messages_count || 0) > 0 ? ( @@ -857,10 +869,20 @@ const TaskCardContent = memo(function TaskCardContent({ ) : null} {(task.comments_count || 0) > 0 ? ( - + ) : null} @@ -1307,7 +1329,7 @@ function TaskAnchoredMenuPopover({ type TaskColumnCardsProps = { tasks: TaskCard[]; - onOpenTask: (task: TaskCard) => void; + onOpenTask: (task: TaskCard, target?: TaskViewTarget) => void; onEditTask: (task: TaskCard) => void; onDeleteTask: (task: TaskCard) => void; currentUserId?: number; @@ -1397,7 +1419,7 @@ function BoardColumn({ tasks: TaskCard[]; displayTasksCount?: number; onCreateTask: (columnId: number, rowId: number | null) => void; - onOpenTask: (task: TaskCard) => void; + onOpenTask: (task: TaskCard, target?: TaskViewTarget) => void; onEditTask: (task: TaskCard) => void; onDeleteTask: (task: TaskCard) => void; currentUserId?: number; @@ -3757,6 +3779,7 @@ function TasksPageContent() { const [activeColumnDragId, setActiveColumnDragId] = useState(null); const [columnDragOverId, setColumnDragOverId] = useState(null); const [viewTaskId, setViewTaskId] = useState(null); + const [taskViewTarget, setTaskViewTarget] = useState(null); const [taskModalOpen, setTaskModalOpen] = useState(false); const [quickTaskTarget, setQuickTaskTarget] = useState(null); const [quickTaskTitle, setQuickTaskTitle] = useState(""); @@ -3848,6 +3871,7 @@ function TasksPageContent() { const [editingCommentId, setEditingCommentId] = useState(null); const [editingCommentDraft, setEditingCommentDraft] = useState(""); const [taskCommentsLoading, setTaskCommentsLoading] = useState(false); + const [taskCommentsLoadedForId, setTaskCommentsLoadedForId] = useState(null); const [commentDraft, setCommentDraft] = useState(""); const [commentFiles, setCommentFiles] = useState([]); const [coverPickerOpen, setCoverPickerOpen] = useState(false); @@ -3891,6 +3915,9 @@ function TasksPageContent() { const columnNodeRefs = useRef>(new Map()); const boardSearchInputRef = useRef(null); const taskAttachmentInputRef = useRef(null); + const taskAttachmentsSectionRef = useRef(null); + const taskCommentsSectionRef = useRef(null); + const taskCommentComposerRef = useRef(null); const taskCoverUploadInputRef = useRef(null); const taskFormAttachmentInputRef = useRef(null); const taskChecklistDraftRef = useRef(null); @@ -4280,6 +4307,7 @@ function TasksPageContent() { setError(getTaskError(commentsError, "Не удалось загрузить комментарии")); } finally { setTaskCommentsLoading(false); + setTaskCommentsLoadedForId(taskId); } }, []); @@ -4886,6 +4914,7 @@ function TasksPageContent() { setTaskExternalLinks([]); setTaskActivities([]); setTaskComments([]); + setTaskCommentsLoadedForId(null); setTaskAttachments([]); setTaskChecklist([]); setTaskChecklistLoaded(false); @@ -4920,6 +4949,36 @@ function TasksPageContent() { void loadTaskAttachments(viewTaskId); }, [attachmentsOpen, loadTaskAttachments, viewTask?.attachments_count, viewTaskId]); + useEffect(() => { + if (!viewTaskId || !taskViewTarget) return; + if (taskViewTarget === "attachments" && !attachmentsOpen) return; + if (taskViewTarget === "comments" && !commentsOpen) return; + if (taskViewTarget === "comments" && taskCommentsLoadedForId !== viewTaskId) return; + + let focusFrame: number | null = null; + const frame = window.requestAnimationFrame(() => { + const targetSection = taskViewTarget === "comments" + ? taskCommentsSectionRef.current + : taskAttachmentsSectionRef.current; + targetSection?.scrollIntoView({ block: "start" }); + + if (taskViewTarget === "comments") { + focusFrame = window.requestAnimationFrame(() => { + taskCommentComposerRef.current?.scrollIntoView({ block: "center" }); + taskCommentComposerRef.current?.focus({ preventScroll: true }); + setTaskViewTarget(null); + }); + return; + } + setTaskViewTarget(null); + }); + + return () => { + window.cancelAnimationFrame(frame); + if (focusFrame !== null) window.cancelAnimationFrame(focusFrame); + }; + }, [attachmentsOpen, commentsOpen, taskCommentsLoadedForId, taskViewTarget, viewTaskId]); + useEffect(() => { if (!viewTaskId || !checklistOpen) return; void loadTaskChecklist(viewTaskId); @@ -5040,8 +5099,9 @@ function TasksPageContent() { setTaskModalOpen(true); }, [clearTaskFormResources, leafColumns, subcolumnsByParent, user?.id]); - const openTaskView = useCallback((task: TaskCard) => { + const openTaskView = useCallback((task: TaskCard, target?: TaskViewTarget) => { setViewTaskId(task.id); + setTaskViewTarget(target || null); setParticipantModalOpen(false); setParticipantModalSearch(""); setParticipantModalSelection([]); @@ -5053,8 +5113,8 @@ function TasksPageContent() { setViewTaskDueDateDraft(task.due_date || ""); setViewColumnMenuOpen(false); setLinkedObjectsOpen(false); - setCommentsOpen(false); - setAttachmentsOpen(false); + setCommentsOpen(target === "comments"); + setAttachmentsOpen(target === "attachments"); setChecklistOpen(false); setActivityOpen(false); setTaskDetailsOpen(false); @@ -5068,6 +5128,7 @@ function TasksPageContent() { setLinkedGuestVisits([]); setLinkedAttendanceRecords([]); setTaskComments([]); + setTaskCommentsLoadedForId(null); setEditingCommentId(null); setEditingCommentDraft(""); setTaskAttachments([]); @@ -5106,6 +5167,7 @@ function TasksPageContent() { const closeTaskView = useCallback(() => { if (saving || taskAttachmentsUploading || taskChecklistSaving) return; setViewTaskId(null); + setTaskViewTarget(null); setTaskMenuOpen(false); setTaskAddMenuOpen(false); setTaskAddPanel(null); @@ -5122,6 +5184,7 @@ function TasksPageContent() { setLinkedGuestVisits([]); setLinkedAttendanceRecords([]); setTaskComments([]); + setTaskCommentsLoadedForId(null); setEditingCommentId(null); setEditingCommentDraft(""); setTaskAttachments([]); @@ -9620,7 +9683,10 @@ function TasksPageContent() { ) : null} {attachmentsBadgeCount > 0 || attachmentsOpen ? ( -
+