Files
kandev/apps/web/components/watcher-repository-fields.tsx
T

115 lines
4.3 KiB
TypeScript

"use client";
import { Label } from "@kandev/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { useRepositories } from "@/hooks/domains/workspace/use-repositories";
import { useBranches } from "@/hooks/domains/workspace/use-repository-branches";
import {
NO_REPOSITORY,
NO_REPOSITORY_LABEL,
DEFAULT_BRANCH,
DEFAULT_BRANCH_LABEL,
branchPlaceholder,
resolveBaseBranch,
resolveRepositoryId,
} from "@/lib/watcher-repository-default";
type PickItem = { id: string; label: string };
function PickSelect(props: {
label: string;
description: string;
value: string;
onChange: (v: string) => void;
placeholder: string;
items: PickItem[];
disabled?: boolean;
}) {
return (
<div className="space-y-1.5">
<Label>{props.label}</Label>
<p className="text-xs text-muted-foreground">{props.description}</p>
<Select
value={props.value || undefined}
onValueChange={props.onChange}
disabled={props.disabled}
>
<SelectTrigger className="cursor-pointer">
<SelectValue placeholder={props.placeholder} />
</SelectTrigger>
<SelectContent>
{props.items.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
/**
* Shared repository + base-branch picker for the Linear / Jira / Sentry watcher
* dialogs. Binds the watcher to an optional repository so its tasks run against
* that codebase instead of an empty scratch checkout; an empty repository =
* unbound (repo-less task). How the repository is materialised (isolated
* worktree vs in-place) is decided by the executor profile, not this field. The
* base-branch select is disabled until a repository is chosen and defaults to
* the repository's default branch. The `onChange` callbacks receive values
* already collapsed from the dropdown sentinels back to "".
*/
export function WatcherRepositoryFields({
workspaceId,
repositoryId,
baseBranch,
onRepositoryChange,
onBaseBranchChange,
}: {
workspaceId: string;
repositoryId: string;
baseBranch: string;
onRepositoryChange: (repositoryId: string) => void;
onBaseBranchChange: (baseBranch: string) => void;
}) {
// forceRefresh: a repo created in settings doesn't update the shared store
// slice, and the lazy fetch is gated by isLoaded — so without this the picker
// could miss repositories created since the slice first loaded. The hook owns
// the fetch (keeps data-flow in the hook layer, not the component).
const { repositories } = useRepositories(workspaceId, !!workspaceId, true);
const branchSource = repositoryId ? ({ kind: "id", workspaceId, repositoryId } as const) : null;
const { branches, isLoading: branchesLoading } = useBranches(branchSource, !!repositoryId);
// A branch name can appear twice (local + remote tracking, e.g. "main" and
// origin/"main"), which would emit two <SelectItem value="main"> — Radix then
// renders every matching item's text in the trigger ("mainmain") and React
// warns on duplicate keys. Dedupe by name so each branch is one option.
const uniqueBranchNames = Array.from(new Set(branches.map((b) => b.name)));
return (
<div className="grid grid-cols-2 gap-4">
<PickSelect
label="Repository"
description="Optional — the repository the agent works in."
value={repositoryId || NO_REPOSITORY}
onChange={(v) => onRepositoryChange(resolveRepositoryId(v))}
placeholder={NO_REPOSITORY_LABEL}
items={[
{ id: NO_REPOSITORY, label: NO_REPOSITORY_LABEL },
...repositories.map((r) => ({ id: r.id, label: r.name })),
]}
/>
<PickSelect
label="Base Branch"
description="The base branch the agent starts from."
value={baseBranch || DEFAULT_BRANCH}
onChange={(v) => onBaseBranchChange(resolveBaseBranch(v))}
placeholder={branchPlaceholder(repositoryId, branchesLoading)}
items={[
{ id: DEFAULT_BRANCH, label: DEFAULT_BRANCH_LABEL },
...uniqueBranchNames.map((name) => ({ id: name, label: name })),
]}
disabled={!repositoryId || branchesLoading}
/>
</div>
);
}