fix(ui): simplify GitHub repository access controls (#13047)

## Thinking Path

> - Paperclip helps people manage AI agents and their connected apps.
> - GitHub permissions show which repositories an account can access.
> - The page adds an account dropdown and search above the repository
list.
> - These controls add clutter to a view meant to show the full access
list.
> - This change removes both controls and their filtering state.
> - People see all repositories directly, with the existing scrolling
and access controls.

## Linked Issues or Issue Description

Refs #12998.

**Current behavior**

GitHub permissions show an “All accounts” dropdown and a “Search
repositories” input above the repository list.

**Proposed behavior**

Show the full repository list directly. Keep the scroll limit,
repository links, private-repository icons, refresh button, and GitHub
configuration links.

**Reason and benefit**

Remove unnecessary controls from the access summary. Users manage
repository permissions on GitHub.

## What Changed

- Remove the account dropdown, search input, filtering state, and unused
imports.
- Rename the GitHub configuration button to “Add More Repos on GitHub”.
- Render every returned repository and simplify the empty-list message.
- Update the existing multi-account test and permissions documentation.

## Verification

- All 55 AppDetail tests passed.
- `pnpm check:token-gates` passed.
- Full `pnpm build` and `pnpm -r typecheck` passed. All 31 CI checks
passed on `cb9b8bb14a880d926393c62821265fbd2d8c5d31`. Storybook visual
regression was correctly skipped.
- Fresh Greptile review on the same commit: 5/5, with no unresolved
findings.

## Risks

Users can no longer narrow this list by account or search text. The list
still scrolls and shows each repository’s full owner/name. This change
does not alter GitHub permissions or credential selection.

## Model Used

OpenAI GPT-6 through Codex, with code editing and shell verification
tools. The exact model variant and context-window size are not exposed
in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-09-08 12:28:23 -05:00
1 parent be6bb768b1
commit db85bf4b7a
3 files changed
+20 -71

No files matched your search

+13 -30
View File
@@ -1508,19 +1508,19 @@ describe("AppDetail", () => {
await act(async () => { findButton("Load GitHub configuration")!.click(); });
await flushReact();
expect(checkConnectionHealthMock).toHaveBeenCalledWith("conn-1");
expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Configure on GitHub");
expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Add More Repos on GitHub");
expect(findButton("Load GitHub configuration")).toBeUndefined();
});
it("filters the combined GitHub repository list by owner and search without changing access", async () => {
it.each([false, true])("shows repositories across accounts without filter controls (empty: %s)", async (empty) => {
mockParams.tab = "permissions";
getConnectionMock.mockResolvedValue(perUserConnection());
listConnectionGrantsMock.mockResolvedValue({
connection: { id: "conn-1", uid: "conn-1" },
grants: [dedicatedGitHubGrant({ kind: "user", subjectAgentId: null, subjectUserId: "user-1" }, {
repositoryCount: 3,
repositoryCount: empty ? 0 : 3,
installationOwnerLogins: ["paperclipai", "dottabot", "empty-org"],
repositories: [
repositories: empty ? [] : [
{ id: "1", fullName: "paperclipai/first", installationId: "456" },
{ id: "2", fullName: "paperclipai/second", installationId: "456" },
{ id: "3", fullName: "dottabot/first", installationId: "789" },
@@ -1530,31 +1530,14 @@ describe("AppDetail", () => {
});
await renderAppDetail();
const repositoryNames = () => [...container.querySelectorAll('ul[aria-label="Accessible GitHub repositories"] a')].map((link) => link.textContent);
const selectOwner = async (label: string) => {
await act(async () => {
container.querySelector('[role="combobox"][aria-label="Filter repositories by account or organization"]')!
.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
});
const option = [...document.querySelectorAll('[role="option"]')].find((item) => item.textContent === label);
expect(option).toBeTruthy();
await act(async () => {
option!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
});
};
expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second", "dottabot/first"]);
await selectOwner("paperclipai");
expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second"]);
const search = container.querySelector<HTMLInputElement>('input[aria-label="Search GitHub repositories"]')!;
await act(async () => { setInputValue(search, "FIRST"); });
expect(repositoryNames()).toEqual(["paperclipai/first"]);
await selectOwner("All accounts");
expect(repositoryNames()).toEqual(["paperclipai/first", "dottabot/first"]);
await act(async () => { setInputValue(search, "missing"); });
expect(container.textContent).toContain("No repositories match your search.");
await act(async () => { setInputValue(search, ""); });
await selectOwner("empty-org");
expect(container.textContent).toContain("No accessible repositories for this account or organization.");
expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Configure on GitHub");
expect(repositoryNames()).toEqual(empty ? [] : ["paperclipai/first", "paperclipai/second", "dottabot/first"]);
if (empty) {
expect(container.querySelector('p[role="status"]')?.textContent?.trim()).toBe("No accessible repositories.");
expect(container.textContent).not.toContain("Refresh access to load the current repository list.");
}
expect(container.querySelector('[aria-label="Filter repositories by account or organization"]')).toBeNull();
expect(container.querySelector('input[aria-label="Search GitHub repositories"]')).toBeNull();
expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Add More Repos on GitHub");
expect(updateConnectionMock).not.toHaveBeenCalled();
});
@@ -1581,7 +1564,7 @@ describe("AppDetail", () => {
expect(container.querySelector('a[href="https://github.com/paperclipai/test-repo"]')?.textContent).toBe("paperclipai/test-repo");
expect(container.querySelector(
'a[href="https://github.com/apps/paperclip-test/installations/new"]',
)?.textContent).toBe("Configure on GitHub");
)?.textContent).toBe("Add More Repos on GitHub");
expect(container.querySelector('button[aria-label="Refresh access"]')).toBeTruthy();
expect(container.textContent).not.toContain("Installation");
expect(container.textContent).not.toContain("Token continuity");
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { Building2, Loader2, Lock, RefreshCw, Search, TriangleAlert, UserRound } from "lucide-react";
import { Building2, Loader2, Lock, RefreshCw, TriangleAlert, UserRound } from "lucide-react";
import type {
ConnectionAudienceMember,
ConnectionGrant,
@@ -9,8 +9,6 @@ import type {
import { Button } from "@/components/ui/button";
import { Identity } from "@/components/Identity";
import { GithubIcon } from "@/components/icons/github-icon";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { InlineBanner } from "@/components/InlineBanner";
import { MemberMultiSelect } from "@/components/MemberMultiSelect";
@@ -295,24 +293,8 @@ function GitHubConnectionSummary({
onRefreshAccess?: () => void;
refreshPending: boolean;
}) {
const [repositoryOwner, setRepositoryOwner] = useState("*");
const [repositorySearch, setRepositorySearch] = useState("");
useEffect(() => {
setRepositoryOwner("*");
setRepositorySearch("");
}, [grant.id]);
const github = grant.providerTenant?.github;
if (!github) return null;
const owners = [...new Set([
...(github.installationOwnerLogins ?? []),
...(github.repositories ?? []).map((repository) => repository.fullName.split("/")[0]),
])].sort((a, b) => a.localeCompare(b));
const selectedOwner = owners.includes(repositoryOwner) ? repositoryOwner : "*";
const search = repositorySearch.trim().toLowerCase();
const visibleRepositories = github.repositories?.filter((repository) => (
(selectedOwner === "*" || repository.fullName.split("/")[0] === selectedOwner)
&& repository.fullName.toLowerCase().includes(search)
));
const configurationUrl = github.appSlug
? `https://github.com/apps/${encodeURIComponent(github.appSlug)}/installations/new`
: /^https:\/\/github\.com\/apps\/[a-z0-9-]+\/installations\/new$/.test(github.installationUrl ?? "")
@@ -361,7 +343,7 @@ function GitHubConnectionSummary({
) : null}
{configurationUrl ? (
<Button asChild size="sm" variant="outline">
<a href={configurationUrl} target="_blank" rel="noreferrer">Configure on GitHub</a>
<a href={configurationUrl} target="_blank" rel="noreferrer">Add More Repos on GitHub</a>
</Button>
) : onRefreshAccess ? (
<Button size="sm" variant="outline" disabled={refreshPending} onClick={onRefreshAccess}>
@@ -371,27 +353,9 @@ function GitHubConnectionSummary({
) : null}
</div>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<Select value={selectedOwner} onValueChange={setRepositoryOwner}>
<SelectTrigger aria-label="Filter repositories by account or organization" className="w-full">
<span className="flex min-w-0 items-center gap-2">
<GithubIcon className="h-4 w-4 shrink-0" aria-hidden="true" />
<SelectValue />
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="*">All accounts</SelectItem>
{owners.map((owner) => <SelectItem key={owner} value={owner}>{owner}</SelectItem>)}
</SelectContent>
</Select>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
<Input aria-label="Search GitHub repositories" placeholder="Search repositories" className="pl-9" value={repositorySearch} onChange={(event) => setRepositorySearch(event.target.value)} />
</div>
</div>
{github.repositories ? (
visibleRepositories?.length ? <ul aria-label="Accessible GitHub repositories" tabIndex={0} className="max-h-(--sz-github-repository-list) space-y-2 overflow-y-auto text-sm">
{visibleRepositories.map((repository) => (
github.repositories.length ? <ul aria-label="Accessible GitHub repositories" tabIndex={0} className="max-h-(--sz-github-repository-list) space-y-2 overflow-y-auto text-sm">
{github.repositories.map((repository) => (
<li key={repository.id}>
<a className="flex items-center gap-2 text-muted-foreground hover:underline" href={`https://github.com/${repository.fullName.split("/").map(encodeURIComponent).join("/")}`} target="_blank" rel="noreferrer">
<GithubIcon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
@@ -401,7 +365,7 @@ function GitHubConnectionSummary({
</li>
))}
</ul> : <p role="status" className="text-sm text-muted-foreground">
{search ? "No repositories match your search." : "No accessible repositories for this account or organization."}
No accessible repositories.
</p>
) : (
<p className="text-sm text-muted-foreground">Refresh access to load the current repository list.</p>