-
+ {quickStatusAction || canReject ? (
+
+
{t("issues.detailPage.quickActions")}
+
+ {quickStatusAction ? (
+
+ ) : null}
+ {canReject ? (
+
+ ) : null}
+
) : null}
);
}
+function quickStatusActionFor(status: IssueStatus): {
+ status: IssueStatus;
+ labelKey: "statuses.ready" | "issues.board.draft" | "statuses.done";
+} | null {
+ if (status === "backlog") return { status: "ready", labelKey: "statuses.ready" };
+ if (status === "ready") return { status: "backlog", labelKey: "issues.board.draft" };
+ if (status === "review") return { status: "done", labelKey: "statuses.done" };
+ return null;
+}
+
function dependencyIssueLinks(
dependencyIDs: number[],
issueOptions: IssueSummary[],
diff --git a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.stories.tsx b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.stories.tsx
index 75d478e0..c57d2485 100644
--- a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.stories.tsx
+++ b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.stories.tsx
@@ -22,6 +22,6 @@ export const WithContinueAction: Story = {
...storyComments[0],
type: "blocker",
},
- onContinueWithComment: () => undefined,
+ onContinueWithComment: async () => undefined,
},
};
diff --git a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.test.tsx b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.test.tsx
index ff95538f..47d1437a 100644
--- a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.test.tsx
+++ b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, within } from "@testing-library/react";
+import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import "@/lib/i18n";
@@ -25,11 +25,43 @@ describe("CommentCard", () => {
await user.click(screen.getByRole("button", { name: "Comment actions for codex" }));
const menu = screen.getByRole("menu", { name: "Comment actions for codex" });
- const action = within(menu).getByRole("menuitem", { name: "Continue with comment" });
+ const action = within(menu).getByRole("menuitem", { name: "Write a comment…" });
expect(action.querySelector(".lucide-arrow-right")).toBeInTheDocument();
await user.click(action);
expect(onContinueWithComment).toHaveBeenCalledOnce();
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
+
+ it("selects the Ok shortcut with its separate label and body", async () => {
+ const user = userEvent.setup();
+ const onContinueWithComment = vi.fn();
+
+ render(
);
+
+ await user.click(screen.getByRole("button", { name: "Comment actions for codex" }));
+ expect(screen.getByRole("menuitem", { name: "Retry" })).toBeVisible();
+ await user.click(screen.getByRole("menuitem", { name: "Ok" }));
+
+ expect(onContinueWithComment).toHaveBeenCalledWith({ id: "ok", label: "Ok", body: "Ok" });
+ });
+
+ it("prevents another shortcut selection while submitting", async () => {
+ const user = userEvent.setup();
+ let resolveSubmission: () => void = () => undefined;
+ const onContinueWithComment = vi.fn(
+ () => new Promise
((resolve) => { resolveSubmission = resolve; }),
+ );
+
+ render();
+
+ const trigger = screen.getByRole("button", { name: "Comment actions for codex" });
+ await user.click(trigger);
+ await user.click(screen.getByRole("menuitem", { name: "Ok" }));
+
+ expect(trigger).toBeDisabled();
+ expect(onContinueWithComment).toHaveBeenCalledTimes(1);
+ resolveSubmission();
+ await waitFor(() => expect(trigger).toBeEnabled());
+ });
});
diff --git a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.tsx b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.tsx
index 33f5f65d..7d094983 100644
--- a/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.tsx
+++ b/cmd/web/frontend/src/features/issues/components/issue-detail-page/comment-card/index.tsx
@@ -1,9 +1,10 @@
import { useId, useState } from "react";
import { useTranslation } from "react-i18next";
-import { ContextMenu, ContextMenuItem } from "@/components/ui/context-menu";
+import { ContextMenu, ContextMenuGroupLabel, ContextMenuItem } from "@/components/ui/context-menu";
import { IconProxy } from "@/components/ui/icon-proxy";
import { Markdown } from "@/components/ui/markdown";
import { CommentTypeBadge } from "@/features/issues/components/comment-type-badge";
+import { builtInChangeRequestShortcuts, type ChangeRequestShortcut } from "@/features/issues/change-request-shortcuts";
import type { Comment } from "@/lib/types";
import { formatDateTime } from "../format";
import styles from "./index.module.css";
@@ -11,15 +12,28 @@ import styles from "./index.module.css";
export function CommentCard({
comment,
onContinueWithComment,
+ shortcuts = builtInChangeRequestShortcuts.continue,
}: {
comment: Comment;
- onContinueWithComment?: () => void;
+ onContinueWithComment?: (shortcut?: ChangeRequestShortcut) => Promise;
+ shortcuts?: readonly ChangeRequestShortcut[];
}) {
const { t } = useTranslation();
const menuID = useId();
const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const menuLabel = t("issues.detailPage.commentActions", { author: comment.author });
+ async function handleShortcut(shortcut: ChangeRequestShortcut) {
+ setIsMenuOpen(false);
+ setIsSubmitting(true);
+ try {
+ await onContinueWithComment?.(shortcut);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
return (
@@ -40,6 +54,7 @@ export function CommentCard({
{...triggerProps}
aria-label={menuLabel}
className={styles.menuButton}
+ disabled={isSubmitting}
title={menuLabel}
type="button"
>
@@ -47,15 +62,26 @@ export function CommentCard({
)}
>
+
{t("issues.continueWithComment.action")}
}
onSelect={() => {
setIsMenuOpen(false);
- onContinueWithComment();
+ void onContinueWithComment();
}}
>
- {t("issues.continueWithComment.action")}
+ {t("issues.changeRequest.writeComment")}
+ {shortcuts.map((shortcut) => (
+
void handleShortcut(shortcut)}
+ >
+ {shortcut.label}
+
+ ))}
) : (
@@ -483,25 +535,27 @@ export function IssueDetailPage() {
) : null}
>
) : null}
- {issueState.kind === "ready" && changeRequestDialogVariant ? (
+ {issueState.kind === "ready" && changeRequestDialog ? (
{
setChangeRequestError("");
- setChangeRequestDialogVariant(null);
+ setChangeRequestDialog(null);
}}
- onMoveIssueReady={handleMoveIssueReady}
+ onMoveIssueReady={() => handleMoveIssueReady(changeRequestDialog.variant)}
onSuccess={() => {
setChangeRequestError("");
- setChangeRequestDialogVariant(null);
- if (changeRequestDialogVariant === "continue") {
+ setChangeRequestDialog(null);
+ if (changeRequestDialog.variant === "continue") {
void loadChangeRequests();
}
}}
- variant={changeRequestDialogVariant}
+ variant={changeRequestDialog.variant}
/>
) : null}
diff --git a/cmd/web/frontend/src/features/issues/components/issues-view/index.tsx b/cmd/web/frontend/src/features/issues/components/issues-view/index.tsx
index c32b9fc5..5641217f 100644
--- a/cmd/web/frontend/src/features/issues/components/issues-view/index.tsx
+++ b/cmd/web/frontend/src/features/issues/components/issues-view/index.tsx
@@ -2,7 +2,7 @@
import type { IssueStatus, Summary } from "@/lib/types";
import { IssueBoard } from "@/features/issues/components/board";
-import type { RejectIssueHandler, StatusChangeHandler } from "./types";
+import type { RejectIssueHandler, RejectShortcutHandler, StatusChangeHandler } from "./types";
import styles from "./index.module.css";
export function IssuesView({
@@ -10,12 +10,14 @@ export function IssuesView({
summary,
onAddIssue,
onRejectIssue,
+ onRejectShortcut,
onStatusChange,
}: {
showFilterSortActions?: boolean;
summary: Summary;
onAddIssue: (status?: IssueStatus) => void;
onRejectIssue?: RejectIssueHandler;
+ onRejectShortcut?: RejectShortcutHandler;
onStatusChange: StatusChangeHandler;
}) {
return (
@@ -25,6 +27,7 @@ export function IssuesView({
summary={summary}
onAddIssue={onAddIssue}
onRejectIssue={onRejectIssue}
+ onRejectShortcut={onRejectShortcut}
onStatusChange={onStatusChange}
/>