mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 18:30:39 +09:00
Guard closed isolated workspaces on issues
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
4993b5338c
commit
65818c3447
7 changed files with 372 additions and 85 deletions
|
|
@ -120,4 +120,28 @@ describe("CommentThread", () => {
|
|||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces the composer with a warning when comments are disabled", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<CommentThread
|
||||
comments={[]}
|
||||
composerDisabledReason="Workspace is closed."
|
||||
onAdd={async () => {}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Workspace is closed.");
|
||||
expect(container.querySelector('textarea[aria-label="Comment editor"]')).toBeNull();
|
||||
expect(container.textContent).not.toContain("Comment");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ interface CommentThreadProps {
|
|||
mentions?: MentionOption[];
|
||||
onInterruptQueued?: (runId: string) => Promise<void>;
|
||||
interruptingQueuedRunId?: string | null;
|
||||
composerDisabledReason?: string | null;
|
||||
}
|
||||
|
||||
const DRAFT_DEBOUNCE_MS = 800;
|
||||
|
|
@ -569,6 +570,7 @@ export function CommentThread({
|
|||
mentions: providedMentions,
|
||||
onInterruptQueued,
|
||||
interruptingQueuedRunId = null,
|
||||
composerDisabledReason = null,
|
||||
}: CommentThreadProps) {
|
||||
const [body, setBody] = useState("");
|
||||
const [reopen, setReopen] = useState(true);
|
||||
|
|
@ -796,90 +798,96 @@ export function CommentThread({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder="Leave a comment..."
|
||||
mentions={mentions}
|
||||
onSubmit={handleSubmit}
|
||||
imageUploadHandler={imageUploadHandler}
|
||||
contentClassName="min-h-[60px] text-sm"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{(imageUploadHandler || onAttachImage) && (
|
||||
<div className="mr-auto flex items-center gap-3">
|
||||
<input
|
||||
ref={attachInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
className="hidden"
|
||||
onChange={handleAttachFile}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => attachInputRef.current?.click()}
|
||||
disabled={attaching}
|
||||
title="Attach image"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={reopen}
|
||||
onChange={(e) => setReopen(e.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Re-open
|
||||
</label>
|
||||
{enableReassign && reassignOptions.length > 0 && (
|
||||
<InlineEntitySelector
|
||||
value={reassignTarget}
|
||||
options={reassignOptions}
|
||||
placeholder="Assignee"
|
||||
noneLabel="No assignee"
|
||||
searchPlaceholder="Search assignees..."
|
||||
emptyMessage="No assignees found."
|
||||
onChange={setReassignTarget}
|
||||
className="text-xs h-8"
|
||||
renderTriggerValue={(option) => {
|
||||
if (!option) return <span className="text-muted-foreground">Assignee</span>;
|
||||
const agentId = option.id.startsWith("agent:") ? option.id.slice("agent:".length) : null;
|
||||
const agent = agentId ? agentMap?.get(agentId) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
renderOption={(option) => {
|
||||
if (!option.id) return <span className="truncate">{option.label}</span>;
|
||||
const agentId = option.id.startsWith("agent:") ? option.id.slice("agent:".length) : null;
|
||||
const agent = agentId ? agentMap?.get(agentId) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button size="sm" disabled={!canSubmit} onClick={handleSubmit}>
|
||||
{submitting ? "Posting..." : "Comment"}
|
||||
</Button>
|
||||
{composerDisabledReason ? (
|
||||
<div className="rounded-md border border-amber-300/70 bg-amber-50/80 px-3 py-2 text-sm text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
{composerDisabledReason}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder="Leave a comment..."
|
||||
mentions={mentions}
|
||||
onSubmit={handleSubmit}
|
||||
imageUploadHandler={imageUploadHandler}
|
||||
contentClassName="min-h-[60px] text-sm"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{(imageUploadHandler || onAttachImage) && (
|
||||
<div className="mr-auto flex items-center gap-3">
|
||||
<input
|
||||
ref={attachInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
className="hidden"
|
||||
onChange={handleAttachFile}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => attachInputRef.current?.click()}
|
||||
disabled={attaching}
|
||||
title="Attach image"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={reopen}
|
||||
onChange={(e) => setReopen(e.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Re-open
|
||||
</label>
|
||||
{enableReassign && reassignOptions.length > 0 && (
|
||||
<InlineEntitySelector
|
||||
value={reassignTarget}
|
||||
options={reassignOptions}
|
||||
placeholder="Assignee"
|
||||
noneLabel="No assignee"
|
||||
searchPlaceholder="Search assignees..."
|
||||
emptyMessage="No assignees found."
|
||||
onChange={setReassignTarget}
|
||||
className="text-xs h-8"
|
||||
renderTriggerValue={(option) => {
|
||||
if (!option) return <span className="text-muted-foreground">Assignee</span>;
|
||||
const agentId = option.id.startsWith("agent:") ? option.id.slice("agent:".length) : null;
|
||||
const agent = agentId ? agentMap?.get(agentId) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
renderOption={(option) => {
|
||||
if (!option.id) return <span className="truncate">{option.label}</span>;
|
||||
const agentId = option.id.startsWith("agent:") ? option.id.slice("agent:".length) : null;
|
||||
const agent = agentId ? agentMap?.get(agentId) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button size="sm" disabled={!canSubmit} onClick={handleSubmit}>
|
||||
{submitting ? "Posting..." : "Comment"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,8 +71,16 @@ import {
|
|||
SlidersHorizontal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import type { ActivityEvent } from "@paperclipai/shared";
|
||||
import type { Agent, FeedbackVote, Issue, IssueAttachment, IssueComment } from "@paperclipai/shared";
|
||||
import {
|
||||
getClosedIsolatedExecutionWorkspaceMessage,
|
||||
isClosedIsolatedExecutionWorkspace,
|
||||
type ActivityEvent,
|
||||
type Agent,
|
||||
type FeedbackVote,
|
||||
type Issue,
|
||||
type IssueAttachment,
|
||||
type IssueComment,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
type CommentReassignment = IssueCommentReassignment;
|
||||
type IssueDetailComment = (IssueComment | OptimisticIssueComment) & {
|
||||
|
|
@ -306,6 +314,12 @@ export function IssueDetail() {
|
|||
enabled: !!issueId,
|
||||
});
|
||||
const resolvedCompanyId = issue?.companyId ?? selectedCompanyId;
|
||||
const commentComposerDisabledReason = useMemo(() => {
|
||||
if (!issue?.currentExecutionWorkspace || !isClosedIsolatedExecutionWorkspace(issue.currentExecutionWorkspace)) {
|
||||
return null;
|
||||
}
|
||||
return getClosedIsolatedExecutionWorkspaceMessage(issue.currentExecutionWorkspace);
|
||||
}, [issue?.currentExecutionWorkspace]);
|
||||
|
||||
const { data: comments } = useQuery({
|
||||
queryKey: queryKeys.issues.comments(issueId!),
|
||||
|
|
@ -1522,6 +1536,7 @@ export function IssueDetail() {
|
|||
await interruptQueuedComment.mutateAsync(runId);
|
||||
}}
|
||||
interruptingQueuedRunId={interruptQueuedComment.isPending ? runningIssueRun?.id ?? null : null}
|
||||
composerDisabledReason={commentComposerDisabledReason}
|
||||
onVote={async (commentId, vote, options) => {
|
||||
await feedbackVoteMutation.mutateAsync({
|
||||
targetType: "issue_comment",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue