"use client"; import { useEffect, useRef, useState } from "react"; import { CalendarDays, X } from "lucide-react"; import type { PlaitElement } from "@plait/core"; import { getTopicEntries, getTopicTitle } from "@/lib/flow-board/topic-details"; import { activeWorkingCalendar, getProjectCalendar, taskDueDate, workingDaysBetween, } from "@/lib/flow-board/project-calendar"; import { DAY, getTopicTask, newTopicTask, validateTopicTask, type TopicTask, type TaskDependency, } from "@/lib/flow-board/topic-tasks"; export function XmindTaskEditor({ topic, elements, onSave, onClose, onGantt, onCalendar, }: { topic: PlaitElement; elements: PlaitElement[]; onSave: (id: string, task: TopicTask | null) => void; onClose: () => void; onGantt: () => void; onCalendar: () => void; }) { const dialogRef = useRef(null); useEffect(() => { const previous = document.activeElement as HTMLElement | null; dialogRef.current?.querySelector("input")?.focus(); return () => { if (previous?.isConnected) previous.focus(); }; }, []); const calendar = activeWorkingCalendar(getProjectCalendar(elements)); const existing = getTopicTask(topic); const [draft, setDraft] = useState( () => existing || newTopicTask() ); const [error, setError] = useState(""); const [assigneesDraft, setAssigneesDraft] = useState( (existing?.assignees || []).join(", ") ); const [dependency, setDependency] = useState(""); const [dependencyType, setDependencyType] = useState("FS"); const [lag, setLag] = useState("0"); const source = JSON.stringify(existing); useEffect(() => { setDraft(getTopicTask(topic) || newTopicTask()); setAssigneesDraft((getTopicTask(topic)?.assignees || []).join(", ")); setError(""); }, [topic.id, source]); const tasks = getTopicEntries(elements).filter( (e) => e.element.id !== topic.id && getTopicTask(e.element) ); const patch = (p: Partial) => setDraft((t) => ({ ...t, ...p })); const withAssignees = () => ({ ...draft, assignees: [ ...new Set( assigneesDraft .split(/[,,]/) .map((s) => s.trim()) .filter(Boolean) ), ], }); const save = () => { const task = withAssignees(); const problem = validateTopicTask(topic.id, task, elements); if (problem) { setError(problem); return; } onSave(topic.id, task); onClose(); }; return (
e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") { e.stopPropagation(); onClose(); } if (e.key === "Tab") { const controls = Array.from( dialogRef.current?.querySelectorAll( "button:not(:disabled),input,select" ) || [] ); const first = controls[0], last = controls.at(-1); if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus(); } if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus(); } } }} >

任务信息

{getTopicTitle(topic)}

前置任务

{draft.dependencies.map((dep, i) => (
{tasks.find((e) => e.element.id === dep.id)?.title || "已删除的任务"}{" "} · {dep.type} {dep.lag ? ` · ${dep.lag / DAY} 天` : ""}
))}
setLag(e.target.value)} title="间隔天数(负数为提前)" />
{error ? (

{error}

) : null}
{calendar ? `已跳过非工作日 · ${calendar.name}` : "包含非工作日"}
); }