Merge branch 'develop'
This commit is contained in:
commit
1a22321a47
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user