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:
parent
3df1060970
commit
60cc538b28
@ -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>
|
||||||
|
<div className="flex-1 relative">
|
||||||
<input
|
<input
|
||||||
value={linkTargetId}
|
value={linkSearch}
|
||||||
onChange={e => setLinkTargetId(e.target.value)}
|
onChange={async (e) => {
|
||||||
placeholder="ID задачи..."
|
const v = e.target.value;
|
||||||
className="flex-1 px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs outline-none"
|
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"
|
||||||
|
|||||||
@ -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)}`);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user