Files
kandev/apps/web/components/task/simple/components/parent-picker.tsx
T
2026-05-16 20:07:57 +01:00

101 lines
3.2 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { Combobox, type ComboboxOption } from "@/components/combobox";
import { useAppStore } from "@/components/state-provider";
import { searchTasks, updateTask } from "@/lib/api/domains/office-extended-api";
import { useOptimisticTaskMutation } from "@/hooks/use-optimistic-task-mutation";
import type { OfficeTask } from "@/lib/state/slices/office/types";
import type { Task } from "@/app/office/tasks/[id]/types";
type ParentPickerProps = {
task: Task;
};
const NO_PARENT = "__none__";
function buildOptions(candidates: OfficeTask[], currentTaskId: string): ComboboxOption[] {
const noOpt: ComboboxOption = {
value: NO_PARENT,
label: "No parent",
keywords: ["none"],
renderLabel: () => <span className="text-muted-foreground">No parent</span>,
};
const taskOpts = candidates
.filter((t) => t.id !== currentTaskId)
.map<ComboboxOption>((t) => ({
value: t.id,
label: `${t.identifier} ${t.title}`,
keywords: [t.identifier, t.title],
renderLabel: () => (
<span className="flex items-center gap-2 min-w-0">
<span className="font-mono text-xs text-muted-foreground shrink-0">{t.identifier}</span>
<span className="truncate">{t.title}</span>
</span>
),
}));
return [noOpt, ...taskOpts];
}
export function ParentPicker({ task }: ParentPickerProps) {
const storeTasks = useAppStore((s) => s.office.tasks.items);
const workspaceId = useAppStore((s) => s.workspaces.activeId);
const [fetched, setFetched] = useState<OfficeTask[]>([]);
const mutate = useOptimisticTaskMutation();
// If the store doesn't already have tasks for the workspace, lazily fetch.
useEffect(() => {
if (!workspaceId || storeTasks.length > 0) return;
let cancelled = false;
searchTasks(workspaceId, "", 50)
.then((res) => {
if (!cancelled) setFetched(res.tasks ?? []);
})
.catch(() => {
if (!cancelled) setFetched([]);
});
return () => {
cancelled = true;
};
}, [workspaceId, storeTasks.length]);
const candidates = storeTasks.length > 0 ? storeTasks : fetched;
const options = useMemo(() => buildOptions(candidates, task.id), [candidates, task.id]);
const currentValue = task.parentId || NO_PARENT;
const handleSelect = async (next: string) => {
const sendValue = next === NO_PARENT || next === "" ? "" : next;
if (sendValue === (task.parentId ?? "")) return;
const matched = candidates.find((t) => t.id === sendValue);
try {
await mutate(
task.id,
{
parentId: sendValue || undefined,
parentTitle: matched?.title,
parentIdentifier: matched?.identifier,
},
() => updateTask(task.id, { parent_id: sendValue }),
);
} catch {
/* toast already raised */
}
};
return (
<Combobox
options={options}
value={currentValue}
onValueChange={handleSelect}
placeholder="No parent"
searchPlaceholder="Search tasks..."
emptyMessage="No tasks found."
triggerClassName="h-7 w-full justify-end px-2"
popoverAlign="end"
testId="parent-picker-trigger"
/>
);
}