after the fact handoff and clear search
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m9s

This commit is contained in:
2026-03-25 10:40:35 -05:00
parent e4f274034e
commit 9400a04fe2
3 changed files with 71 additions and 7 deletions
+16 -1
View File
@@ -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<TaskDetailProps> = (props) => {
</For>
</div>
</Show>
<Show when={canHandoffCollaborativelySharedTask(props.task)}>
<Button
variant="outline"
size="sm"
class="w-full h-8 text-[0.6875rem] justify-center"
onClick={async () => {
const didHandoff = await handoffCollaborativelySharedTask(props.task.id);
if (didHandoff) {
props.onClose();
}
}}
>
Handoff to collaborators
</Button>
</Show>
</div>
</div>
</div>
+11 -5
View File
@@ -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 = () => {
<Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/50 group-focus-within:text-primary transition-colors" />
<input
ref={(el) => {
searchInputRef = el;
if (el) setTimeout(() => el.focus(), 50);
}}
type="text"
@@ -138,12 +140,16 @@ export const TaskSearchPopover: Component = () => {
</Button>
<Button
variant="ghost"
size="icon"
class="h-10 w-10 rounded-xl shrink-0"
title="Close search"
onClick={closePanel}
size="sm"
class="h-10 rounded-xl shrink-0 px-3 text-xs font-bold uppercase tracking-wide"
title="Clear search"
disabled={!store.filter.query}
onClick={() => {
setFilter({ query: "" });
searchInputRef?.focus();
}}
>
<X size={16} />
Clear
</Button>
</div>
+44 -1
View File
@@ -1229,6 +1229,17 @@ export const isTaskCollaborativelyShared = (task: Task) =>
return ref.kind === "user" && policy === "collaborative" && !!targetUserId && targetUserId !== currentUserId;
});
export const canHandoffCollaborativelySharedTask = (task: Pick<Task, "shareRefs" | "ownerId">) => {
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<Task>): Partial<Task> =>
};
export const updateTask = async (id: string, updates: Partial<Task>) => {
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<Task>) => {
// 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;
}
};