From 4b6d603f6da886ecfec577ab00342ee74b1f05e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Tue, 21 Jul 2026 19:24:02 +0100 Subject: [PATCH] feat(github): explain repository scope (#1842) --- .../github/github-repo-scope-section.tsx | 59 ++++++++++++++++++- .../web/components/github/github-settings.tsx | 2 +- .../components/settings/settings-section.tsx | 15 +++-- .../github-workspace-settings.spec.ts | 18 ++++++ .../mobile-github-workspace-settings.spec.ts | 37 ++++++++++++ 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 apps/web/e2e/tests/integrations/mobile-github-workspace-settings.spec.ts diff --git a/apps/web/components/github/github-repo-scope-section.tsx b/apps/web/components/github/github-repo-scope-section.tsx index 126f1c16f8..4ca71a0fb6 100644 --- a/apps/web/components/github/github-repo-scope-section.tsx +++ b/apps/web/components/github/github-repo-scope-section.tsx @@ -1,13 +1,25 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { IconInfoCircle } from "@tabler/icons-react"; +import { Button } from "@kandev/ui/button"; import { CardContent } from "@kandev/ui/card"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@kandev/ui/drawer"; import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsCard } from "@/components/settings/settings-card"; import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider"; +import { useTouchDrawer } from "@/hooks/use-compact-task-chrome"; import { fetchGitHubWorkspaceSettings, updateGitHubWorkspaceSettings, @@ -39,6 +51,50 @@ function repoFiltersToInput(repos: RepoFilter[]): string { return repos.map((repo) => `${repo.owner}/${repo.name}`).join(", "); } +const repositoryScopeHelp = + "Limits the GitHub pull requests and issues Kandev discovers for this workspace, including My GitHub results and review and issue watches. It does not change GitHub permissions or repository access."; + +function RepositoryScopeHelp() { + const usesTouchDrawer = useTouchDrawer(); + const [open, setOpen] = useState(false); + const button = ( + + ); + const drawerTrigger = {button}; + const trigger = usesTouchDrawer ? ( + drawerTrigger + ) : ( + + {drawerTrigger} + + {repositoryScopeHelp} + + + ); + + return ( + + {trigger} + + + Repository Scope + {repositoryScopeHelp} + + + + ); +} + type ScopeFieldsProps = { mode: GitHubRepoScopeMode; orgs: string; @@ -240,7 +296,8 @@ export function GitHubRepoScopeSection({ workspaceId }: { workspaceId: string }) return ( } + description="Limits GitHub pull requests and issues shown or imported in this workspace." > - + diff --git a/apps/web/components/settings/settings-section.tsx b/apps/web/components/settings/settings-section.tsx index c90a85449f..04e0946e11 100644 --- a/apps/web/components/settings/settings-section.tsx +++ b/apps/web/components/settings/settings-section.tsx @@ -1,8 +1,9 @@ -import { ReactNode } from "react"; +import type { ReactNode } from "react"; type SettingsSectionProps = { icon?: ReactNode; title: string; + titleAccessory?: ReactNode; description?: string; action?: ReactNode; children: ReactNode; @@ -11,6 +12,7 @@ type SettingsSectionProps = { export function SettingsSection({ icon, title, + titleAccessory, description, action, children, @@ -22,10 +24,13 @@ export function SettingsSection({ compressed by a long description when side by side. */}
-

- {icon} - {title} -

+
+

+ {icon} + {title} +

+ {titleAccessory} +
{description &&

{description}

}
{action &&
{action}
} diff --git a/apps/web/e2e/tests/integrations/github-workspace-settings.spec.ts b/apps/web/e2e/tests/integrations/github-workspace-settings.spec.ts index 3983268789..bca8a9f103 100644 --- a/apps/web/e2e/tests/integrations/github-workspace-settings.spec.ts +++ b/apps/web/e2e/tests/integrations/github-workspace-settings.spec.ts @@ -38,6 +38,24 @@ test.describe("GitHub workspace settings", () => { await testPage.goto(`/settings/workspace/${seedData.workspaceId}/integrations/github`); await expect(testPage.getByTestId("github-integration-heading")).toBeVisible(); + const issueWatchesHeading = testPage.getByRole("heading", { name: "Issue Watches" }); + const repositoryScopeHeading = testPage.getByRole("heading", { + name: "Repository Scope", + exact: true, + }); + const [issueWatchesBox, repositoryScopeBox] = await Promise.all([ + issueWatchesHeading.boundingBox(), + repositoryScopeHeading.boundingBox(), + ]); + expect(issueWatchesBox).not.toBeNull(); + expect(repositoryScopeBox).not.toBeNull(); + expect(repositoryScopeBox!.y).toBeGreaterThan(issueWatchesBox!.y); + + await testPage.getByRole("button", { name: "Explain repository scope" }).hover(); + await expect(testPage.getByRole("tooltip")).toContainText( + "Limits the GitHub pull requests and issues Kandev discovers for this workspace", + ); + await testPage.getByTestId("github-scope-mode").click(); await testPage.getByRole("option", { name: "Selected repositories" }).click(); await testPage.getByTestId("github-scope-repos-input").fill("kdlbs/kandev"); diff --git a/apps/web/e2e/tests/integrations/mobile-github-workspace-settings.spec.ts b/apps/web/e2e/tests/integrations/mobile-github-workspace-settings.spec.ts new file mode 100644 index 0000000000..ac9dbcda3b --- /dev/null +++ b/apps/web/e2e/tests/integrations/mobile-github-workspace-settings.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "../../fixtures/test-base"; + +test.describe("GitHub workspace settings on mobile", () => { + test("explains repository scope below issue watches without requiring hover", async ({ + testPage, + seedData, + }) => { + await testPage.goto(`/settings/workspace/${seedData.workspaceId}/integrations/github`); + + const issueWatchesHeading = testPage.getByRole("heading", { name: "Issue Watches" }); + const repositoryScopeHeading = testPage.getByRole("heading", { + name: "Repository Scope", + exact: true, + }); + const scopeDescription = testPage.getByText( + "Limits GitHub pull requests and issues shown or imported in this workspace.", + { exact: true }, + ); + + await expect(scopeDescription).toBeVisible(); + const scopeHelpButton = testPage.getByRole("button", { name: "Explain repository scope" }); + await expect(scopeHelpButton).toBeVisible(); + + const [issueWatchesBox, repositoryScopeBox] = await Promise.all([ + issueWatchesHeading.boundingBox(), + repositoryScopeHeading.boundingBox(), + ]); + expect(issueWatchesBox).not.toBeNull(); + expect(repositoryScopeBox).not.toBeNull(); + expect(repositoryScopeBox!.y).toBeGreaterThan(issueWatchesBox!.y); + + await scopeHelpButton.click(); + await expect(testPage.getByRole("dialog", { name: "Repository Scope" })).toContainText( + "including My GitHub results and review and issue watches", + ); + }); +});