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.
This commit is contained in:
Jiayuan Zhang
2026-04-29 16:07:34 +02:00
committed by GitHub
parent b9118ae9b8
commit b98c2a5a0f
3 changed files with 60 additions and 1 deletions

View File

@@ -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({
</span>
</div>
<div className="flex shrink-0 items-center gap-1">
{onDone && (
<span
role="button"
tabIndex={-1}
title="Mark as done"
onClick={(e) => {
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"
>
<CircleCheck className="h-3.5 w-3.5" />
</span>
)}
<span
role="button"
tabIndex={-1}

View File

@@ -20,6 +20,7 @@ import {
useArchiveAllReadInbox,
useArchiveCompletedInbox,
} from "@multica/core/inbox/mutations";
import { useUpdateIssue } from "@multica/core/issues/mutations";
import { IssueDetail } from "../../issues/components";
import { useNavigation } from "../../navigation";
import { toast } from "sonner";
@@ -117,6 +118,7 @@ export function InboxPage() {
const archiveAllMutation = useArchiveAllInbox();
const archiveAllReadMutation = useArchiveAllReadInbox();
const archiveCompletedMutation = useArchiveCompletedInbox();
const updateIssueMutation = useUpdateIssue();
// Auto-mark-read whenever a selected item is unread — covers both click-
// to-select and URL-param-select (e.g. OS notification click on desktop).
@@ -145,6 +147,18 @@ export function InboxPage() {
});
};
const handleDone = (item: InboxItem) => {
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
}
/>
))}
</div>

View File

@@ -9,6 +9,7 @@ import {
ChevronDown,
ChevronLeft,
ChevronRight,
CircleCheck,
MoreHorizontal,
PanelRight,
Pin,
@@ -511,6 +512,23 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
{issue.status !== "done" && issue.status !== "cancelled" && (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => handleUpdateField({ status: "done" })}
>
<CircleCheck />
</Button>
}
/>
<TooltipContent side="bottom">Mark as done</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger
render={