mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
Speed up issue detail comments and refreshes
This commit is contained in:
parent
a4b05d8831
commit
9e8cd28f81
4 changed files with 276 additions and 77 deletions
|
|
@ -82,7 +82,21 @@ export const issuesApi = {
|
||||||
expectedStatuses: ["todo", "backlog", "blocked", "in_review"],
|
expectedStatuses: ["todo", "backlog", "blocked", "in_review"],
|
||||||
}),
|
}),
|
||||||
release: (id: string) => api.post<Issue>(`/issues/${id}/release`, {}),
|
release: (id: string) => api.post<Issue>(`/issues/${id}/release`, {}),
|
||||||
listComments: (id: string) => api.get<IssueComment[]>(`/issues/${id}/comments`),
|
listComments: (
|
||||||
|
id: string,
|
||||||
|
filters?: {
|
||||||
|
after?: string;
|
||||||
|
order?: "asc" | "desc";
|
||||||
|
limit?: number;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters?.after) params.set("after", filters.after);
|
||||||
|
if (filters?.order) params.set("order", filters.order);
|
||||||
|
if (filters?.limit) params.set("limit", String(filters.limit));
|
||||||
|
const qs = params.toString();
|
||||||
|
return api.get<IssueComment[]>(`/issues/${id}/comments${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
listFeedbackVotes: (id: string) => api.get<FeedbackVote[]>(`/issues/${id}/feedback-votes`),
|
listFeedbackVotes: (id: string) => api.get<FeedbackVote[]>(`/issues/${id}/feedback-votes`),
|
||||||
listFeedbackTraces: (id: string, filters?: Record<string, string | boolean | undefined>) => {
|
listFeedbackTraces: (id: string, filters?: Record<string, string | boolean | undefined>) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import {
|
||||||
applyOptimisticIssueFieldUpdateToCollection,
|
applyOptimisticIssueFieldUpdateToCollection,
|
||||||
applyOptimisticIssueCommentUpdate,
|
applyOptimisticIssueCommentUpdate,
|
||||||
createOptimisticIssueComment,
|
createOptimisticIssueComment,
|
||||||
|
flattenIssueCommentPages,
|
||||||
isQueuedIssueComment,
|
isQueuedIssueComment,
|
||||||
matchesIssueRef,
|
matchesIssueRef,
|
||||||
mergeIssueComments,
|
mergeIssueComments,
|
||||||
upsertIssueComment,
|
upsertIssueComment,
|
||||||
|
upsertIssueCommentInPages,
|
||||||
} from "./optimistic-issue-comments";
|
} from "./optimistic-issue-comments";
|
||||||
|
|
||||||
describe("optimistic issue comments", () => {
|
describe("optimistic issue comments", () => {
|
||||||
|
|
@ -128,6 +130,91 @@ describe("optimistic issue comments", () => {
|
||||||
expect(next[0]?.body).toBe("Updated");
|
expect(next[0]?.body).toBe("Updated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flattens paged comments into one chronological thread", () => {
|
||||||
|
const flattened = flattenIssueCommentPages([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "comment-3",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Newest",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:03.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:03.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "comment-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Oldest",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:01.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:01.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comment-2",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Middle",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:02.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:02.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(flattened.map((comment) => comment.id)).toEqual(["comment-1", "comment-2", "comment-3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upserts paged comments without dropping older pages", () => {
|
||||||
|
const nextPages = upsertIssueCommentInPages(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "comment-3",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Newest",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:03.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:03.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "comment-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Oldest",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:01.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:01.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
id: "comment-4",
|
||||||
|
companyId: "company-1",
|
||||||
|
issueId: "issue-1",
|
||||||
|
authorAgentId: null,
|
||||||
|
authorUserId: "board-1",
|
||||||
|
body: "Brand new",
|
||||||
|
createdAt: new Date("2026-03-28T14:00:04.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-28T14:00:04.000Z"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nextPages[0]?.map((comment) => comment.id)).toEqual(["comment-4", "comment-3"]);
|
||||||
|
expect(nextPages[1]?.map((comment) => comment.id)).toEqual(["comment-1"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("applies optimistic reopen and reassignment updates to the issue cache", () => {
|
it("applies optimistic reopen and reassignment updates to the issue cache", () => {
|
||||||
const next = applyOptimisticIssueCommentUpdate(
|
const next = applyOptimisticIssueCommentUpdate(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ export function sortIssueComments<T extends { createdAt: Date | string; id: stri
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortIssueCommentsDesc<T extends { createdAt: Date | string; id: string }>(comments: T[]) {
|
||||||
|
return sortIssueComments(comments).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
export function createOptimisticIssueComment(params: {
|
export function createOptimisticIssueComment(params: {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
|
@ -92,6 +96,12 @@ export function mergeIssueComments(
|
||||||
return sortIssueComments(merged);
|
return sortIssueComments(merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenIssueCommentPages(
|
||||||
|
pages: ReadonlyArray<ReadonlyArray<IssueComment>> | undefined,
|
||||||
|
): IssueComment[] {
|
||||||
|
return sortIssueComments((pages ?? []).flatMap((page) => page));
|
||||||
|
}
|
||||||
|
|
||||||
export function upsertIssueComment(
|
export function upsertIssueComment(
|
||||||
comments: IssueComment[] | undefined,
|
comments: IssueComment[] | undefined,
|
||||||
nextComment: IssueComment,
|
nextComment: IssueComment,
|
||||||
|
|
@ -210,3 +220,24 @@ export function applyOptimisticIssueFieldUpdateToCollection(
|
||||||
|
|
||||||
return changed ? nextIssues : issues;
|
return changed ? nextIssues : issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function upsertIssueCommentInPages(
|
||||||
|
pages: ReadonlyArray<ReadonlyArray<IssueComment>> | undefined,
|
||||||
|
nextComment: IssueComment,
|
||||||
|
): IssueComment[][] {
|
||||||
|
if (!pages || pages.length === 0) {
|
||||||
|
return [[nextComment]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPages = pages.map((page) => [...page]);
|
||||||
|
for (let pageIndex = 0; pageIndex < nextPages.length; pageIndex += 1) {
|
||||||
|
const existingIndex = nextPages[pageIndex]!.findIndex((comment) => comment.id === nextComment.id);
|
||||||
|
if (existingIndex === -1) continue;
|
||||||
|
nextPages[pageIndex]![existingIndex] = nextComment;
|
||||||
|
nextPages[pageIndex] = sortIssueCommentsDesc(nextPages[pageIndex]!);
|
||||||
|
return nextPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPages[0] = sortIssueCommentsDesc([...nextPages[0]!, nextComment]);
|
||||||
|
return nextPages;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
||||||
import { pickTextColorForPillBg } from "@/lib/color-contrast";
|
import { pickTextColorForPillBg } from "@/lib/color-contrast";
|
||||||
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
|
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQuery, useMutation, useQueryClient, type InfiniteData } from "@tanstack/react-query";
|
||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
import { approvalsApi } from "../api/approvals";
|
import { approvalsApi } from "../api/approvals";
|
||||||
import { activityApi, type RunForIssue } from "../api/activity";
|
import { activityApi, type RunForIssue } from "../api/activity";
|
||||||
|
|
@ -35,10 +35,11 @@ import {
|
||||||
applyOptimisticIssueFieldUpdateToCollection,
|
applyOptimisticIssueFieldUpdateToCollection,
|
||||||
applyOptimisticIssueCommentUpdate,
|
applyOptimisticIssueCommentUpdate,
|
||||||
createOptimisticIssueComment,
|
createOptimisticIssueComment,
|
||||||
|
flattenIssueCommentPages,
|
||||||
isQueuedIssueComment,
|
isQueuedIssueComment,
|
||||||
matchesIssueRef,
|
matchesIssueRef,
|
||||||
mergeIssueComments,
|
mergeIssueComments,
|
||||||
upsertIssueComment,
|
upsertIssueCommentInPages,
|
||||||
type IssueCommentReassignment,
|
type IssueCommentReassignment,
|
||||||
type OptimisticIssueComment,
|
type OptimisticIssueComment,
|
||||||
} from "../lib/optimistic-issue-comments";
|
} from "../lib/optimistic-issue-comments";
|
||||||
|
|
@ -134,6 +135,11 @@ const ACTION_LABELS: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos";
|
const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos";
|
||||||
|
const ISSUE_COMMENT_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
function keepPreviousData<T>(previousData: T | undefined) {
|
||||||
|
return previousData;
|
||||||
|
}
|
||||||
|
|
||||||
function humanizeValue(value: unknown): string {
|
function humanizeValue(value: unknown): string {
|
||||||
if (typeof value !== "string") return String(value ?? "none");
|
if (typeof value !== "string") return String(value ?? "none");
|
||||||
|
|
@ -393,28 +399,50 @@ export function IssueDetail() {
|
||||||
return getClosedIsolatedExecutionWorkspaceMessage(issue.currentExecutionWorkspace);
|
return getClosedIsolatedExecutionWorkspaceMessage(issue.currentExecutionWorkspace);
|
||||||
}, [issue?.currentExecutionWorkspace]);
|
}, [issue?.currentExecutionWorkspace]);
|
||||||
|
|
||||||
const { data: comments, isLoading: commentsLoading } = useQuery({
|
const {
|
||||||
|
data: commentPages,
|
||||||
|
isLoading: commentsLoading,
|
||||||
|
isFetchingNextPage: commentsLoadingOlder,
|
||||||
|
hasNextPage: hasOlderComments,
|
||||||
|
fetchNextPage: fetchOlderComments,
|
||||||
|
} = useInfiniteQuery({
|
||||||
queryKey: queryKeys.issues.comments(issueId!),
|
queryKey: queryKeys.issues.comments(issueId!),
|
||||||
queryFn: () => issuesApi.listComments(issueId!),
|
queryFn: ({ pageParam }) =>
|
||||||
|
issuesApi.listComments(issueId!, {
|
||||||
|
order: "desc",
|
||||||
|
limit: ISSUE_COMMENT_PAGE_SIZE,
|
||||||
|
...(pageParam ? { after: pageParam } : {}),
|
||||||
|
}),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
|
initialPageParam: null as string | null,
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.length === ISSUE_COMMENT_PAGE_SIZE ? lastPage[lastPage.length - 1]?.id : undefined,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
const comments = useMemo(
|
||||||
|
() => flattenIssueCommentPages(commentPages?.pages),
|
||||||
|
[commentPages?.pages],
|
||||||
|
);
|
||||||
|
|
||||||
const { data: activity, isLoading: activityLoading } = useQuery({
|
const { data: activity, isLoading: activityLoading } = useQuery({
|
||||||
queryKey: queryKeys.issues.activity(issueId!),
|
queryKey: queryKeys.issues.activity(issueId!),
|
||||||
queryFn: () => activityApi.forIssue(issueId!),
|
queryFn: () => activityApi.forIssue(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: linkedApprovals } = useQuery({
|
const { data: linkedApprovals } = useQuery({
|
||||||
queryKey: queryKeys.issues.approvals(issueId!),
|
queryKey: queryKeys.issues.approvals(issueId!),
|
||||||
queryFn: () => issuesApi.listApprovals(issueId!),
|
queryFn: () => issuesApi.listApprovals(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: attachments, isLoading: attachmentsLoading } = useQuery({
|
const { data: attachments, isLoading: attachmentsLoading } = useQuery({
|
||||||
queryKey: queryKeys.issues.attachments(issueId!),
|
queryKey: queryKeys.issues.attachments(issueId!),
|
||||||
queryFn: () => issuesApi.listAttachments(issueId!),
|
queryFn: () => issuesApi.listAttachments(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: liveRuns, isLoading: liveRunsLoading } = useQuery({
|
const { data: liveRuns, isLoading: liveRunsLoading } = useQuery({
|
||||||
|
|
@ -427,26 +455,31 @@ export function IssueDetail() {
|
||||||
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
||||||
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS;
|
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS;
|
||||||
},
|
},
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: activeRun, isLoading: activeRunLoading } = useQuery({
|
const { data: activeRun, isLoading: activeRunLoading } = useQuery({
|
||||||
queryKey: queryKeys.issues.activeRun(issueId!),
|
queryKey: queryKeys.issues.activeRun(issueId!),
|
||||||
queryFn: () => heartbeatsApi.activeRunForIssue(issueId!),
|
queryFn: () => heartbeatsApi.activeRunForIssue(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId && (!!issue?.executionRunId || issue?.status === "in_progress"),
|
||||||
refetchInterval: (query) =>
|
refetchInterval: (query) =>
|
||||||
query.state.data
|
(liveRuns?.length ?? 0) > 0
|
||||||
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
? false
|
||||||
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS,
|
: query.state.data
|
||||||
|
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
||||||
|
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
||||||
const { data: linkedRuns } = useQuery({
|
const { data: linkedRuns, isLoading: linkedRunsLoading } = useQuery({
|
||||||
queryKey: queryKeys.issues.runs(issueId!),
|
queryKey: queryKeys.issues.runs(issueId!),
|
||||||
queryFn: () => activityApi.runsForIssue(issueId!),
|
queryFn: () => activityApi.runsForIssue(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
refetchInterval: hasLiveRuns
|
refetchInterval: hasLiveRuns
|
||||||
? ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS
|
? ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS
|
||||||
: IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS,
|
: IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
const runningIssueRun = useMemo(
|
const runningIssueRun = useMemo(
|
||||||
() => (
|
() => (
|
||||||
|
|
@ -481,6 +514,7 @@ export function IssueDetail() {
|
||||||
: ["issues", "parent", "pending"],
|
: ["issues", "parent", "pending"],
|
||||||
queryFn: () => issuesApi.list(resolvedCompanyId!, { parentId: issue!.id }),
|
queryFn: () => issuesApi.list(resolvedCompanyId!, { parentId: issue!.id }),
|
||||||
enabled: !!resolvedCompanyId && !!issue?.id,
|
enabled: !!resolvedCompanyId && !!issue?.id,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: agents } = useQuery({
|
const { data: agents } = useQuery({
|
||||||
|
|
@ -734,26 +768,18 @@ export function IssueDetail() {
|
||||||
};
|
};
|
||||||
}, [linkedRuns]);
|
}, [linkedRuns]);
|
||||||
|
|
||||||
const invalidateIssue = () => {
|
const invalidateIssueDetail = useCallback(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(issueId!) });
|
||||||
|
}, [issueId, queryClient]);
|
||||||
|
|
||||||
|
const invalidateIssueRunState = useCallback(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(issueId!) });
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.approvals(issueId!) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.feedbackVotes(issueId!) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(issueId!) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId!) });
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId!) });
|
||||||
if (selectedCompanyId) {
|
}, [issueId, queryClient]);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidateIssueCollections = () => {
|
const invalidateIssueCollections = useCallback(() => {
|
||||||
if (selectedCompanyId) {
|
if (selectedCompanyId) {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
||||||
|
|
@ -761,7 +787,7 @@ export function IssueDetail() {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
||||||
}
|
}
|
||||||
};
|
}, [queryClient, selectedCompanyId]);
|
||||||
|
|
||||||
const applyOptimisticIssueCacheUpdate = useCallback((refs: Iterable<string>, data: Record<string, unknown>) => {
|
const applyOptimisticIssueCacheUpdate = useCallback((refs: Iterable<string>, data: Record<string, unknown>) => {
|
||||||
queryClient.setQueriesData<Issue>(
|
queryClient.setQueriesData<Issue>(
|
||||||
|
|
@ -867,7 +893,9 @@ export function IssueDetail() {
|
||||||
setPendingApprovalAction({ approvalId, action });
|
setPendingApprovalAction({ approvalId, action });
|
||||||
},
|
},
|
||||||
onSuccess: (_approval, variables) => {
|
onSuccess: (_approval, variables) => {
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.approvals(issueId!) });
|
||||||
|
invalidateIssueCollections();
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.detail(variables.approvalId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.detail(variables.approvalId) });
|
||||||
if (resolvedCompanyId) {
|
if (resolvedCompanyId) {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(resolvedCompanyId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(resolvedCompanyId) });
|
||||||
|
|
@ -930,9 +958,15 @@ export function IssueDetail() {
|
||||||
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
queryClient.setQueryData<IssueComment[]>(
|
queryClient.setQueryData<InfiniteData<IssueComment[], string | null>>(
|
||||||
queryKeys.issues.comments(issueId!),
|
queryKeys.issues.comments(issueId!),
|
||||||
(current) => upsertIssueComment(current, comment),
|
(current) => current ? {
|
||||||
|
...current,
|
||||||
|
pages: upsertIssueCommentInPages(current.pages, comment),
|
||||||
|
} : {
|
||||||
|
pageParams: [null],
|
||||||
|
pages: upsertIssueCommentInPages(undefined, comment),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: (err, _variables, context) => {
|
onError: (err, _variables, context) => {
|
||||||
|
|
@ -950,9 +984,14 @@ export function IssueDetail() {
|
||||||
tone: "error",
|
tone: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: (_result, _error, variables) => {
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
if (variables.interrupt) {
|
||||||
|
invalidateIssueRunState();
|
||||||
|
}
|
||||||
|
if (variables.reopen) {
|
||||||
|
invalidateIssueCollections();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1017,9 +1056,15 @@ export function IssueDetail() {
|
||||||
const { comment, ...nextIssue } = result;
|
const { comment, ...nextIssue } = result;
|
||||||
queryClient.setQueryData(queryKeys.issues.detail(issueId!), nextIssue);
|
queryClient.setQueryData(queryKeys.issues.detail(issueId!), nextIssue);
|
||||||
if (comment) {
|
if (comment) {
|
||||||
queryClient.setQueryData<IssueComment[]>(
|
queryClient.setQueryData<InfiniteData<IssueComment[], string | null>>(
|
||||||
queryKeys.issues.comments(issueId!),
|
queryKeys.issues.comments(issueId!),
|
||||||
(current) => upsertIssueComment(current, comment),
|
(current) => current ? {
|
||||||
|
...current,
|
||||||
|
pages: upsertIssueCommentInPages(current.pages, comment),
|
||||||
|
} : {
|
||||||
|
pageParams: [null],
|
||||||
|
pages: upsertIssueCommentInPages(undefined, comment),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1038,9 +1083,12 @@ export function IssueDetail() {
|
||||||
tone: "error",
|
tone: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: (_result, _error, variables) => {
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
if (variables.interrupt) {
|
||||||
|
invalidateIssueRunState();
|
||||||
|
}
|
||||||
|
invalidateIssueCollections();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1085,7 +1133,8 @@ export function IssueDetail() {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
|
invalidateIssueRunState();
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Interrupt requested",
|
title: "Interrupt requested",
|
||||||
body: "The active run is stopping so queued comments can continue next.",
|
body: "The active run is stopping so queued comments can continue next.",
|
||||||
|
|
@ -1177,7 +1226,7 @@ export function IssueDetail() {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setAttachmentError(null);
|
setAttachmentError(null);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setAttachmentError(err instanceof Error ? err.message : "Upload failed");
|
setAttachmentError(err instanceof Error ? err.message : "Upload failed");
|
||||||
|
|
@ -1201,7 +1250,8 @@ export function IssueDetail() {
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setAttachmentError(null);
|
setAttachmentError(null);
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(issueId!) });
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setAttachmentError(err instanceof Error ? err.message : "Document import failed");
|
setAttachmentError(err instanceof Error ? err.message : "Document import failed");
|
||||||
|
|
@ -1213,7 +1263,7 @@ export function IssueDetail() {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setAttachmentError(null);
|
setAttachmentError(null);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
||||||
invalidateIssue();
|
invalidateIssueDetail();
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setAttachmentError(err instanceof Error ? err.message : "Delete failed");
|
setAttachmentError(err instanceof Error ? err.message : "Delete failed");
|
||||||
|
|
@ -1223,7 +1273,7 @@ export function IssueDetail() {
|
||||||
const archiveFromInbox = useMutation({
|
const archiveFromInbox = useMutation({
|
||||||
mutationFn: (id: string) => issuesApi.archiveFromInbox(id),
|
mutationFn: (id: string) => issuesApi.archiveFromInbox(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateIssue();
|
invalidateIssueCollections();
|
||||||
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
|
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
|
||||||
pushToast({ title: "Issue archived from inbox", tone: "success" });
|
pushToast({ title: "Issue archived from inbox", tone: "success" });
|
||||||
},
|
},
|
||||||
|
|
@ -1504,7 +1554,7 @@ export function IssueDetail() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueChatInitialLoading =
|
const issueChatInitialLoading =
|
||||||
(commentsLoading && comments === undefined)
|
(commentsLoading && commentPages === undefined)
|
||||||
|| (activityLoading && activity === undefined)
|
|| (activityLoading && activity === undefined)
|
||||||
|| (linkedRunsLoading && linkedRuns === undefined)
|
|| (linkedRunsLoading && linkedRuns === undefined)
|
||||||
|| (liveRunsLoading && liveRuns === undefined)
|
|| (liveRunsLoading && liveRuns === undefined)
|
||||||
|
|
@ -2067,41 +2117,58 @@ export function IssueDetail() {
|
||||||
{issueChatInitialLoading ? (
|
{issueChatInitialLoading ? (
|
||||||
<IssueChatSkeleton />
|
<IssueChatSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<IssueChatThread
|
<div className="space-y-3">
|
||||||
composerRef={commentComposerRef}
|
{hasOlderComments ? (
|
||||||
comments={commentsWithRunMeta}
|
<div className="flex justify-center">
|
||||||
feedbackVotes={feedbackVotes}
|
<Button
|
||||||
feedbackDataSharingPreference={feedbackDataSharingPreference}
|
type="button"
|
||||||
feedbackTermsUrl={FEEDBACK_TERMS_URL}
|
variant="outline"
|
||||||
linkedRuns={timelineRuns}
|
size="sm"
|
||||||
timelineEvents={timelineEvents}
|
disabled={commentsLoadingOlder}
|
||||||
liveRuns={liveRuns}
|
onClick={() => {
|
||||||
activeRun={activeRun}
|
void fetchOlderComments();
|
||||||
companyId={issue.companyId}
|
}}
|
||||||
projectId={issue.projectId}
|
>
|
||||||
issueStatus={issue.status}
|
{commentsLoadingOlder ? "Loading earlier comments..." : "Load earlier comments"}
|
||||||
agentMap={agentMap}
|
</Button>
|
||||||
currentUserId={currentUserId}
|
</div>
|
||||||
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
) : null}
|
||||||
enableReassign
|
<IssueChatThread
|
||||||
reassignOptions={commentReassignOptions}
|
composerRef={commentComposerRef}
|
||||||
currentAssigneeValue={actualAssigneeValue}
|
comments={commentsWithRunMeta}
|
||||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
feedbackVotes={feedbackVotes}
|
||||||
mentions={mentionOptions}
|
feedbackDataSharingPreference={feedbackDataSharingPreference}
|
||||||
onInterruptQueued={handleInterruptQueued}
|
feedbackTermsUrl={FEEDBACK_TERMS_URL}
|
||||||
interruptingQueuedRunId={interruptQueuedComment.isPending ? interruptQueuedComment.variables ?? null : null}
|
linkedRuns={timelineRuns}
|
||||||
composerDisabledReason={commentComposerDisabledReason}
|
timelineEvents={timelineEvents}
|
||||||
onVote={handleCommentVote}
|
liveRuns={liveRuns}
|
||||||
onAdd={handleCommentAdd}
|
activeRun={activeRun}
|
||||||
imageUploadHandler={handleCommentImageUpload}
|
companyId={issue.companyId}
|
||||||
onAttachImage={handleCommentAttachImage}
|
projectId={issue.projectId}
|
||||||
onCancelRun={runningIssueRun
|
issueStatus={issue.status}
|
||||||
? async () => {
|
agentMap={agentMap}
|
||||||
await interruptQueuedComment.mutateAsync(runningIssueRun.id);
|
currentUserId={currentUserId}
|
||||||
}
|
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
||||||
: undefined}
|
enableReassign
|
||||||
onImageClick={handleChatImageClick}
|
reassignOptions={commentReassignOptions}
|
||||||
/>
|
currentAssigneeValue={actualAssigneeValue}
|
||||||
|
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||||
|
mentions={mentionOptions}
|
||||||
|
onInterruptQueued={handleInterruptQueued}
|
||||||
|
interruptingQueuedRunId={interruptQueuedComment.isPending ? interruptQueuedComment.variables ?? null : null}
|
||||||
|
composerDisabledReason={commentComposerDisabledReason}
|
||||||
|
onVote={handleCommentVote}
|
||||||
|
onAdd={handleCommentAdd}
|
||||||
|
imageUploadHandler={handleCommentImageUpload}
|
||||||
|
onAttachImage={handleCommentAttachImage}
|
||||||
|
onCancelRun={runningIssueRun
|
||||||
|
? async () => {
|
||||||
|
await interruptQueuedComment.mutateAsync(runningIssueRun.id);
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
onImageClick={handleChatImageClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue