"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 { SettingsFieldLabel } from "@/components/settings/settings-typography";
import { settingsControlClassName } from "@/components/settings/settings-control";
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,
} from "@/lib/api/domains/github-api";
import type {
GitHubRepoScopeMode,
RepoFilter,
UpdateGitHubWorkspaceSettingsRequest,
} from "@/lib/types/github";
import { useTranslation } from "react-i18next";
import { t as translate } from "@/lib/i18n";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
function splitCSV(value: string): string[] {
return value
.split(",")
.map((part) => part.trim())
.filter(Boolean);
}
function parseRepoFilters(value: string): RepoFilter[] {
return splitCSV(value)
.map((repo) => {
const [owner, name, ...rest] = repo.split("/");
if (!owner || !name || rest.length > 0) return null;
return { owner, name };
})
.filter((repo): repo is RepoFilter => repo !== null);
}
function repoFiltersToInput(repos: RepoFilter[]): string {
return repos.map((repo) => `${repo.owner}/${repo.name}`).join(", ");
}
// Only the list matching the selected mode is sent; the other stays untouched
// server-side.
function scopePayload({
workspaceId,
mode,
orgs,
parsedRepos,
}: {
workspaceId: string;
mode: GitHubRepoScopeMode;
orgs: string;
parsedRepos: RepoFilter[];
}): UpdateGitHubWorkspaceSettingsRequest {
const payload: UpdateGitHubWorkspaceSettingsRequest = {
workspace_id: workspaceId,
repo_scope_mode: mode,
};
if (mode === "orgs") payload.repo_scope_orgs = splitCSV(orgs);
if (mode === "repos") payload.repo_scope_repos = parsedRepos;
return payload;
}
function RepositoryScopeHelp() {
const { t } = useTranslation();
const usesTouchDrawer = useTouchDrawer();
const [open, setOpen] = useState(false);
const button = (
);
const drawerTrigger = {button};
const trigger = usesTouchDrawer ? (
drawerTrigger
) : (
{drawerTrigger}
{t("github:repositoryScopeHelp")}
);
return (
{trigger}
{t("github:repositoryScope")}{t("github:repositoryScopeHelp")}
);
}
type ScopeFieldsProps = {
mode: GitHubRepoScopeMode;
orgs: string;
repos: string;
baseline: { mode: GitHubRepoScopeMode; orgs: string; repos: string };
loading: boolean;
invalidRepos: boolean;
onModeChange: (mode: GitHubRepoScopeMode) => void;
onOrgsChange: (orgs: string) => void;
onReposChange: (repos: string) => void;
};
function RepositoryScopeFields({
mode,
orgs,
repos,
baseline,
loading,
invalidRepos,
onModeChange,
onOrgsChange,
onReposChange,
}: ScopeFieldsProps) {
const { t } = useTranslation();
return (
{t("github:mode")}
{t("github:organizations")}
onOrgsChange(event.target.value)}
disabled={loading || mode !== "orgs"}
// Sample GitHub organization logins, i.e. the shape of the value
// this field accepts. A translated `example-org` would stop being
// a usable example.
// eslint-disable-next-line i18next/no-literal-string -- example org logins
placeholder="kdlbs, example-org"
data-testid="github-scope-orgs-input"
/>
{t("github:repositories")}
onReposChange(event.target.value)}
disabled={loading || mode !== "repos"}
aria-invalid={invalidRepos}
// Sample `owner/repo` slugs — the format the sibling validation
// message describes, not copy.
// eslint-disable-next-line i18next/no-literal-string -- example repo slugs
placeholder="kdlbs/kandev, example/api"
data-testid="github-scope-repos-input"
/>
{invalidRepos && (