Add issue comment interrupt support
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
cfb7dd4818
commit
4226e15128
5 changed files with 179 additions and 35 deletions
|
|
@ -66,6 +66,7 @@ export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>;
|
||||||
export const updateIssueSchema = createIssueSchema.partial().extend({
|
export const updateIssueSchema = createIssueSchema.partial().extend({
|
||||||
comment: z.string().min(1).optional(),
|
comment: z.string().min(1).optional(),
|
||||||
reopen: z.boolean().optional(),
|
reopen: z.boolean().optional(),
|
||||||
|
interrupt: z.boolean().optional(),
|
||||||
hiddenAt: z.string().datetime().nullable().optional(),
|
hiddenAt: z.string().datetime().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ const mockAccessService = vi.hoisted(() => ({
|
||||||
const mockHeartbeatService = vi.hoisted(() => ({
|
const mockHeartbeatService = vi.hoisted(() => ({
|
||||||
wakeup: vi.fn(async () => undefined),
|
wakeup: vi.fn(async () => undefined),
|
||||||
reportRunActivity: vi.fn(async () => undefined),
|
reportRunActivity: vi.fn(async () => undefined),
|
||||||
|
getRun: vi.fn(async () => null),
|
||||||
|
getActiveRunForAgent: vi.fn(async () => null),
|
||||||
|
cancelRun: vi.fn(async () => null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockAgentService = vi.hoisted(() => ({
|
const mockAgentService = vi.hoisted(() => ({
|
||||||
|
|
@ -143,4 +146,46 @@ describe("issue comment reopen routes", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("interrupts an active run before a combined comment update", async () => {
|
||||||
|
const issue = {
|
||||||
|
...makeIssue("todo"),
|
||||||
|
executionRunId: "run-1",
|
||||||
|
};
|
||||||
|
mockIssueService.getById.mockResolvedValue(issue);
|
||||||
|
mockIssueService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => ({
|
||||||
|
...issue,
|
||||||
|
...patch,
|
||||||
|
}));
|
||||||
|
mockHeartbeatService.getRun.mockResolvedValue({
|
||||||
|
id: "run-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
agentId: "22222222-2222-4222-8222-222222222222",
|
||||||
|
status: "running",
|
||||||
|
});
|
||||||
|
mockHeartbeatService.cancelRun.mockResolvedValue({
|
||||||
|
id: "run-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
agentId: "22222222-2222-4222-8222-222222222222",
|
||||||
|
status: "cancelled",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(createApp())
|
||||||
|
.patch("/api/issues/11111111-1111-4111-8111-111111111111")
|
||||||
|
.send({ comment: "hello", interrupt: true, assigneeAgentId: "33333333-3333-4333-8333-333333333333" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(mockHeartbeatService.getRun).toHaveBeenCalledWith("run-1");
|
||||||
|
expect(mockHeartbeatService.cancelRun).toHaveBeenCalledWith("run-1");
|
||||||
|
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "heartbeat.cancelled",
|
||||||
|
details: expect.objectContaining({
|
||||||
|
source: "issue_comment_interrupt",
|
||||||
|
issueId: "11111111-1111-4111-8111-111111111111",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Router, type Request, type Response } from "express";
|
import { Router, type Request, type Response } from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
|
import { z } from "zod";
|
||||||
import type { Db } from "@paperclipai/db";
|
import type { Db } from "@paperclipai/db";
|
||||||
import {
|
import {
|
||||||
addIssueCommentSchema,
|
addIssueCommentSchema,
|
||||||
|
|
@ -38,6 +39,9 @@ import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.
|
||||||
import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js";
|
import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js";
|
||||||
|
|
||||||
const MAX_ISSUE_COMMENT_LIMIT = 500;
|
const MAX_ISSUE_COMMENT_LIMIT = 500;
|
||||||
|
const updateIssueRouteSchema = updateIssueSchema.extend({
|
||||||
|
interrupt: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export function issueRoutes(db: Db, storage: StorageService) {
|
export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
@ -161,6 +165,30 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveActiveIssueRun(issue: {
|
||||||
|
id: string;
|
||||||
|
assigneeAgentId: string | null;
|
||||||
|
executionRunId?: string | null;
|
||||||
|
}) {
|
||||||
|
let runToInterrupt = issue.executionRunId ? await heartbeat.getRun(issue.executionRunId) : null;
|
||||||
|
|
||||||
|
if ((!runToInterrupt || runToInterrupt.status !== "running") && issue.assigneeAgentId) {
|
||||||
|
const activeRun = await heartbeat.getActiveRunForAgent(issue.assigneeAgentId);
|
||||||
|
const activeIssueId =
|
||||||
|
activeRun &&
|
||||||
|
activeRun.contextSnapshot &&
|
||||||
|
typeof activeRun.contextSnapshot === "object" &&
|
||||||
|
typeof (activeRun.contextSnapshot as Record<string, unknown>).issueId === "string"
|
||||||
|
? ((activeRun.contextSnapshot as Record<string, unknown>).issueId as string)
|
||||||
|
: null;
|
||||||
|
if (activeRun && activeRun.status === "running" && activeIssueId === issue.id) {
|
||||||
|
runToInterrupt = activeRun;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runToInterrupt?.status === "running" ? runToInterrupt : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function normalizeIssueIdentifier(rawId: string): Promise<string> {
|
async function normalizeIssueIdentifier(rawId: string): Promise<string> {
|
||||||
if (/^[A-Z]+-\d+$/i.test(rawId)) {
|
if (/^[A-Z]+-\d+$/i.test(rawId)) {
|
||||||
const issue = await svc.getByIdentifier(rawId);
|
const issue = await svc.getByIdentifier(rawId);
|
||||||
|
|
@ -919,7 +947,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
res.status(201).json(issue);
|
res.status(201).json(issue);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.patch("/issues/:id", validate(updateIssueSchema), async (req, res) => {
|
router.patch("/issues/:id", validate(updateIssueRouteSchema), async (req, res) => {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
const existing = await svc.getById(id);
|
const existing = await svc.getById(id);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
|
|
@ -949,7 +977,45 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
|
|
||||||
const actor = getActorInfo(req);
|
const actor = getActorInfo(req);
|
||||||
const isClosed = existing.status === "done" || existing.status === "cancelled";
|
const isClosed = existing.status === "done" || existing.status === "cancelled";
|
||||||
const { comment: commentBody, reopen: reopenRequested, hiddenAt: hiddenAtRaw, ...updateFields } = req.body;
|
const {
|
||||||
|
comment: commentBody,
|
||||||
|
reopen: reopenRequested,
|
||||||
|
interrupt: interruptRequested,
|
||||||
|
hiddenAt: hiddenAtRaw,
|
||||||
|
...updateFields
|
||||||
|
} = req.body;
|
||||||
|
let interruptedRunId: string | null = null;
|
||||||
|
|
||||||
|
if (interruptRequested) {
|
||||||
|
if (!commentBody) {
|
||||||
|
res.status(400).json({ error: "Interrupt is only supported when posting a comment" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.actor.type !== "board") {
|
||||||
|
res.status(403).json({ error: "Only board users can interrupt active runs from issue comments" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runToInterrupt = await resolveActiveIssueRun(existing);
|
||||||
|
if (runToInterrupt) {
|
||||||
|
const cancelled = await heartbeat.cancelRun(runToInterrupt.id);
|
||||||
|
if (cancelled) {
|
||||||
|
interruptedRunId = cancelled.id;
|
||||||
|
await logActivity(db, {
|
||||||
|
companyId: cancelled.companyId,
|
||||||
|
actorType: actor.actorType,
|
||||||
|
actorId: actor.actorId,
|
||||||
|
agentId: actor.agentId,
|
||||||
|
runId: actor.runId,
|
||||||
|
action: "heartbeat.cancelled",
|
||||||
|
entityType: "heartbeat_run",
|
||||||
|
entityId: cancelled.id,
|
||||||
|
details: { agentId: cancelled.agentId, source: "issue_comment_interrupt", issueId: existing.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hiddenAtRaw !== undefined) {
|
if (hiddenAtRaw !== undefined) {
|
||||||
updateFields.hiddenAt = hiddenAtRaw ? new Date(hiddenAtRaw) : null;
|
updateFields.hiddenAt = hiddenAtRaw ? new Date(hiddenAtRaw) : null;
|
||||||
}
|
}
|
||||||
|
|
@ -1024,6 +1090,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
identifier: issue.identifier,
|
identifier: issue.identifier,
|
||||||
...(commentBody ? { source: "comment" } : {}),
|
...(commentBody ? { source: "comment" } : {}),
|
||||||
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus } : {}),
|
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus } : {}),
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
_previous: hasFieldChanges ? previous : undefined,
|
_previous: hasFieldChanges ? previous : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -1050,6 +1117,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
identifier: issue.identifier,
|
identifier: issue.identifier,
|
||||||
issueTitle: issue.title,
|
issueTitle: issue.title,
|
||||||
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus, source: "comment" } : {}),
|
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus, source: "comment" } : {}),
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
...(hasFieldChanges ? { updated: true } : {}),
|
...(hasFieldChanges ? { updated: true } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -1071,10 +1139,18 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
source: "assignment",
|
source: "assignment",
|
||||||
triggerDetail: "system",
|
triggerDetail: "system",
|
||||||
reason: "issue_assigned",
|
reason: "issue_assigned",
|
||||||
payload: { issueId: issue.id, mutation: "update" },
|
payload: {
|
||||||
|
issueId: issue.id,
|
||||||
|
mutation: "update",
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
|
},
|
||||||
requestedByActorType: actor.actorType,
|
requestedByActorType: actor.actorType,
|
||||||
requestedByActorId: actor.actorId,
|
requestedByActorId: actor.actorId,
|
||||||
contextSnapshot: { issueId: issue.id, source: "issue.update" },
|
contextSnapshot: {
|
||||||
|
issueId: issue.id,
|
||||||
|
source: "issue.update",
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1083,10 +1159,18 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
source: "automation",
|
source: "automation",
|
||||||
triggerDetail: "system",
|
triggerDetail: "system",
|
||||||
reason: "issue_status_changed",
|
reason: "issue_status_changed",
|
||||||
payload: { issueId: issue.id, mutation: "update" },
|
payload: {
|
||||||
|
issueId: issue.id,
|
||||||
|
mutation: "update",
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
|
},
|
||||||
requestedByActorType: actor.actorType,
|
requestedByActorType: actor.actorType,
|
||||||
requestedByActorId: actor.actorId,
|
requestedByActorId: actor.actorId,
|
||||||
contextSnapshot: { issueId: issue.id, source: "issue.status_change" },
|
contextSnapshot: {
|
||||||
|
issueId: issue.id,
|
||||||
|
source: "issue.status_change",
|
||||||
|
...(interruptedRunId ? { interruptedRunId } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1379,28 +1463,8 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let runToInterrupt = currentIssue.executionRunId
|
const runToInterrupt = await resolveActiveIssueRun(currentIssue);
|
||||||
? await heartbeat.getRun(currentIssue.executionRunId)
|
if (runToInterrupt) {
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!runToInterrupt || runToInterrupt.status !== "running") &&
|
|
||||||
currentIssue.assigneeAgentId
|
|
||||||
) {
|
|
||||||
const activeRun = await heartbeat.getActiveRunForAgent(currentIssue.assigneeAgentId);
|
|
||||||
const activeIssueId =
|
|
||||||
activeRun &&
|
|
||||||
activeRun.contextSnapshot &&
|
|
||||||
typeof activeRun.contextSnapshot === "object" &&
|
|
||||||
typeof (activeRun.contextSnapshot as Record<string, unknown>).issueId === "string"
|
|
||||||
? ((activeRun.contextSnapshot as Record<string, unknown>).issueId as string)
|
|
||||||
: null;
|
|
||||||
if (activeRun && activeRun.status === "running" && activeIssueId === currentIssue.id) {
|
|
||||||
runToInterrupt = activeRun;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runToInterrupt && runToInterrupt.status === "running") {
|
|
||||||
const cancelled = await heartbeat.cancelRun(runToInterrupt.id);
|
const cancelled = await heartbeat.cancelRun(runToInterrupt.id);
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
interruptedRunId = cancelled.id;
|
interruptedRunId = cancelled.id;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,12 @@ interface CommentThreadProps {
|
||||||
linkedRuns?: LinkedRunItem[];
|
linkedRuns?: LinkedRunItem[];
|
||||||
companyId?: string | null;
|
companyId?: string | null;
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
|
onAdd: (
|
||||||
|
body: string,
|
||||||
|
reopen?: boolean,
|
||||||
|
reassignment?: CommentReassignment,
|
||||||
|
interrupt?: boolean,
|
||||||
|
) => Promise<void>;
|
||||||
issueStatus?: string;
|
issueStatus?: string;
|
||||||
agentMap?: Map<string, Agent>;
|
agentMap?: Map<string, Agent>;
|
||||||
imageUploadHandler?: (file: File) => Promise<string>;
|
imageUploadHandler?: (file: File) => Promise<string>;
|
||||||
|
|
@ -50,6 +55,7 @@ interface CommentThreadProps {
|
||||||
currentAssigneeValue?: string;
|
currentAssigneeValue?: string;
|
||||||
suggestedAssigneeValue?: string;
|
suggestedAssigneeValue?: string;
|
||||||
mentions?: MentionOption[];
|
mentions?: MentionOption[];
|
||||||
|
interruptAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DRAFT_DEBOUNCE_MS = 800;
|
const DRAFT_DEBOUNCE_MS = 800;
|
||||||
|
|
@ -279,9 +285,11 @@ export function CommentThread({
|
||||||
currentAssigneeValue = "",
|
currentAssigneeValue = "",
|
||||||
suggestedAssigneeValue,
|
suggestedAssigneeValue,
|
||||||
mentions: providedMentions,
|
mentions: providedMentions,
|
||||||
|
interruptAvailable = false,
|
||||||
}: CommentThreadProps) {
|
}: CommentThreadProps) {
|
||||||
const [body, setBody] = useState("");
|
const [body, setBody] = useState("");
|
||||||
const [reopen, setReopen] = useState(true);
|
const [reopen, setReopen] = useState(true);
|
||||||
|
const [interrupt, setInterrupt] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [attaching, setAttaching] = useState(false);
|
const [attaching, setAttaching] = useState(false);
|
||||||
const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue;
|
const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue;
|
||||||
|
|
@ -351,6 +359,14 @@ export function CommentThread({
|
||||||
setReassignTarget(effectiveSuggestedAssigneeValue);
|
setReassignTarget(effectiveSuggestedAssigneeValue);
|
||||||
}, [effectiveSuggestedAssigneeValue]);
|
}, [effectiveSuggestedAssigneeValue]);
|
||||||
|
|
||||||
|
const interruptVisible = interruptAvailable && body.trim().length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!interruptVisible && interrupt) {
|
||||||
|
setInterrupt(false);
|
||||||
|
}
|
||||||
|
}, [interruptVisible, interrupt]);
|
||||||
|
|
||||||
// Scroll to comment when URL hash matches #comment-{id}
|
// Scroll to comment when URL hash matches #comment-{id}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hash = location.hash;
|
const hash = location.hash;
|
||||||
|
|
@ -377,10 +393,11 @@ export function CommentThread({
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await onAdd(trimmed, reopen ? true : undefined, reassignment ?? undefined);
|
await onAdd(trimmed, reopen ? true : undefined, reassignment ?? undefined, interrupt ? true : undefined);
|
||||||
setBody("");
|
setBody("");
|
||||||
if (draftKey) clearDraft(draftKey);
|
if (draftKey) clearDraft(draftKey);
|
||||||
setReopen(true);
|
setReopen(true);
|
||||||
|
setInterrupt(false);
|
||||||
setReassignTarget(effectiveSuggestedAssigneeValue);
|
setReassignTarget(effectiveSuggestedAssigneeValue);
|
||||||
} catch {
|
} catch {
|
||||||
// Parent mutation handlers surface the failure and keep the draft intact.
|
// Parent mutation handlers surface the failure and keep the draft intact.
|
||||||
|
|
@ -465,6 +482,17 @@ export function CommentThread({
|
||||||
/>
|
/>
|
||||||
Re-open
|
Re-open
|
||||||
</label>
|
</label>
|
||||||
|
{interruptVisible && (
|
||||||
|
<label className="flex items-center gap-1.5 text-xs text-red-700 dark:text-red-300 cursor-pointer select-none">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={interrupt}
|
||||||
|
onChange={(e) => setInterrupt(e.target.checked)}
|
||||||
|
className="rounded border-red-300 accent-red-600"
|
||||||
|
/>
|
||||||
|
Interrupt
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{enableReassign && reassignOptions.length > 0 && (
|
{enableReassign && reassignOptions.length > 0 && (
|
||||||
<InlineEntitySelector
|
<InlineEntitySelector
|
||||||
value={reassignTarget}
|
value={reassignTarget}
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,8 @@ export function IssueDetail() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
||||||
|
const hasRunningIssueRun =
|
||||||
|
activeRun?.status === "running" || (liveRuns ?? []).some((run) => run.status === "running");
|
||||||
const sourceBreadcrumb = useMemo(
|
const sourceBreadcrumb = useMemo(
|
||||||
() => readIssueDetailBreadcrumb(location.state, location.search) ?? { label: "Issues", href: "/issues" },
|
() => readIssueDetailBreadcrumb(location.state, location.search) ?? { label: "Issues", href: "/issues" },
|
||||||
[location.state, location.search],
|
[location.state, location.search],
|
||||||
|
|
@ -505,8 +507,8 @@ export function IssueDetail() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const addComment = useMutation({
|
const addComment = useMutation({
|
||||||
mutationFn: ({ body, reopen }: { body: string; reopen?: boolean }) =>
|
mutationFn: ({ body, reopen, interrupt }: { body: string; reopen?: boolean; interrupt?: boolean }) =>
|
||||||
issuesApi.addComment(issueId!, body, reopen),
|
issuesApi.addComment(issueId!, body, reopen, interrupt),
|
||||||
onMutate: async ({ body, reopen }) => {
|
onMutate: async ({ body, reopen }) => {
|
||||||
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
||||||
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
||||||
|
|
@ -572,10 +574,12 @@ export function IssueDetail() {
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
body,
|
body,
|
||||||
reopen,
|
reopen,
|
||||||
|
interrupt,
|
||||||
reassignment,
|
reassignment,
|
||||||
}: {
|
}: {
|
||||||
body: string;
|
body: string;
|
||||||
reopen?: boolean;
|
reopen?: boolean;
|
||||||
|
interrupt?: boolean;
|
||||||
reassignment: CommentReassignment;
|
reassignment: CommentReassignment;
|
||||||
}) =>
|
}) =>
|
||||||
issuesApi.update(issueId!, {
|
issuesApi.update(issueId!, {
|
||||||
|
|
@ -583,6 +587,7 @@ export function IssueDetail() {
|
||||||
assigneeAgentId: reassignment.assigneeAgentId,
|
assigneeAgentId: reassignment.assigneeAgentId,
|
||||||
assigneeUserId: reassignment.assigneeUserId,
|
assigneeUserId: reassignment.assigneeUserId,
|
||||||
...(reopen ? { status: "todo" } : {}),
|
...(reopen ? { status: "todo" } : {}),
|
||||||
|
...(interrupt ? { interrupt } : {}),
|
||||||
}),
|
}),
|
||||||
onMutate: async ({ body, reopen, reassignment }) => {
|
onMutate: async ({ body, reopen, reassignment }) => {
|
||||||
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
||||||
|
|
@ -1171,12 +1176,13 @@ export function IssueDetail() {
|
||||||
currentAssigneeValue={actualAssigneeValue}
|
currentAssigneeValue={actualAssigneeValue}
|
||||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||||
mentions={mentionOptions}
|
mentions={mentionOptions}
|
||||||
onAdd={async (body, reopen, reassignment) => {
|
interruptAvailable={hasRunningIssueRun}
|
||||||
|
onAdd={async (body, reopen, reassignment, interrupt) => {
|
||||||
if (reassignment) {
|
if (reassignment) {
|
||||||
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment });
|
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment, interrupt });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await addComment.mutateAsync({ body, reopen });
|
await addComment.mutateAsync({ body, reopen, interrupt });
|
||||||
}}
|
}}
|
||||||
imageUploadHandler={async (file) => {
|
imageUploadHandler={async (file) => {
|
||||||
const attachment = await uploadAttachment.mutateAsync(file);
|
const attachment = await uploadAttachment.mutateAsync(file);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue