Files
kandev/apps/web/components/automations/trigger-configs/github-pr-config.tsx
T
João Salavisa 24c3b594ce fix(i18n): anchor the literal guard's exclusions so they stop swallowing copy (#2288)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:36:27 +02:00

147 lines
5.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Checkbox } from "@kandev/ui/checkbox";
import { Label } from "@kandev/ui/label";
import { Input } from "@kandev/ui/input";
import { Switch } from "@kandev/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { IconInfoCircle } from "@tabler/icons-react";
import { RepoFilterSelector } from "@/components/github/repo-filter-selector";
import type { RepoFilter } from "@/lib/types/github";
type GitHubPRConfigProps = {
config: Record<string, unknown>;
onUpdate: (config: Record<string, unknown>) => void;
};
// `value` is the GitHub PR event name persisted in the trigger config and
// matched by the backend — protocol. Label and tooltip are copy.
const PR_EVENTS = [
{
value: "opened",
labelKey: "automations:prEventOpened",
tooltipKey: "automations:prEventOpenedTooltip",
},
] as const;
export function GitHubPRConfig({ config, onUpdate }: GitHubPRConfigProps) {
const { t } = useTranslation();
const events = (config.events as string[]) ?? [];
const repos = (config.repos as RepoFilter[]) ?? [];
const allRepos = (config.all_repos as boolean) ?? true;
const configBranches = ((config.branches as string[]) ?? []).join(", ");
const configAuthors = ((config.authors as string[]) ?? []).join(", ");
const [branches, setBranches] = useState(configBranches);
const [authors, setAuthors] = useState(configAuthors);
useEffect(() => {
setBranches(configBranches);
}, [configBranches]);
useEffect(() => {
setAuthors(configAuthors);
}, [configAuthors]);
const excludeDraft = (config.exclude_draft as boolean) ?? false;
const toggleEvent = (event: string) => {
const next = events.includes(event) ? events.filter((e) => e !== event) : [...events, event];
onUpdate({ ...config, events: next });
};
const handleBranchesBlur = () => {
const parsed = branches
.split(",")
.map((b) => b.trim())
.filter(Boolean);
onUpdate({ ...config, branches: parsed });
};
const handleAuthorsBlur = () => {
const parsed = authors
.split(",")
.map((a) => a.trim())
.filter(Boolean);
onUpdate({ ...config, authors: parsed });
};
return (
<div className="space-y-3">
<EventsSection events={events} onToggle={toggleEvent} />
<RepoFilterSelector
allRepos={allRepos}
selectedRepos={repos}
onAllReposChange={(checked) => {
onUpdate({ ...config, all_repos: checked, repos: checked ? [] : repos });
}}
onSelectedReposChange={(next) => onUpdate({ ...config, repos: next })}
/>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">{t("automations:baseBranchesLabel")}</Label>
{/* Example branch names — data the user types verbatim, not copy. */}
<Input
value={branches}
onChange={(e) => setBranches(e.target.value)}
onBlur={handleBranchesBlur}
// eslint-disable-next-line i18next/no-literal-string -- example branch names, see above
placeholder="main, develop"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t("automations:authorsLabel")}</Label>
{/* Example GitHub usernames — data, not copy. */}
<Input
value={authors}
onChange={(e) => setAuthors(e.target.value)}
onBlur={handleAuthorsBlur}
// eslint-disable-next-line i18next/no-literal-string -- example usernames, see above
placeholder="alice, bob"
/>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
size="sm"
checked={excludeDraft}
onCheckedChange={(checked) => onUpdate({ ...config, exclude_draft: checked })}
className="cursor-pointer"
/>
<Label className="text-xs">{t("automations:excludeDraftPrs")}</Label>
</div>
</div>
);
}
function EventsSection({
events,
onToggle,
}: {
events: string[];
onToggle: (event: string) => void;
}) {
const { t } = useTranslation();
return (
<div className="space-y-2">
<Label className="text-xs">{t("automations:eventsLabel")}</Label>
<div className="flex flex-wrap gap-3">
{PR_EVENTS.map((evt) => (
<label key={evt.value} className="flex items-center gap-1.5 cursor-pointer">
<Checkbox
checked={events.includes(evt.value)}
onCheckedChange={() => onToggle(evt.value)}
className="cursor-pointer"
/>
<span className="text-sm">{t(evt.labelKey)}</span>
<Tooltip>
<TooltipTrigger asChild>
<IconInfoCircle className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-[220px]">{t(evt.tooltipKey)}</TooltipContent>
</Tooltip>
</label>
))}
</div>
</div>
);
}