feat(app,server): add pull request pane to the workspace sidebar
Introduce a PR tab in the explorer sidebar that shows status checks, reviews, and the merged timeline for the current branch's pull request. Extend checkout PR status with number, draft flag, repo identity, and per-check workflow/duration; add a new pull_request_timeline request/response pair backed by a GraphQL query; enrich the workspace git snapshot to carry the fields through to the client; and extend the checkout-git-actions store so timeline queries are invalidated alongside status when mutations land.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
__resetCheckoutGitActionsStoreForTests,
|
||||
invalidateCheckoutGitQueriesForClient,
|
||||
useCheckoutGitActionsStore,
|
||||
} from "@/stores/checkout-git-actions-store";
|
||||
|
||||
@@ -61,4 +63,30 @@ describe("checkout-git-actions-store", () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle");
|
||||
});
|
||||
|
||||
it("invalidates checkout PR status and every PR pane timeline for a checkout", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
queryClient.setQueryData(["checkoutPrStatus", serverId, cwd], { status: { number: 12 } });
|
||||
queryClient.setQueryData(["prPaneTimeline", serverId, cwd, 12], { items: [] });
|
||||
queryClient.setQueryData(["prPaneTimeline", serverId, cwd, 13], { items: [] });
|
||||
queryClient.setQueryData(["prPaneTimeline", serverId, "/tmp/other", 12], { items: [] });
|
||||
|
||||
await invalidateCheckoutGitQueriesForClient(queryClient, { serverId, cwd });
|
||||
|
||||
expect(queryClient.getQueryState(["checkoutPrStatus", serverId, cwd])?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queryClient.getQueryState(["prPaneTimeline", serverId, cwd, 12])?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queryClient.getQueryState(["prPaneTimeline", serverId, cwd, 13])?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
queryClient.getQueryState(["prPaneTimeline", serverId, "/tmp/other", 12])?.isInvalidated,
|
||||
).toBe(false);
|
||||
|
||||
queryClient.clear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { create } from "zustand";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { queryClient } from "@/query/query-client";
|
||||
import { queryClient as appQueryClient } from "@/query/query-client";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceLayoutStore,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useWorkspaceTabsStore } from "@/stores/workspace-tabs-store";
|
||||
|
||||
const SUCCESS_DISPLAY_MS = 1000;
|
||||
@@ -59,34 +60,54 @@ function setStatus(
|
||||
});
|
||||
}
|
||||
|
||||
export async function invalidateCheckoutGitQueriesForClient(
|
||||
queryClient: QueryClient,
|
||||
{ serverId, cwd }: { serverId: string; cwd: string },
|
||||
) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["checkoutStatus", serverId, cwd],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) && key[0] === "checkoutDiff" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) &&
|
||||
key[0] === "checkoutPrStatus" &&
|
||||
key[1] === serverId &&
|
||||
key[2] === cwd
|
||||
);
|
||||
},
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) && key[0] === "prPaneTimeline" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function invalidateCheckoutGitQueries(serverId: string, cwd: string) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["checkoutStatus", serverId, cwd],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) && key[0] === "checkoutDiff" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) && key[0] === "checkoutPrStatus" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
});
|
||||
return invalidateCheckoutGitQueriesForClient(appQueryClient, { serverId, cwd });
|
||||
}
|
||||
|
||||
function invalidateWorktreeList() {
|
||||
void queryClient.invalidateQueries({
|
||||
void appQueryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
void appQueryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === "sidebarPaseoWorktreeList",
|
||||
});
|
||||
@@ -107,7 +128,7 @@ function removeWorktreeFromCachedLists(input: { serverId: string; worktreePath:
|
||||
return filtered.length === current.length ? current : filtered;
|
||||
};
|
||||
|
||||
queryClient.setQueriesData(
|
||||
appQueryClient.setQueriesData(
|
||||
{
|
||||
predicate: (query) =>
|
||||
Array.isArray(query.queryKey) &&
|
||||
@@ -117,7 +138,7 @@ function removeWorktreeFromCachedLists(input: { serverId: string; worktreePath:
|
||||
removeFromList,
|
||||
);
|
||||
|
||||
queryClient.setQueriesData(
|
||||
appQueryClient.setQueriesData(
|
||||
{
|
||||
predicate: (query) =>
|
||||
Array.isArray(query.queryKey) &&
|
||||
@@ -201,7 +222,7 @@ async function runCheckoutAction({
|
||||
const promise = (async () => {
|
||||
try {
|
||||
await run();
|
||||
invalidateCheckoutGitQueries(serverId, cwd);
|
||||
await invalidateCheckoutGitQueries(serverId, cwd);
|
||||
setStatus(key, actionId, "success");
|
||||
const timer = setTimeout(() => {
|
||||
setStatus(key, actionId, "idle");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type ExplorerTab = "changes" | "files";
|
||||
export type ExplorerTab = "changes" | "files" | "pr";
|
||||
|
||||
export function isExplorerTab(value: unknown): value is ExplorerTab {
|
||||
return value === "changes" || value === "files";
|
||||
return value === "changes" || value === "files" || value === "pr";
|
||||
}
|
||||
|
||||
export function buildExplorerCheckoutKey(serverId: string, cwd: string): string | null {
|
||||
|
||||
Reference in New Issue
Block a user