mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 20:10:39 +09:00
Add generic issue-linked board approvals
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
6b4f3b56e4
commit
365b6d9bd8
10 changed files with 345 additions and 39 deletions
|
|
@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useRef, useState, type ChangeEvent } from "re
|
|||
import { Link, useLocation } from "react-router-dom";
|
||||
import type {
|
||||
Agent,
|
||||
Approval,
|
||||
FeedbackDataSharingPreference,
|
||||
FeedbackVote,
|
||||
FeedbackVoteValue,
|
||||
|
|
@ -15,7 +16,7 @@ import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySel
|
|||
import { MarkdownBody } from "./MarkdownBody";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
||||
import { OutputFeedbackButtons } from "./OutputFeedbackButtons";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import { ApprovalCard } from "./ApprovalCard";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||
import type { IssueTimelineAssignee, IssueTimelineEvent } from "../lib/issue-timeline-events";
|
||||
|
|
@ -50,6 +51,7 @@ interface CommentReassignment {
|
|||
interface CommentThreadProps {
|
||||
comments: CommentWithRunMeta[];
|
||||
queuedComments?: CommentWithRunMeta[];
|
||||
linkedApprovals?: Approval[];
|
||||
feedbackVotes?: FeedbackVote[];
|
||||
feedbackDataSharingPreference?: FeedbackDataSharingPreference;
|
||||
feedbackTermsUrl?: string | null;
|
||||
|
|
@ -57,6 +59,12 @@ interface CommentThreadProps {
|
|||
timelineEvents?: IssueTimelineEvent[];
|
||||
companyId?: string | null;
|
||||
projectId?: string | null;
|
||||
onApproveApproval?: (approvalId: string) => Promise<void>;
|
||||
onRejectApproval?: (approvalId: string) => Promise<void>;
|
||||
pendingApprovalAction?: {
|
||||
approvalId: string;
|
||||
action: "approve" | "reject";
|
||||
} | null;
|
||||
onVote?: (
|
||||
commentId: string,
|
||||
vote: FeedbackVoteValue,
|
||||
|
|
@ -375,6 +383,7 @@ function CommentCard({
|
|||
|
||||
type TimelineItem =
|
||||
| { kind: "comment"; id: string; createdAtMs: number; comment: CommentWithRunMeta }
|
||||
| { kind: "approval"; id: string; createdAtMs: number; approval: Approval }
|
||||
| { kind: "event"; id: string; createdAtMs: number; event: IssueTimelineEvent }
|
||||
| { kind: "run"; id: string; createdAtMs: number; run: LinkedRunItem };
|
||||
|
||||
|
|
@ -447,6 +456,9 @@ const TimelineList = memo(function TimelineList({
|
|||
currentUserId,
|
||||
companyId,
|
||||
projectId,
|
||||
onApproveApproval,
|
||||
onRejectApproval,
|
||||
pendingApprovalAction,
|
||||
feedbackVoteByTargetId,
|
||||
feedbackDataSharingPreference = "prompt",
|
||||
feedbackTermsUrl = null,
|
||||
|
|
@ -459,6 +471,12 @@ const TimelineList = memo(function TimelineList({
|
|||
currentUserId?: string | null;
|
||||
companyId?: string | null;
|
||||
projectId?: string | null;
|
||||
onApproveApproval?: (approvalId: string) => Promise<void>;
|
||||
onRejectApproval?: (approvalId: string) => Promise<void>;
|
||||
pendingApprovalAction?: {
|
||||
approvalId: string;
|
||||
action: "approve" | "reject";
|
||||
} | null;
|
||||
feedbackVoteByTargetId?: Map<string, FeedbackVoteValue>;
|
||||
feedbackDataSharingPreference?: FeedbackDataSharingPreference;
|
||||
feedbackTermsUrl?: string | null;
|
||||
|
|
@ -488,6 +506,24 @@ const TimelineList = memo(function TimelineList({
|
|||
);
|
||||
}
|
||||
|
||||
if (item.kind === "approval") {
|
||||
const approval = item.approval;
|
||||
const isPending = pendingApprovalAction?.approvalId === approval.id;
|
||||
return (
|
||||
<div id={`approval-${approval.id}`} key={`approval:${approval.id}`} className="py-1.5">
|
||||
<ApprovalCard
|
||||
approval={approval}
|
||||
requesterAgent={approval.requestedByAgentId ? agentMap?.get(approval.requestedByAgentId) ?? null : null}
|
||||
onApprove={onApproveApproval ? () => void onApproveApproval(approval.id) : undefined}
|
||||
onReject={onRejectApproval ? () => void onRejectApproval(approval.id) : undefined}
|
||||
detailLink={`/approvals/${approval.id}`}
|
||||
isPending={isPending}
|
||||
pendingAction={isPending ? pendingApprovalAction?.action ?? null : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind === "run") {
|
||||
const run = item.run;
|
||||
const actorName = agentMap?.get(run.agentId)?.name ?? run.agentId.slice(0, 8);
|
||||
|
|
@ -548,6 +584,7 @@ const TimelineList = memo(function TimelineList({
|
|||
export function CommentThread({
|
||||
comments,
|
||||
queuedComments = [],
|
||||
linkedApprovals = [],
|
||||
feedbackVotes = [],
|
||||
feedbackDataSharingPreference = "prompt",
|
||||
feedbackTermsUrl = null,
|
||||
|
|
@ -555,6 +592,9 @@ export function CommentThread({
|
|||
timelineEvents = [],
|
||||
companyId,
|
||||
projectId,
|
||||
onApproveApproval,
|
||||
onRejectApproval,
|
||||
pendingApprovalAction = null,
|
||||
onVote,
|
||||
onAdd,
|
||||
agentMap,
|
||||
|
|
@ -593,6 +633,12 @@ export function CommentThread({
|
|||
createdAtMs: new Date(comment.createdAt).getTime(),
|
||||
comment,
|
||||
}));
|
||||
const approvalItems: TimelineItem[] = linkedApprovals.map((approval) => ({
|
||||
kind: "approval",
|
||||
id: approval.id,
|
||||
createdAtMs: new Date(approval.createdAt).getTime(),
|
||||
approval,
|
||||
}));
|
||||
const eventItems: TimelineItem[] = timelineEvents.map((event) => ({
|
||||
kind: "event",
|
||||
id: event.id,
|
||||
|
|
@ -605,17 +651,18 @@ export function CommentThread({
|
|||
createdAtMs: new Date(runTimestamp(run)).getTime(),
|
||||
run,
|
||||
}));
|
||||
return [...commentItems, ...eventItems, ...runItems].sort((a, b) => {
|
||||
return [...commentItems, ...approvalItems, ...eventItems, ...runItems].sort((a, b) => {
|
||||
if (a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs;
|
||||
if (a.kind === b.kind) return a.id.localeCompare(b.id);
|
||||
const kindOrder = {
|
||||
event: 0,
|
||||
comment: 1,
|
||||
run: 2,
|
||||
approval: 1,
|
||||
comment: 2,
|
||||
run: 3,
|
||||
} as const;
|
||||
return kindOrder[a.kind] - kindOrder[b.kind];
|
||||
});
|
||||
}, [comments, timelineEvents, linkedRuns]);
|
||||
}, [comments, linkedApprovals, timelineEvents, linkedRuns]);
|
||||
|
||||
const feedbackVoteByTargetId = useMemo(() => {
|
||||
const map = new Map<string, FeedbackVoteValue>();
|
||||
|
|
@ -754,6 +801,9 @@ export function CommentThread({
|
|||
currentUserId={currentUserId}
|
||||
companyId={companyId}
|
||||
projectId={projectId}
|
||||
onApproveApproval={onApproveApproval}
|
||||
onRejectApproval={onRejectApproval}
|
||||
pendingApprovalAction={pendingApprovalAction}
|
||||
feedbackVoteByTargetId={feedbackVoteByTargetId}
|
||||
feedbackDataSharingPreference={feedbackDataSharingPreference}
|
||||
onVote={onVote ? handleFeedbackVote : undefined}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue