- searchGithubRepositories action fetches up to 300 repos via paginated /user/repos, filters by name, upserts results into gitRepositories so createProjectFromRepository works unchanged; token never leaves backend - connectGithub now persists grantedScopesJson from getAccessToken().scopes - listProviderAccounts and list queries return grantedScopesJson - Gear/settings popover on connected GitHub chip shows scopes and triggers linkSocial with expanded scope set [repo, read:org, read:user, user:email] - RepositorySelector merges debounced live search results with synced snapshot - Restored CONVEX_SITE_URL env var (removed in prior commit but still used)
167 lines
4.5 KiB
TypeScript
167 lines
4.5 KiB
TypeScript
import { authClient } from "@code/auth/web";
|
|
import { Button } from "@code/ui/components/button";
|
|
import {
|
|
CheckCircle2,
|
|
GitBranch,
|
|
LoaderCircle,
|
|
Server,
|
|
XCircle,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { GithubConnectionSettings } from "./github-connection-settings";
|
|
import { PuterConnectForm } from "./puter-connect-form";
|
|
|
|
export interface GitProviderAccountOption {
|
|
readonly externalUsername: string;
|
|
readonly grantedScopesJson?: string;
|
|
readonly id: string;
|
|
readonly provider: "github" | "gitea";
|
|
readonly serverUrl: string;
|
|
readonly status:
|
|
| "pending-auth"
|
|
| "active"
|
|
| "reauth-required"
|
|
| "revoked"
|
|
| "unavailable";
|
|
}
|
|
|
|
interface ProviderChipsProps {
|
|
readonly accounts: readonly GitProviderAccountOption[] | undefined;
|
|
}
|
|
|
|
interface ProviderChipProps {
|
|
readonly account: GitProviderAccountOption | undefined;
|
|
readonly icon: React.ElementType;
|
|
readonly label: string;
|
|
readonly loading: boolean;
|
|
readonly onClick: () => void;
|
|
}
|
|
|
|
const isHealthy = (status: GitProviderAccountOption["status"]) =>
|
|
status === "active";
|
|
|
|
const ProviderStatusIcon = ({ connected }: { readonly connected: boolean }) =>
|
|
connected ? (
|
|
<CheckCircle2 className="size-3 text-emerald-400" />
|
|
) : (
|
|
<XCircle className="size-3 text-amber-500" />
|
|
);
|
|
|
|
const ProviderChip = ({
|
|
account,
|
|
icon: Icon,
|
|
label,
|
|
loading,
|
|
onClick,
|
|
}: ProviderChipProps) => {
|
|
const connected = account !== undefined && isHealthy(account.status);
|
|
|
|
return (
|
|
<Button
|
|
aria-pressed={connected}
|
|
className={
|
|
connected
|
|
? "h-8 gap-1.5 rounded border-[#d7d3c7] bg-[#f5f3eb] px-3 text-[#20201d] hover:bg-[#f5f3eb]"
|
|
: "h-8 gap-1.5 rounded border-[#c9c5b9] bg-[#fffefa] px-3 text-[#68665e] hover:bg-[#f5f3eb]"
|
|
}
|
|
disabled={loading || connected}
|
|
onClick={onClick}
|
|
size="sm"
|
|
type="button"
|
|
variant="outline"
|
|
>
|
|
{loading ? (
|
|
<LoaderCircle className="size-3.5 animate-spin" />
|
|
) : (
|
|
<Icon className="size-3.5 text-[#747168]" />
|
|
)}
|
|
<span className="text-xs font-medium">
|
|
{connected ? account.externalUsername : `Connect ${label}`}
|
|
</span>
|
|
{account === undefined ? null : (
|
|
<ProviderStatusIcon connected={connected} />
|
|
)}
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
export const ProviderChips = ({ accounts }: ProviderChipsProps) => {
|
|
const [pendingProvider, setPendingProvider] = useState<
|
|
"github" | "gitea" | undefined
|
|
>();
|
|
const [showPuterForm, setShowPuterForm] = useState(false);
|
|
|
|
const githubAccount = accounts?.find(
|
|
(account) => account.provider === "github"
|
|
);
|
|
const puterAccount = accounts?.find(
|
|
(account) => account.provider === "gitea"
|
|
);
|
|
|
|
const linkGithub = async () => {
|
|
if (pendingProvider) {
|
|
return;
|
|
}
|
|
setPendingProvider("github");
|
|
try {
|
|
await authClient.linkSocial({
|
|
callbackURL: `${window.location.origin}/projects?resume=github`,
|
|
provider: "github",
|
|
});
|
|
} finally {
|
|
setPendingProvider(undefined);
|
|
}
|
|
};
|
|
|
|
const handlePuterChipClick = () => {
|
|
if (puterAccount && isHealthy(puterAccount.status)) {
|
|
return;
|
|
}
|
|
setShowPuterForm((value) => !value);
|
|
};
|
|
|
|
const handlePuterConnected = () => {
|
|
setShowPuterForm(false);
|
|
};
|
|
|
|
const accountsLoading = accounts === undefined;
|
|
const githubConnected =
|
|
githubAccount !== undefined && isHealthy(githubAccount.status);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<div className="flex items-center gap-1">
|
|
<ProviderChip
|
|
account={githubAccount}
|
|
icon={GitBranch}
|
|
label="GitHub"
|
|
loading={accountsLoading || pendingProvider === "github"}
|
|
onClick={linkGithub}
|
|
/>
|
|
{githubConnected ? (
|
|
<GithubConnectionSettings
|
|
connectionId={githubAccount.id}
|
|
externalUsername={githubAccount.externalUsername}
|
|
grantedScopesJson={githubAccount.grantedScopesJson}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
<ProviderChip
|
|
account={puterAccount}
|
|
icon={Server}
|
|
label="Puter Git"
|
|
loading={accountsLoading || pendingProvider === "gitea"}
|
|
onClick={handlePuterChipClick}
|
|
/>
|
|
</div>
|
|
{showPuterForm ? (
|
|
<div className="rounded border border-[#d7d3c7] bg-[#fffefa] p-3 shadow-sm">
|
|
<PuterConnectForm onConnected={handlePuterConnected} />
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
};
|