Merge branch 'develop'

This commit is contained in:
lizerk 2026-08-24 15:08:52 +03:00
commit 1a22321a47
2 changed files with 86 additions and 13 deletions

View File

@ -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<typeof useDraggable>["attributes"];
listeners?: ReturnType<typeof useDraggable>["listeners"];
@ -845,10 +847,20 @@ const TaskCardContent = memo(function TaskCardContent({
</span>
) : null}
{(task.attachments_count || 0) > 0 ? (
<span className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px]">
<button
type="button"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
onOpen(task, "attachments");
}}
className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] transition hover:border-[var(--border-strong)] hover:text-[var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-primary)]"
title="Открыть вложения задачи"
aria-label={`Открыть вложения задачи: ${task.attachments_count}`}
>
<Paperclip size={11} />
{task.attachments_count}
</span>
</button>
) : null}
{(task.linked_objects_count || task.linked_messages_count || 0) > 0 ? (
<span className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px]">
@ -857,10 +869,20 @@ const TaskCardContent = memo(function TaskCardContent({
</span>
) : null}
{(task.comments_count || 0) > 0 ? (
<span className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px]">
<button
type="button"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
onOpen(task, "comments");
}}
className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] transition hover:border-[var(--border-strong)] hover:text-[var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-primary)]"
title="Открыть комментарии задачи"
aria-label={`Открыть комментарии задачи: ${task.comments_count}`}
>
<MessageSquare size={11} />
{task.comments_count}
</span>
</button>
) : null}
</div>
</article>
@ -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<number | null>(null);
const [columnDragOverId, setColumnDragOverId] = useState<number | null>(null);
const [viewTaskId, setViewTaskId] = useState<number | null>(null);
const [taskViewTarget, setTaskViewTarget] = useState<TaskViewTarget | null>(null);
const [taskModalOpen, setTaskModalOpen] = useState(false);
const [quickTaskTarget, setQuickTaskTarget] = useState<QuickTaskTarget | null>(null);
const [quickTaskTitle, setQuickTaskTitle] = useState("");
@ -3848,6 +3871,7 @@ function TasksPageContent() {
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
const [editingCommentDraft, setEditingCommentDraft] = useState("");
const [taskCommentsLoading, setTaskCommentsLoading] = useState(false);
const [taskCommentsLoadedForId, setTaskCommentsLoadedForId] = useState<number | null>(null);
const [commentDraft, setCommentDraft] = useState("");
const [commentFiles, setCommentFiles] = useState<File[]>([]);
const [coverPickerOpen, setCoverPickerOpen] = useState(false);
@ -3891,6 +3915,9 @@ function TasksPageContent() {
const columnNodeRefs = useRef<Map<number, HTMLElement>>(new Map());
const boardSearchInputRef = useRef<HTMLInputElement | null>(null);
const taskAttachmentInputRef = useRef<HTMLInputElement | null>(null);
const taskAttachmentsSectionRef = useRef<HTMLDivElement | null>(null);
const taskCommentsSectionRef = useRef<HTMLDivElement | null>(null);
const taskCommentComposerRef = useRef<HTMLTextAreaElement | null>(null);
const taskCoverUploadInputRef = useRef<HTMLInputElement | null>(null);
const taskFormAttachmentInputRef = useRef<HTMLInputElement | null>(null);
const taskChecklistDraftRef = useRef<HTMLInputElement | null>(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 ? (
<div className="rounded-xl border border-[var(--border-subtle)]">
<div
ref={taskAttachmentsSectionRef}
className="rounded-xl border border-[var(--border-subtle)]"
>
<div className="flex items-center px-1 py-1">
<button
type="button"
@ -9741,7 +9807,10 @@ function TasksPageContent() {
) : null}
{commentsBadgeCount > 0 || commentsOpen ? (
<div className="rounded-xl border border-[var(--border-subtle)]">
<div
ref={taskCommentsSectionRef}
className="rounded-xl border border-[var(--border-subtle)]"
>
<button
type="button"
onClick={() => setCommentsOpen((current) => !current)}
@ -9894,6 +9963,7 @@ function TasksPageContent() {
)}
<CommentComposer
textareaRef={taskCommentComposerRef}
value={commentDraft}
onChange={setCommentDraft}
files={commentFiles}

View File

@ -1,7 +1,7 @@
"use client";
import { Paperclip, Send, Trash2, X } from "lucide-react";
import { useRef } from "react";
import { useRef, type Ref } from "react";
type CommentComposerProps = {
disabled?: boolean;
@ -14,6 +14,7 @@ type CommentComposerProps = {
placeholder?: string;
rows?: number;
submitDisabled?: boolean;
textareaRef?: Ref<HTMLTextAreaElement>;
value: string;
};
@ -28,6 +29,7 @@ export function CommentComposer({
placeholder = "Добавить комментарий",
rows = 2,
submitDisabled,
textareaRef,
value,
}: CommentComposerProps) {
const fileInputRef = useRef<HTMLInputElement | null>(null);
@ -67,6 +69,7 @@ export function CommentComposer({
/>
) : null}
<textarea
ref={textareaRef}
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}