fix: suggest comment reassignment from recent commenter
This commit is contained in:
@@ -46,6 +46,7 @@ interface CommentThreadProps {
|
||||
enableReassign?: boolean;
|
||||
reassignOptions?: InlineEntityOption[];
|
||||
currentAssigneeValue?: string;
|
||||
suggestedAssigneeValue?: string;
|
||||
mentions?: MentionOption[];
|
||||
}
|
||||
|
||||
@@ -269,13 +270,15 @@ export function CommentThread({
|
||||
enableReassign = false,
|
||||
reassignOptions = [],
|
||||
currentAssigneeValue = "",
|
||||
suggestedAssigneeValue,
|
||||
mentions: providedMentions,
|
||||
}: CommentThreadProps) {
|
||||
const [body, setBody] = useState("");
|
||||
const [reopen, setReopen] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [attaching, setAttaching] = useState(false);
|
||||
const [reassignTarget, setReassignTarget] = useState(currentAssigneeValue);
|
||||
const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue;
|
||||
const [reassignTarget, setReassignTarget] = useState(effectiveSuggestedAssigneeValue);
|
||||
const [highlightCommentId, setHighlightCommentId] = useState<string | null>(null);
|
||||
const editorRef = useRef<MarkdownEditorRef>(null);
|
||||
const attachInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -337,8 +340,8 @@ export function CommentThread({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setReassignTarget(currentAssigneeValue);
|
||||
}, [currentAssigneeValue]);
|
||||
setReassignTarget(effectiveSuggestedAssigneeValue);
|
||||
}, [effectiveSuggestedAssigneeValue]);
|
||||
|
||||
// Scroll to comment when URL hash matches #comment-{id}
|
||||
useEffect(() => {
|
||||
@@ -370,7 +373,7 @@ export function CommentThread({
|
||||
setBody("");
|
||||
if (draftKey) clearDraft(draftKey);
|
||||
setReopen(false);
|
||||
setReassignTarget(currentAssigneeValue);
|
||||
setReassignTarget(effectiveSuggestedAssigneeValue);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
currentUserAssigneeOption,
|
||||
formatAssigneeUserLabel,
|
||||
parseAssigneeValue,
|
||||
suggestedCommentAssigneeValue,
|
||||
} from "./assignees";
|
||||
|
||||
describe("assignee selection helpers", () => {
|
||||
@@ -50,4 +51,27 @@ describe("assignee selection helpers", () => {
|
||||
expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board");
|
||||
expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-");
|
||||
});
|
||||
|
||||
it("suggests the last non-me commenter without changing the actual assignee encoding", () => {
|
||||
expect(
|
||||
suggestedCommentAssigneeValue(
|
||||
{ assigneeUserId: "board-user" },
|
||||
[
|
||||
{ authorUserId: "board-user" },
|
||||
{ authorAgentId: "agent-123" },
|
||||
],
|
||||
"board-user",
|
||||
),
|
||||
).toBe("agent:agent-123");
|
||||
});
|
||||
|
||||
it("falls back to the actual assignee when there is no better commenter hint", () => {
|
||||
expect(
|
||||
suggestedCommentAssigneeValue(
|
||||
{ assigneeUserId: "board-user" },
|
||||
[{ authorUserId: "board-user" }],
|
||||
"board-user",
|
||||
),
|
||||
).toBe("user:board-user");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,40 @@ export interface AssigneeOption {
|
||||
searchText?: string;
|
||||
}
|
||||
|
||||
interface CommentAssigneeSuggestionInput {
|
||||
assigneeAgentId?: string | null;
|
||||
assigneeUserId?: string | null;
|
||||
}
|
||||
|
||||
interface CommentAssigneeSuggestionComment {
|
||||
authorAgentId?: string | null;
|
||||
authorUserId?: string | null;
|
||||
}
|
||||
|
||||
export function assigneeValueFromSelection(selection: Partial<AssigneeSelection>): string {
|
||||
if (selection.assigneeAgentId) return `agent:${selection.assigneeAgentId}`;
|
||||
if (selection.assigneeUserId) return `user:${selection.assigneeUserId}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
export function suggestedCommentAssigneeValue(
|
||||
issue: CommentAssigneeSuggestionInput,
|
||||
comments: CommentAssigneeSuggestionComment[] | null | undefined,
|
||||
currentUserId: string | null | undefined,
|
||||
): string {
|
||||
if (comments && comments.length > 0 && currentUserId) {
|
||||
for (let i = comments.length - 1; i >= 0; i--) {
|
||||
const comment = comments[i];
|
||||
if (comment.authorAgentId) return assigneeValueFromSelection({ assigneeAgentId: comment.authorAgentId });
|
||||
if (comment.authorUserId && comment.authorUserId !== currentUserId) {
|
||||
return assigneeValueFromSelection({ assigneeUserId: comment.authorUserId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assigneeValueFromSelection(issue);
|
||||
}
|
||||
|
||||
export function parseAssigneeValue(value: string): AssigneeSelection {
|
||||
if (!value) {
|
||||
return { assigneeAgentId: null, assigneeUserId: null };
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useCompany } from "../context/CompanyContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb";
|
||||
import { useProjectOrder } from "../hooks/useProjectOrder";
|
||||
@@ -374,21 +375,15 @@ export function IssueDetail() {
|
||||
return options;
|
||||
}, [agents, currentUserId]);
|
||||
|
||||
const currentAssigneeValue = useMemo(() => {
|
||||
// Default to the last commenter who is not "me" so the user doesn't
|
||||
// accidentally reassign to themselves when commenting on their own issue.
|
||||
if (comments && comments.length > 0 && currentUserId) {
|
||||
for (let i = comments.length - 1; i >= 0; i--) {
|
||||
const c = comments[i];
|
||||
if (c.authorAgentId) return `agent:${c.authorAgentId}`;
|
||||
if (c.authorUserId && c.authorUserId !== currentUserId)
|
||||
return `user:${c.authorUserId}`;
|
||||
}
|
||||
}
|
||||
if (issue?.assigneeAgentId) return `agent:${issue.assigneeAgentId}`;
|
||||
if (issue?.assigneeUserId) return `user:${issue.assigneeUserId}`;
|
||||
return "";
|
||||
}, [issue?.assigneeAgentId, issue?.assigneeUserId, comments, currentUserId]);
|
||||
const actualAssigneeValue = useMemo(
|
||||
() => assigneeValueFromSelection(issue ?? {}),
|
||||
[issue],
|
||||
);
|
||||
|
||||
const suggestedAssigneeValue = useMemo(
|
||||
() => suggestedCommentAssigneeValue(issue ?? {}, comments, currentUserId),
|
||||
[issue, comments, currentUserId],
|
||||
);
|
||||
|
||||
const commentsWithRunMeta = useMemo(() => {
|
||||
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null }>();
|
||||
@@ -1011,7 +1006,8 @@ export function IssueDetail() {
|
||||
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
||||
enableReassign
|
||||
reassignOptions={commentReassignOptions}
|
||||
currentAssigneeValue={currentAssigneeValue}
|
||||
currentAssigneeValue={actualAssigneeValue}
|
||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||
mentions={mentionOptions}
|
||||
onAdd={async (body, reopen, reassignment) => {
|
||||
if (reassignment) {
|
||||
|
||||
Reference in New Issue
Block a user