mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
feat(web): promote Approve to dedicated buttons
Pending approval is the only action a user can take to unblock a run, so the Approve action shouldn't be buried in the Actions dropdown. - Run detail header: render a primary teal "Approve" button beside the Actions menu when approval is pending; remove the duplicate menu item. - Board view (/runs): surface `pendingApproval` on RunItem and render an inline Approve button on cards in the Pending column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47d8d6365b
commit
7cc38e2f8d
3 changed files with 66 additions and 19 deletions
|
|
@ -33,6 +33,7 @@ export interface RunItem {
|
|||
actionDisabled?: boolean;
|
||||
comments?: number;
|
||||
question?: string;
|
||||
pendingApproval?: boolean;
|
||||
sandboxId?: string;
|
||||
sandboxWorkingDirectory?: string;
|
||||
sourceDirectory?: string;
|
||||
|
|
@ -98,6 +99,9 @@ export function mapRunListItem(item: Run): RunItem {
|
|||
elapsed: item.timing != null ? formatDurationMs(item.timing.wall_time_ms) : undefined,
|
||||
resources: undefined,
|
||||
question: item.current_question?.text,
|
||||
pendingApproval:
|
||||
item.lifecycle.status.kind === "pending"
|
||||
&& item.lifecycle.approval?.state === "pending",
|
||||
sandboxId: runtime?.id ?? undefined,
|
||||
sandboxWorkingDirectory: runtime?.working_directory ?? undefined,
|
||||
sourceDirectory: item.source_directory ?? undefined,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "react";
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
|
|
@ -44,6 +45,7 @@ import {
|
|||
PopoverHeader,
|
||||
PopoverRow,
|
||||
PopoverRows,
|
||||
PRIMARY_BUTTON_CLASS,
|
||||
SECONDARY_BUTTON_CLASS,
|
||||
Tooltip,
|
||||
} from "../components/ui";
|
||||
|
|
@ -701,6 +703,22 @@ export default function RunDetail({ params }: { params: { id: string } }) {
|
|||
</HoverCard>
|
||||
)}
|
||||
|
||||
{approvalActionVisible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void approveMutation.trigger()}
|
||||
disabled={approvePending}
|
||||
className={PRIMARY_BUTTON_CLASS}
|
||||
>
|
||||
{approvePending ? (
|
||||
<ArrowPathIcon className="size-4 animate-spin" aria-hidden="true" />
|
||||
) : (
|
||||
<CheckIcon className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
{approvePending ? "Approving…" : "Approve"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ActionsMenu
|
||||
runId={params.id}
|
||||
canSendInterrupt={statusKind === "running"}
|
||||
|
|
@ -719,9 +737,7 @@ export default function RunDetail({ params }: { params: { id: string } }) {
|
|||
canArchive={visibility.showArchive}
|
||||
archivePending={archivePending}
|
||||
onArchive={() => void archiveMutation.trigger()}
|
||||
canApprove={approvalActionVisible}
|
||||
approvePending={approvePending}
|
||||
onApprove={() => void approveMutation.trigger()}
|
||||
canDeny={approvalActionVisible}
|
||||
denyPending={denyPending}
|
||||
onDeny={() => void denyMutation.trigger()}
|
||||
|
|
@ -961,9 +977,7 @@ interface ActionsMenuProps {
|
|||
canArchive: boolean;
|
||||
archivePending: boolean;
|
||||
onArchive: () => void;
|
||||
canApprove: boolean;
|
||||
approvePending: boolean;
|
||||
onApprove: () => void;
|
||||
canDeny: boolean;
|
||||
denyPending: boolean;
|
||||
onDeny: () => void;
|
||||
|
|
@ -988,7 +1002,7 @@ function ActionsMenu(props: ActionsMenuProps) {
|
|||
canFocusSteer, onFocusSteer,
|
||||
canPreview, previewPending, onPreview,
|
||||
canArchive, archivePending, onArchive,
|
||||
canApprove, approvePending, onApprove,
|
||||
approvePending,
|
||||
canDeny, denyPending, onDeny,
|
||||
canRetry, retryPending, onRetry,
|
||||
canUnarchive, unarchivePending, onUnarchive,
|
||||
|
|
@ -998,7 +1012,7 @@ function ActionsMenu(props: ActionsMenuProps) {
|
|||
|
||||
const [runIdCopied, setRunIdCopied] = useState(false);
|
||||
|
||||
const hasLifecycle = canApprove || canRetry || canArchive || canUnarchive;
|
||||
const hasLifecycle = canRetry || canArchive || canUnarchive;
|
||||
const hasDestructive = canDeny || canCancel || canDelete;
|
||||
const anyPending =
|
||||
previewPending ||
|
||||
|
|
@ -1083,18 +1097,6 @@ function ActionsMenu(props: ActionsMenuProps) {
|
|||
{separators.afterOperations && (
|
||||
<div className="my-1 h-px bg-line" role="separator" />
|
||||
)}
|
||||
{canApprove && (
|
||||
<MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
disabled={approvePending}
|
||||
className={MENU_ITEM_CLASS}
|
||||
>
|
||||
{approvePending ? "Approving…" : "Approve"}
|
||||
</button>
|
||||
</MenuItem>
|
||||
)}
|
||||
{canRetry && (
|
||||
<MenuItem>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { ConfirmDialog } from "../components/ui";
|
|||
import { mutateRunListCaches } from "../lib/board-cache";
|
||||
import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events";
|
||||
import { useAllRuns, useAuthConfig, useRunsPage, useSystemInfo } from "../lib/queries";
|
||||
import { archiveRuns, canArchive, canDelete, canUnarchive, deleteRuns, unarchiveRuns } from "../lib/run-actions";
|
||||
import { approveRun, archiveRuns, canArchive, canDelete, canUnarchive, deleteRuns, mapError, unarchiveRuns } from "../lib/run-actions";
|
||||
import type {
|
||||
BatchDeleteRunsResponse,
|
||||
BatchRunLifecycleResponse,
|
||||
|
|
@ -437,10 +437,51 @@ function PrCard({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pr.pendingApproval && (
|
||||
<div className="mt-3 flex items-center gap-1.5">
|
||||
<ApproveBoardButton runId={pr.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApproveBoardButton({ runId }: { runId: string }) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { push } = useToast();
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
async function handleClick(event: React.MouseEvent) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (pending) return;
|
||||
setPending(true);
|
||||
try {
|
||||
await approveRun(runId);
|
||||
mutateRunListCaches(mutate);
|
||||
push({ message: "Run approved." });
|
||||
} catch (error) {
|
||||
push({ message: mapError(error, "approve"), tone: "error" });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
disabled={pending}
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-teal-500 px-2.5 py-1 text-[11px] font-medium text-on-primary transition-colors hover:bg-teal-300 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-teal-500"
|
||||
>
|
||||
<CheckIcon className="size-3" aria-hidden="true" />
|
||||
{pending ? "Approving…" : "Approve"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SortablePrCard({
|
||||
pr,
|
||||
iconColor,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue