feat(github): explain repository scope (#1842)
This commit is contained in:
@@ -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 = (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer text-muted-foreground sm:h-7 sm:w-7"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
aria-label="Explain repository scope"
|
||||
>
|
||||
<IconInfoCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
const drawerTrigger = <DrawerTrigger asChild>{button}</DrawerTrigger>;
|
||||
const trigger = usesTouchDrawer ? (
|
||||
drawerTrigger
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{drawerTrigger}</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" className="max-w-[320px] text-xs leading-relaxed">
|
||||
{repositoryScopeHelp}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
{trigger}
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Repository Scope</DrawerTitle>
|
||||
<DrawerDescription>{repositoryScopeHelp}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
type ScopeFieldsProps = {
|
||||
mode: GitHubRepoScopeMode;
|
||||
orgs: string;
|
||||
@@ -240,7 +296,8 @@ export function GitHubRepoScopeSection({ workspaceId }: { workspaceId: string })
|
||||
return (
|
||||
<SettingsSection
|
||||
title="Repository Scope"
|
||||
description="Choose which GitHub repositories belong to this workspace."
|
||||
titleAccessory={<RepositoryScopeHelp />}
|
||||
description="Limits GitHub pull requests and issues shown or imported in this workspace."
|
||||
>
|
||||
<RepositoryScopeFields
|
||||
mode={draft.mode}
|
||||
|
||||
@@ -267,9 +267,9 @@ export function GitHubConnectionSection() {
|
||||
function PerWorkspaceSection({ workspaceId }: { workspaceId: string }) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<GitHubRepoScopeSection workspaceId={workspaceId} />
|
||||
<ReviewWatchSection workspaceId={workspaceId} />
|
||||
<IssueWatchSection workspaceId={workspaceId} />
|
||||
<GitHubRepoScopeSection workspaceId={workspaceId} />
|
||||
<ActionPresetsSection workspaceId={workspaceId} />
|
||||
<SettingsSection title="PR Analytics" description="Pull request activity for this workspace.">
|
||||
<PRStatsPanel workspaceId={workspaceId} />
|
||||
|
||||
@@ -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. */}
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
{icon}
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
{icon}
|
||||
{title}
|
||||
</h3>
|
||||
{titleAccessory}
|
||||
</div>
|
||||
{description && <p className="text-sm text-muted-foreground mt-1">{description}</p>}
|
||||
</div>
|
||||
{action && <div className="w-full shrink-0 sm:w-auto">{action}</div>}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user