after the fact handoff and clear search
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid";
|
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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const TaskSearchPopover: Component = () => {
|
|||||||
const [isSaving, setIsSaving] = createSignal(false);
|
const [isSaving, setIsSaving] = createSignal(false);
|
||||||
let triggerRef: HTMLDivElement | undefined;
|
let triggerRef: HTMLDivElement | undefined;
|
||||||
let panelRef: HTMLDivElement | undefined;
|
let panelRef: HTMLDivElement | undefined;
|
||||||
|
let searchInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
const hasActiveFilters = () => {
|
const hasActiveFilters = () => {
|
||||||
const f = store.filter;
|
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" />
|
<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
|
<input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
|
searchInputRef = el;
|
||||||
if (el) setTimeout(() => el.focus(), 50);
|
if (el) setTimeout(() => el.focus(), 50);
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -138,12 +140,16 @@ export const TaskSearchPopover: Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
class="h-10 w-10 rounded-xl shrink-0"
|
class="h-10 rounded-xl shrink-0 px-3 text-xs font-bold uppercase tracking-wide"
|
||||||
title="Close search"
|
title="Clear search"
|
||||||
onClick={closePanel}
|
disabled={!store.filter.query}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter({ query: "" });
|
||||||
|
searchInputRef?.focus();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+44
-1
@@ -1229,6 +1229,17 @@ export const isTaskCollaborativelyShared = (task: Task) =>
|
|||||||
return ref.kind === "user" && policy === "collaborative" && !!targetUserId && targetUserId !== currentUserId;
|
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) =>
|
export const isTaskCollapsedUntilUpdated = (task: Task) =>
|
||||||
store.collapsedUntilUpdatedTasks[task.id] === task.updated;
|
store.collapsedUntilUpdatedTasks[task.id] === task.updated;
|
||||||
|
|
||||||
@@ -1242,6 +1253,36 @@ export const expandCollapsedTask = (taskId: string) => {
|
|||||||
void syncPreferences();
|
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)
|
// Auto-persist changes to localStorage (wrapped in root to avoid console warning)
|
||||||
createRoot(() => {
|
createRoot(() => {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -2929,7 +2970,7 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateTask = async (id: string, updates: 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)
|
// 0. Check for Hand-off Logic (Tag-based ownership transfer)
|
||||||
const currentTask = store.tasks.find(t => t.id === id);
|
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
|
// Disable auto-cancellation for updates to prevent aborted errors during rapid typing
|
||||||
await pb.collection(TASGRID_COLLECTION).update(id, pbUpdates, { requestKey: null });
|
await pb.collection(TASGRID_COLLECTION).update(id, pbUpdates, { requestKey: null });
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("update failed", err);
|
console.error("update failed", err);
|
||||||
toast.error("Failed to update task.");
|
toast.error("Failed to update task.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user