From b98c2a5a0fd088474d72ec12a2718ff28bcb6063 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 29 Apr 2026 16:07:34 +0200 Subject: [PATCH] feat(inbox): add one-click Done button to inbox items (#1885) * feat(inbox): add one-click Done button to inbox items Add a hover-visible "Mark as done" button (CircleCheck icon) to each inbox item that has an associated issue not yet in done/cancelled status. Clicking it sets the issue status to "done" and archives the inbox item in one action, replacing the previous multi-step flow of opening the issue detail sidebar to change status. * feat(issues): add Mark Done button to issue detail toolbar Add a "Mark as done" button (CircleCheck icon) to the issue detail header toolbar, positioned to the left of the Pin button. The button is only visible when the issue status is not already done or cancelled. Clicking it sets the issue status to "done" via the existing handleUpdateField action. --- .../inbox/components/inbox-list-item.tsx | 24 ++++++++++++++++++- .../views/inbox/components/inbox-page.tsx | 19 +++++++++++++++ .../views/issues/components/issue-detail.tsx | 18 ++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/views/inbox/components/inbox-list-item.tsx b/packages/views/inbox/components/inbox-list-item.tsx index 06e9b4154..0bc7e4c80 100644 --- a/packages/views/inbox/components/inbox-list-item.tsx +++ b/packages/views/inbox/components/inbox-list-item.tsx @@ -2,7 +2,7 @@ import { StatusIcon } from "../../issues/components"; import { ActorAvatar } from "../../common/actor-avatar"; -import { Archive } from "lucide-react"; +import { Archive, CircleCheck } from "lucide-react"; import type { InboxItem } from "@multica/core/types"; import { InboxDetailLabel } from "./inbox-detail-label"; import { getInboxDisplayTitle } from "./inbox-display"; @@ -25,11 +25,13 @@ export function InboxListItem({ isSelected, onClick, onArchive, + onDone, }: { item: InboxItem; isSelected: boolean; onClick: () => void; onArchive: () => void; + onDone?: () => void; }) { const displayTitle = getInboxDisplayTitle(item); @@ -59,6 +61,26 @@ export function InboxListItem({
+ {onDone && ( + { + e.stopPropagation(); + onDone(); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + onDone(); + } + }} + className="hidden rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-info group-hover:inline-flex" + > + + + )} { + if (!item.issue_id) return; + setSelectedKey(""); + updateIssueMutation.mutate( + { id: item.issue_id, status: "done" }, + { onError: () => toast.error("Failed to mark as done") }, + ); + archiveMutation.mutate(item.id, { + onError: () => toast.error("Failed to archive"), + }); + }; + // Batch operations const handleMarkAllRead = () => { markAllReadMutation.mutate(undefined, { @@ -235,6 +249,11 @@ export function InboxPage() { isSelected={(item.issue_id ?? item.id) === selectedKey} onClick={() => handleSelect(item)} onArchive={() => handleArchive(item.id)} + onDone={ + item.issue_id && item.issue_status !== "done" && item.issue_status !== "cancelled" + ? () => handleDone(item) + : undefined + } /> ))}
diff --git a/packages/views/issues/components/issue-detail.tsx b/packages/views/issues/components/issue-detail.tsx index f0b1c393a..2b35c6239 100644 --- a/packages/views/issues/components/issue-detail.tsx +++ b/packages/views/issues/components/issue-detail.tsx @@ -9,6 +9,7 @@ import { ChevronDown, ChevronLeft, ChevronRight, + CircleCheck, MoreHorizontal, PanelRight, Pin, @@ -511,6 +512,23 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
+ {issue.status !== "done" && issue.status !== "cancelled" && ( + + handleUpdateField({ status: "done" })} + > + + + } + /> + Mark as done + + )}