mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Add explicit review start action in issue sidebar
This commit is contained in:
parent
efc1e336b0
commit
de1cd5858d
2 changed files with 162 additions and 0 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import type { IssueExecutionPolicy, IssueExecutionState } from "@paperclipai/shared";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import type { Issue } from "@paperclipai/shared";
|
import type { Issue } from "@paperclipai/shared";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
@ -143,6 +144,30 @@ function createIssue(overrides: Partial<Issue> = {}): Issue {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createExecutionPolicy(overrides: Partial<IssueExecutionPolicy> = {}): IssueExecutionPolicy {
|
||||||
|
return {
|
||||||
|
mode: "normal",
|
||||||
|
commentRequired: true,
|
||||||
|
stages: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExecutionState(overrides: Partial<IssueExecutionState> = {}): IssueExecutionState {
|
||||||
|
return {
|
||||||
|
status: "changes_requested",
|
||||||
|
currentStageId: "stage-1",
|
||||||
|
currentStageIndex: 0,
|
||||||
|
currentStageType: "review",
|
||||||
|
currentParticipant: { type: "agent", agentId: "agent-1", userId: null },
|
||||||
|
returnAssignee: { type: "agent", agentId: "agent-2", userId: null },
|
||||||
|
completedStageIds: [],
|
||||||
|
lastDecisionId: null,
|
||||||
|
lastDecisionOutcome: "changes_requested",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function renderProperties(container: HTMLDivElement, props: ComponentProps<typeof IssueProperties>) {
|
function renderProperties(container: HTMLDivElement, props: ComponentProps<typeof IssueProperties>) {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
|
@ -201,4 +226,119 @@ describe("IssueProperties", () => {
|
||||||
|
|
||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows a run review action after reviewers are configured and starts execution explicitly when clicked", async () => {
|
||||||
|
const onUpdate = vi.fn();
|
||||||
|
const root = renderProperties(container, {
|
||||||
|
issue: createIssue({
|
||||||
|
executionPolicy: createExecutionPolicy({
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: "review-stage",
|
||||||
|
type: "review",
|
||||||
|
approvalsNeeded: 1,
|
||||||
|
participants: [{ id: "participant-1", type: "agent", agentId: "agent-1", userId: null }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
childIssues: [],
|
||||||
|
onUpdate,
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
const runReviewButton = Array.from(container.querySelectorAll("button"))
|
||||||
|
.find((button) => button.textContent?.includes("Run review now"));
|
||||||
|
expect(runReviewButton).not.toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
runReviewButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onUpdate).toHaveBeenCalledWith({ status: "in_review" });
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a run approval action when approval is the next runnable stage", async () => {
|
||||||
|
const root = renderProperties(container, {
|
||||||
|
issue: createIssue({
|
||||||
|
executionPolicy: createExecutionPolicy({
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: "approval-stage",
|
||||||
|
type: "approval",
|
||||||
|
approvalsNeeded: 1,
|
||||||
|
participants: [{ id: "participant-2", type: "user", agentId: null, userId: "user-1" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
childIssues: [],
|
||||||
|
onUpdate: vi.fn(),
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(container.textContent).toContain("Run approval now");
|
||||||
|
expect(container.textContent).not.toContain("Run review now");
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the run review action available after changes are requested", async () => {
|
||||||
|
const root = renderProperties(container, {
|
||||||
|
issue: createIssue({
|
||||||
|
status: "in_progress",
|
||||||
|
executionPolicy: createExecutionPolicy({
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: "review-stage",
|
||||||
|
type: "review",
|
||||||
|
approvalsNeeded: 1,
|
||||||
|
participants: [{ id: "participant-1", type: "agent", agentId: "agent-1", userId: null }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
executionState: createExecutionState(),
|
||||||
|
}),
|
||||||
|
childIssues: [],
|
||||||
|
onUpdate: vi.fn(),
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(container.textContent).toContain("Run review now");
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the run action while an execution stage is already pending", async () => {
|
||||||
|
const root = renderProperties(container, {
|
||||||
|
issue: createIssue({
|
||||||
|
status: "in_review",
|
||||||
|
executionPolicy: createExecutionPolicy({
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: "review-stage",
|
||||||
|
type: "review",
|
||||||
|
approvalsNeeded: 1,
|
||||||
|
participants: [{ id: "participant-1", type: "agent", agentId: "agent-1", userId: null }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
executionState: createExecutionState({
|
||||||
|
status: "pending",
|
||||||
|
currentStageType: "review",
|
||||||
|
lastDecisionOutcome: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
childIssues: [],
|
||||||
|
onUpdate: vi.fn(),
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(container.textContent).not.toContain("Run review now");
|
||||||
|
expect(container.textContent).not.toContain("Run approval now");
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,26 @@ export function IssueProperties({
|
||||||
const approverTrigger = approverValues.length > 0
|
const approverTrigger = approverValues.length > 0
|
||||||
? <span className="text-sm truncate">{approverValues.map((value) => executionParticipantLabel(value)).join(", ")}</span>
|
? <span className="text-sm truncate">{approverValues.map((value) => executionParticipantLabel(value)).join(", ")}</span>
|
||||||
: <span className="text-sm text-muted-foreground">None</span>;
|
: <span className="text-sm text-muted-foreground">None</span>;
|
||||||
|
const nextRunnableExecutionStage = (() => {
|
||||||
|
if (issue.executionState?.status === "changes_requested" && issue.executionState.currentStageType) {
|
||||||
|
return issue.executionState.currentStageType;
|
||||||
|
}
|
||||||
|
if (issue.executionState) return null;
|
||||||
|
if (reviewerValues.length > 0) return "review";
|
||||||
|
if (approverValues.length > 0) return "approval";
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
const runExecutionButton = (stageType: "review" | "approval") => (
|
||||||
|
<PropertyRow label="">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center rounded-full border border-border px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||||
|
onClick={() => onUpdate({ status: "in_review" })}
|
||||||
|
>
|
||||||
|
{stageType === "review" ? "Run review now" : "Run approval now"}
|
||||||
|
</button>
|
||||||
|
</PropertyRow>
|
||||||
|
);
|
||||||
const currentExecutionLabel = (() => {
|
const currentExecutionLabel = (() => {
|
||||||
if (!issue.executionState?.currentStageType) return null;
|
if (!issue.executionState?.currentStageType) return null;
|
||||||
const stageLabel = issue.executionState.currentStageType === "review" ? "Review" : "Approval";
|
const stageLabel = issue.executionState.currentStageType === "review" ? "Review" : "Approval";
|
||||||
|
|
@ -892,6 +912,7 @@ export function IssueProperties({
|
||||||
() => updateExecutionPolicy([], approverValues),
|
() => updateExecutionPolicy([], approverValues),
|
||||||
)}
|
)}
|
||||||
</PropertyPicker>
|
</PropertyPicker>
|
||||||
|
{nextRunnableExecutionStage === "review" && reviewerValues.length > 0 ? runExecutionButton("review") : null}
|
||||||
|
|
||||||
<PropertyPicker
|
<PropertyPicker
|
||||||
inline={inline}
|
inline={inline}
|
||||||
|
|
@ -910,6 +931,7 @@ export function IssueProperties({
|
||||||
() => updateExecutionPolicy(reviewerValues, []),
|
() => updateExecutionPolicy(reviewerValues, []),
|
||||||
)}
|
)}
|
||||||
</PropertyPicker>
|
</PropertyPicker>
|
||||||
|
{nextRunnableExecutionStage === "approval" && approverValues.length > 0 ? runExecutionButton("approval") : null}
|
||||||
|
|
||||||
{currentExecutionLabel && (
|
{currentExecutionLabel && (
|
||||||
<PropertyRow label="Execution">
|
<PropertyRow label="Execution">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue