Files
kandev/apps/web/components/watcher-repository-fields.tsx
T
João Salavisa d58a1df32d fix(i18n): localize watcher fallback labels (#2596)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-08-13 01:00:32 +01:00

126 lines
4.9 KiB
TypeScript

"use client";
import { useId } from "react";
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,
DEFAULT_BRANCH,
branchPlaceholder,
resolveBaseBranch,
resolveRepositoryId,
} from "@/lib/watcher-repository-default";
import { useTranslation } from "react-i18next";
type PickItem = { id: string; label: string };
function PickSelect(props: {
label: string;
description: string;
value: string | undefined;
onChange: (v: string) => void;
placeholder: string;
items: PickItem[];
disabled?: boolean;
}) {
const id = useId();
return (
<div className="space-y-1.5">
<Label htmlFor={id}>{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 id={id} 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;
}) {
const { t } = useTranslation();
// 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);
const noRepositoryLabel = t("common:noRepositoryOption");
const defaultBranchLabel = t("common:repositoryDefaultBranchOption");
const branchPlaceholderLabels = {
defaultBranch: defaultBranchLabel,
loading: t("common:watcherLoadingBranchesPlaceholder"),
pickRepository: t("common:watcherPickRepositoryPlaceholder"),
};
const branchPlaceholderLabel =
branchPlaceholderLabels[branchPlaceholder(repositoryId, branchesLoading)];
// 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-1 gap-4 sm:grid-cols-2">
<PickSelect
label={t("common:repository")}
description={t("common:optionalTheRepositoryTheAgentWorks")}
value={repositoryId || NO_REPOSITORY}
onChange={(v) => onRepositoryChange(resolveRepositoryId(v))}
placeholder={noRepositoryLabel}
items={[
{ id: NO_REPOSITORY, label: noRepositoryLabel },
...repositories.map((r) => ({ id: r.id, label: r.name })),
]}
/>
<PickSelect
label={t("common:baseBranch")}
description={t("common:theBaseBranchTheAgentStarts")}
value={repositoryId && !branchesLoading ? baseBranch || DEFAULT_BRANCH : undefined}
onChange={(v) => onBaseBranchChange(resolveBaseBranch(v))}
placeholder={branchPlaceholderLabel}
items={[
{ id: DEFAULT_BRANCH, label: defaultBranchLabel },
...uniqueBranchNames.map((name) => ({ id: name, label: name })),
]}
disabled={!repositoryId || branchesLoading}
/>
</div>
);
}