+ {STATE_TABS.map((tab) => (
+ // biome-ignore lint/a11y/useSemanticElements: styled as a pill button, not a native radio input
+
+ ))}
{/* Window-drag leaf standing in for the hidden TopBar. */}
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Repository
+
+
+
+ Author
+
+
+
+ Reviews
+
+
+
+
);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/AuthorFilter/AuthorFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/AuthorFilter/AuthorFilter.tsx
index bcd0c8ce61f..5ed3a752394 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/AuthorFilter/AuthorFilter.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/AuthorFilter/AuthorFilter.tsx
@@ -1,44 +1,84 @@
+import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar";
import { Button } from "@superset/ui/button";
-import { Input } from "@superset/ui/input";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@superset/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover";
-import { type FormEvent, useId, useState } from "react";
-import { HiChevronDown, HiOutlineUserCircle } from "react-icons/hi2";
+import { useQuery } from "@tanstack/react-query";
+import { useMemo, useState } from "react";
+import { HiCheck, HiChevronDown, HiOutlineUserCircle } from "react-icons/hi2";
+import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
+import type { ProjectQueryTarget } from "renderer/routes/_authenticated/_dashboard/hooks/useProjectQueryTargets";
import { normalizeAuthorFilter } from "renderer/routes/_authenticated/_dashboard/pull-requests/utils/normalizeAuthorFilter";
interface AuthorFilterProps {
value: string | null;
onChange: (value: string | null) => void;
+ /** Scoped to a single repo, this drives a live contributor list instead
+ * of a bare text box — "all repositories" or multiple selected repos
+ * have no single contributor set to show. */
+ projectTargets: ProjectQueryTarget[];
}
-export function AuthorFilter({ value, onChange }: AuthorFilterProps) {
+export function AuthorFilter({
+ value,
+ onChange,
+ projectTargets,
+}: AuthorFilterProps) {
const [open, setOpen] = useState(false);
- const [draft, setDraft] = useState(value ?? "");
- const [error, setError] = useState(null);
- const inputId = useId();
- const errorId = useId();
+ const [search, setSearch] = useState("");
const label = value ? `@${value}` : "All authors";
+ const singleTarget =
+ projectTargets.length === 1 ? projectTargets[0] : undefined;
+
+ const {
+ data: contributors,
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: [
+ "pullRequests",
+ "repoContributors",
+ singleTarget?.projectId,
+ singleTarget?.hostUrl,
+ ],
+ queryFn: async () => {
+ if (!singleTarget?.hostUrl) return [];
+ const client = getHostServiceClientByUrl(singleTarget.hostUrl);
+ return client.workspaceCreation.getRepoContributors.query({
+ projectId: singleTarget.projectId,
+ });
+ },
+ enabled: !!singleTarget?.hostUrl,
+ staleTime: 5 * 60_000,
+ gcTime: 10 * 60_000,
+ });
+
+ const filtered = useMemo(() => {
+ const q = search.trim().replace(/^@/, "").toLowerCase();
+ const list = contributors ?? [];
+ if (!q) return list;
+ return list.filter((c) => c.login.toLowerCase().includes(q));
+ }, [contributors, search]);
+
+ const normalizedSearch = normalizeAuthorFilter(search)?.toLowerCase() ?? null;
+ const showCustomOption =
+ !!normalizedSearch &&
+ !filtered.some((c) => c.login.toLowerCase() === normalizedSearch);
+
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen);
- setDraft(value ?? "");
- setError(null);
+ if (!nextOpen) setSearch("");
};
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault();
- const normalized = normalizeAuthorFilter(draft);
- if (draft.trim() && !normalized) {
- setError("Enter a valid GitHub username.");
- return;
- }
- onChange(normalized);
- setOpen(false);
- };
-
- const handleClear = () => {
- onChange(null);
- setDraft("");
- setError(null);
+ const handleSelect = (login: string | null) => {
+ onChange(login);
setOpen(false);
};
@@ -53,52 +93,83 @@ export function AuthorFilter({ value, onChange }: AuthorFilterProps) {
className="h-8 max-w-44 gap-1.5 px-2 text-muted-foreground hover:text-foreground"
>
- {label}
+ {label}
-
-
+ {!isLoading &&
+ !error &&
+ filtered.length === 0 &&
+ !showCustomOption &&
+ (!singleTarget ||
+ (contributors && contributors.length === 0)) && (
+
+ {search ? "No authors found." : "No contributors found."}
+
+ )}
+
+
);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/ReviewFilter/ReviewFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/ReviewFilter/ReviewFilter.tsx
index f2c114a4840..63aebb1233e 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/ReviewFilter/ReviewFilter.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestsView/components/PullRequestsTopBar/components/ReviewFilter/ReviewFilter.tsx
@@ -37,7 +37,7 @@ export function ReviewFilter({ value, onChange }: ReviewFilterProps) {
className="h-8 max-w-52 gap-1.5 px-2 text-muted-foreground hover:text-foreground"
>
- {label}
+ {label}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/layout.tsx
index 7c5cb29c679..188aad95493 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/layout.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/layout.tsx
@@ -1,4 +1,18 @@
-import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { cn } from "@superset/ui/utils";
+import { createFileRoute, Outlet, useParams } from "@tanstack/react-router";
+import { useMemo } from "react";
+import { resolveProjectFilterParams } from "renderer/routes/_authenticated/_dashboard/components/ProjectFilter/project-filter-utils";
+import { parsePositiveIntegerParam } from "renderer/routes/_authenticated/_dashboard/utils/parsePositiveIntegerParam";
+import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel";
+import { useWorkspaceSidebarStore } from "renderer/stores/workspace-sidebar-state";
+import { PullRequestListToggle } from "./components/PullRequestListToggle";
+import { PullRequestsView } from "./components/PullRequestsView";
+import {
+ DEFAULT_PULL_REQUESTS_LIST_WIDTH,
+ MAX_PULL_REQUESTS_LIST_WIDTH,
+ MIN_PULL_REQUESTS_LIST_WIDTH,
+ usePullRequestsSplitViewStore,
+} from "./stores/pullRequestsSplitViewStore";
export type PullRequestsSearch = {
search?: string;
@@ -6,7 +20,7 @@ export type PullRequestsSearch = {
projects?: string;
author?: string;
review?: string;
- state?: "open" | "all";
+ state?: "open" | "all" | "merged";
};
export const Route = createFileRoute(
@@ -19,12 +33,104 @@ export const Route = createFileRoute(
projects: typeof search.projects === "string" ? search.projects : undefined,
author: typeof search.author === "string" ? search.author : undefined,
review: typeof search.review === "string" ? search.review : undefined,
- state: ["open", "all"].includes(search.state as string)
+ state: ["open", "all", "merged"].includes(search.state as string)
? (search.state as PullRequestsSearch["state"])
: undefined,
}),
});
+/**
+ * Split view, per Figma (SuperReviewSplit): the list stays mounted in a
+ * resizable left pane while the child route (index = empty state,
+ * $prNumber = detail) renders in the flexible right pane via ``.
+ * Selecting a different PR updates the right pane only — the list never
+ * unmounts, so scroll position and in-flight pagination survive. Either
+ * pane can be collapsed to give the other the full width; collapsing one
+ * always reveals the other, since hiding both would leave nothing on screen.
+ */
function PullRequestsLayout() {
- return ;
+ const { search, project, projects, author, review, state } =
+ Route.useSearch();
+ const params = useParams({ strict: false }) as { prNumber?: string };
+ const selectedPrNumber = params.prNumber
+ ? parsePositiveIntegerParam(params.prNumber)
+ : null;
+ const isListCollapsed = usePullRequestsSplitViewStore(
+ (s) => s.isListCollapsed,
+ );
+ const isDetailCollapsed = usePullRequestsSplitViewStore(
+ (s) => s.isDetailCollapsed,
+ );
+ const listWidth = usePullRequestsSplitViewStore((s) => s.width);
+ const setListWidth = usePullRequestsSplitViewStore((s) => s.setWidth);
+ const isResizingList = usePullRequestsSplitViewStore((s) => s.isResizing);
+ const setIsResizingList = usePullRequestsSplitViewStore(
+ (s) => s.setIsResizing,
+ );
+ const isAppSidebarCollapsed = useWorkspaceSidebarStore((s) =>
+ s.isCollapsed(),
+ );
+ // Stable identity: effects downstream key off this array.
+ const initialProjects = useMemo(
+ () => resolveProjectFilterParams(projects, project, undefined),
+ [projects, project],
+ );
+
+ const listContent = (
+
+ );
+
+ return (
+