diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index f371ca1..4ed90d6 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -1,6 +1,6 @@ import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js"; import { Sheet, SheetContent } from "@/components/ui/sheet"; -import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag, getVisibleTaskTags, isTaskLockedNoteTag, deriveTaskRelationsFromTags, getTaskShareModeForTag, getTaskSharePolicy } from "@/store"; +import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag, getVisibleTaskTags, isTaskLockedNoteTag, deriveTaskRelationsFromTags, getTaskShareModeForTag, getTaskSharePolicy, canHandoffCollaborativelySharedTask, handoffCollaborativelySharedTask } from "@/store"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid"; @@ -732,6 +732,21 @@ export const TaskDetail: Component = (props) => { + + + diff --git a/src/components/TaskSearchPopover.tsx b/src/components/TaskSearchPopover.tsx index a4fd041..a78cb8d 100644 --- a/src/components/TaskSearchPopover.tsx +++ b/src/components/TaskSearchPopover.tsx @@ -20,6 +20,7 @@ export const TaskSearchPopover: Component = () => { const [isSaving, setIsSaving] = createSignal(false); let triggerRef: HTMLDivElement | undefined; let panelRef: HTMLDivElement | undefined; + let searchInputRef: HTMLInputElement | undefined; const hasActiveFilters = () => { const f = store.filter; @@ -109,6 +110,7 @@ export const TaskSearchPopover: Component = () => { { + searchInputRef = el; if (el) setTimeout(() => el.focus(), 50); }} type="text" @@ -138,12 +140,16 @@ export const TaskSearchPopover: Component = () => { diff --git a/src/store/index.ts b/src/store/index.ts index 557936d..bcc302b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1229,6 +1229,17 @@ export const isTaskCollaborativelyShared = (task: Task) => return ref.kind === "user" && policy === "collaborative" && !!targetUserId && targetUserId !== currentUserId; }); +export const canHandoffCollaborativelySharedTask = (task: Pick) => { + const currentUserId = pb.authStore.model?.id; + if (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId)) return false; + + return task.shareRefs.some(ref => { + if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false; + const targetUserId = getPersonalContextUserIdFromRef(ref); + return !!targetUserId && targetUserId !== currentUserId; + }); +}; + export const isTaskCollapsedUntilUpdated = (task: Task) => store.collapsedUntilUpdatedTasks[task.id] === task.updated; @@ -1242,6 +1253,36 @@ export const expandCollapsedTask = (taskId: string) => { void syncPreferences(); }; +export const handoffCollaborativelySharedTask = async (taskId: string) => { + const task = store.tasks.find(existing => existing.id === taskId); + const currentUserId = pb.authStore.model?.id; + const senderPersonalContext = currentUserId ? getPersonalContext(currentUserId) : null; + + if (!task || !currentUserId || !senderPersonalContext) return false; + if (!canHandoffCollaborativelySharedTask(task)) { + toast.info("This task needs another collaborative @user before you can hand it off."); + return false; + } + + const nextShareRefs = task.shareRefs.filter(ref => !( + ref.kind === "user" && + (ref.contextId === senderPersonalContext.id || ref.key === senderPersonalContext.key) + )); + + const didUpdate = await updateTask(taskId, { + shareRefs: nextShareRefs, + labelTags: task.labelTags, + noteRefs: task.noteRefs, + tags: buildTaskTags(task.labelTags, nextShareRefs, task.noteRefs, getFavoriteTag()) + }); + + if (didUpdate) { + toast.success("Task handed off successfully."); + } + + return didUpdate; +}; + // Auto-persist changes to localStorage (wrapped in root to avoid console warning) createRoot(() => { createEffect(() => { @@ -2929,7 +2970,7 @@ const checkForHandoffs = (task: Task, updates: Partial): Partial => }; export const updateTask = async (id: string, updates: Partial) => { - if (!pb.authStore.isValid) return; + if (!pb.authStore.isValid) return false; // 0. Check for Hand-off Logic (Tag-based ownership transfer) const currentTask = store.tasks.find(t => t.id === id); @@ -3025,9 +3066,11 @@ export const updateTask = async (id: string, updates: Partial) => { // Disable auto-cancellation for updates to prevent aborted errors during rapid typing await pb.collection(TASGRID_COLLECTION).update(id, pbUpdates, { requestKey: null }); + return true; } catch (err) { console.error("update failed", err); toast.error("Failed to update task."); + return false; } };