Task search autocomplete for dependency linking

- searchTasks API function (q= param)
- Autocomplete dropdown: search by number or title
- Shows task key + title, click to select
- Replaces raw ID input
This commit is contained in:
Markov 2026-02-28 00:26:53 +01:00
parent 3df1060970
commit 60cc538b28
2 changed files with 43 additions and 6 deletions

View File

@ -7,6 +7,7 @@ import {
getSteps, createStep, updateStep, deleteStep as _deleteStepApi, getSteps, createStep, updateStep, deleteStep as _deleteStepApi,
getMessages, sendMessage, getMessages, sendMessage,
getTaskLinks, createTaskLink, deleteTaskLink, getTaskLinks, createTaskLink, deleteTaskLink,
searchTasks,
} from "@/lib/api"; } from "@/lib/api";
const STATUSES = [ const STATUSES = [
@ -51,6 +52,8 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
const [links, setLinks] = useState<TaskLink[]>([]); const [links, setLinks] = useState<TaskLink[]>([]);
const [showAddLink, setShowAddLink] = useState(false); const [showAddLink, setShowAddLink] = useState(false);
const [linkTargetId, setLinkTargetId] = useState(""); const [linkTargetId, setLinkTargetId] = useState("");
const [linkSearch, setLinkSearch] = useState("");
const [linkSearchResults, setLinkSearchResults] = useState<Task[]>([]);
const [linkType, setLinkType] = useState("depends_on"); const [linkType, setLinkType] = useState("depends_on");
const [comments, setComments] = useState<Message[]>([]); const [comments, setComments] = useState<Message[]>([]);
const [_saving, setSaving] = useState(false); const [_saving, setSaving] = useState(false);
@ -338,18 +341,48 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
<option value="blocks">Блокирует</option> <option value="blocks">Блокирует</option>
<option value="relates_to">Связано с</option> <option value="relates_to">Связано с</option>
</select> </select>
<input <div className="flex-1 relative">
value={linkTargetId} <input
onChange={e => setLinkTargetId(e.target.value)} value={linkSearch}
placeholder="ID задачи..." onChange={async (e) => {
className="flex-1 px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs outline-none" const v = e.target.value;
/> setLinkSearch(v);
if (v.length >= 1) {
const results = await searchTasks(task.project_id, v);
setLinkSearchResults(results.filter(t => t.id !== task.id));
} else {
setLinkSearchResults([]);
}
}}
placeholder="Поиск по номеру или названию..."
className="w-full px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs outline-none"
/>
{linkSearchResults.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-[var(--card)] border border-[var(--border)] rounded shadow-lg z-10 max-h-40 overflow-y-auto">
{linkSearchResults.map(t => (
<button
key={t.id}
onClick={() => {
setLinkTargetId(t.id);
setLinkSearch(`${t.key} ${t.title}`);
setLinkSearchResults([]);
}}
className="w-full text-left px-2 py-1.5 text-xs hover:bg-white/5 flex gap-2"
>
<span className="text-[var(--accent)] font-mono shrink-0">{t.key}</span>
<span className="truncate">{t.title}</span>
</button>
))}
</div>
)}
</div>
<button <button
onClick={async () => { onClick={async () => {
if (!linkTargetId) return; if (!linkTargetId) return;
const link = await createTaskLink(task.id, linkTargetId, linkType); const link = await createTaskLink(task.id, linkTargetId, linkType);
setLinks([...links, link]); setLinks([...links, link]);
setLinkTargetId(""); setLinkTargetId("");
setLinkSearch("");
setShowAddLink(false); setShowAddLink(false);
}} }}
className="px-2 py-1 bg-[var(--accent)] text-white rounded text-xs" className="px-2 py-1 bg-[var(--accent)] text-white rounded text-xs"

View File

@ -488,3 +488,7 @@ export async function addTaskLabel(taskId: string, labelId: string): Promise<voi
export async function removeTaskLabel(taskId: string, labelId: string): Promise<void> { export async function removeTaskLabel(taskId: string, labelId: string): Promise<void> {
return request(`/tasks/${taskId}/labels/${labelId}`, { method: "DELETE" }); return request(`/tasks/${taskId}/labels/${labelId}`, { method: "DELETE" });
} }
export async function searchTasks(projectId: string, query: string): Promise<Task[]> {
return request(`/tasks?project_id=${projectId}&q=${encodeURIComponent(query)}`);
}