Tighten issue service quick controls

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-16 10:02:04 +00:00
1 parent 4de9bc3451
commit 3bf8cfd127
4 files changed
+39 -13

No files matched your search

+7 -3
View File
@@ -772,13 +772,17 @@ describe("IssueProperties", () => {
});
await flush();
expect(container.querySelector(`a[href="${serviceUrl}"]`)).not.toBeNull();
const serviceLink = container.querySelector(`a[href="${serviceUrl}"]`);
expect(serviceLink).not.toBeNull();
expect(serviceLink?.className).toContain("sm:self-start");
expect(serviceLink?.className).not.toContain("sm:self-end");
expect((container.textContent ?? "").indexOf("Workspace")).toBeLessThan(
(container.textContent ?? "").indexOf("Service"),
);
const stopButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.trim() === "Stop");
const stopButton = container.querySelector<HTMLButtonElement>('button[aria-label="Stop"]');
expect(stopButton).not.toBeUndefined();
expect(stopButton?.getAttribute("data-size")).toBe("icon-xs");
expect(stopButton?.getAttribute("data-variant")).toBe("outline");
await act(async () => {
stopButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+2
View File
@@ -2220,6 +2220,8 @@ export function IssueProperties({
pendingRequest={pendingWorkspaceRuntimeAction}
onAction={(request) => controlWorkspaceRuntime.mutate(request)}
square
align="start"
iconOnly
/>
{runtimeActionMessage ? (
<span className="text-xs text-muted-foreground" role="status">{runtimeActionMessage}</span>
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { act } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import type { WorkspaceRuntimeService } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -14,6 +14,10 @@ import {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function act(callback: () => void) {
flushSync(callback);
}
function createRuntimeService(overrides: Partial<WorkspaceRuntimeService> = {}): WorkspaceRuntimeService {
return {
id: overrides.id ?? "service-1",
+25 -9
View File
@@ -226,12 +226,14 @@ function CommandActionButtons({
pendingRequest,
onAction,
square,
iconOnly,
}: {
item: WorkspaceRuntimeControlItem;
isPending: boolean;
pendingRequest: WorkspaceRuntimeControlRequest | null | undefined;
onAction: (request: WorkspaceRuntimeControlRequest) => void;
square?: boolean;
iconOnly?: boolean;
}) {
const actions: WorkspaceRuntimeAction[] =
item.kind === "job"
@@ -241,7 +243,7 @@ function CommandActionButtons({
: ["start"];
return (
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
<div className={cn("flex gap-2", iconOnly ? "w-auto flex-row flex-wrap" : "w-full flex-col sm:w-auto sm:flex-row sm:flex-wrap")}>
{actions.map((action) => {
const request = buildRequest(item, action);
const Icon = action === "stop" ? Square : action === "restart" ? RotateCcw : Play;
@@ -261,17 +263,19 @@ function CommandActionButtons({
return (
<Button
key={`${item.key}:${action}`}
variant={action === "stop" ? "destructive" : action === "restart" ? "outline" : "default"}
size="sm"
variant={iconOnly ? "outline" : action === "stop" ? "destructive" : action === "restart" ? "outline" : "default"}
size={iconOnly ? "icon-xs" : "sm"}
className={cn(
"w-full justify-start sm:w-auto",
iconOnly ? "shrink-0" : "w-full justify-start sm:w-auto",
square ? "rounded-none" : null,
)}
disabled={disabled}
onClick={() => onAction(request)}
aria-label={label}
title={label}
>
{showSpinner ? <Loader2 className="h-4 w-4 animate-spin" /> : <Icon className="h-4 w-4" />}
{label}
{iconOnly ? <span className="sr-only">{label}</span> : label}
</Button>
);
})}
@@ -289,6 +293,7 @@ function CommandSection({
pendingRequest,
onAction,
square,
iconOnly,
}: {
title: string;
description: string;
@@ -299,6 +304,7 @@ function CommandSection({
pendingRequest: WorkspaceRuntimeControlRequest | null | undefined;
onAction: (request: WorkspaceRuntimeControlRequest) => void;
square?: boolean;
iconOnly?: boolean;
}) {
return (
<div className="space-y-3">
@@ -330,6 +336,7 @@ function CommandSection({
pendingRequest={pendingRequest}
onAction={onAction}
square={square}
iconOnly={iconOnly}
/>
</div>
<div className="space-y-1 text-xs text-muted-foreground">
@@ -466,24 +473,29 @@ export function WorkspaceRuntimeQuickControls({
pendingRequest = null,
onAction,
square,
align = "end",
iconOnly = false,
}: {
sections: WorkspaceRuntimeControlSections;
isPending?: boolean;
pendingRequest?: WorkspaceRuntimeControlRequest | null;
onAction: (request: WorkspaceRuntimeControlRequest) => void;
square?: boolean;
align?: "start" | "end";
iconOnly?: boolean;
}) {
const controlItems = sections.services.length > 0 ? sections.services : sections.otherServices;
const serviceUrl = getRunningRuntimeServiceUrl(sections);
const alignEnd = align === "end";
if (controlItems.length === 0 && !serviceUrl) return null;
return (
<div className="flex min-w-0 flex-col items-stretch gap-2 sm:items-end">
<div className={cn("flex min-w-0 flex-col items-stretch gap-2", alignEnd ? "sm:items-end" : "sm:items-start")}>
{controlItems.length > 0 ? (
<div className="flex max-w-full flex-col gap-2 sm:flex-row sm:flex-wrap sm:justify-end">
<div className={cn("flex max-w-full flex-col gap-2 sm:flex-row sm:flex-wrap", alignEnd ? "sm:justify-end" : "sm:justify-start")}>
{controlItems.map((item) => (
<div key={item.key} className="flex min-w-0 flex-col gap-1 sm:items-end">
<div key={item.key} className={cn("flex min-w-0 flex-col gap-1", alignEnd ? "sm:items-end" : "sm:items-start")}>
{controlItems.length > 1 ? (
<span className="truncate text-xs text-muted-foreground">{item.title}</span>
) : null}
@@ -493,6 +505,7 @@ export function WorkspaceRuntimeQuickControls({
pendingRequest={pendingRequest}
onAction={onAction}
square={square}
iconOnly={iconOnly}
/>
</div>
))}
@@ -503,7 +516,10 @@ export function WorkspaceRuntimeQuickControls({
href={serviceUrl}
target="_blank"
rel="noreferrer"
className="inline-flex min-w-0 items-center gap-1 self-start break-all text-xs text-muted-foreground hover:text-foreground hover:underline sm:self-end"
className={cn(
"inline-flex min-w-0 items-center gap-1 self-start break-all text-xs text-muted-foreground hover:text-foreground hover:underline",
alignEnd ? "sm:self-end" : "sm:self-start",
)}
>
{serviceUrl}
<ExternalLink className="h-3.5 w-3.5 shrink-0" />