Files
kandev/apps/web/components/settings/profile-edit/git-identity-fields.tsx
T
2026-08-16 19:18:21 +00:00

168 lines
6.1 KiB
TypeScript

import { Badge } from "@kandev/ui/badge";
import { Input } from "@kandev/ui/input";
import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group";
import { AccordionContent, AccordionItem, AccordionTrigger } from "@kandev/ui/accordion";
import { useTranslation } from "react-i18next";
import {
SETTINGS_TYPOGRAPHY,
SettingsFieldDescription,
SettingsFieldLabel,
} from "@/components/settings/settings-typography";
import { settingsControlClassName } from "@/components/settings/settings-control";
export type GitIdentityMode = "local" | "override";
export type GitIdentityState = {
userName: string;
userEmail: string;
detected: boolean;
};
type GitIdentityFieldsProps = {
mode: GitIdentityMode;
baselineMode: GitIdentityMode;
onModeChange: (mode: GitIdentityMode) => void;
gitUserName: string;
gitUserEmail: string;
baselineGitUserName: string;
baselineGitUserEmail: string;
onGitUserNameChange: (value: string) => void;
onGitUserEmailChange: (value: string) => void;
localGitIdentity: GitIdentityState;
};
const RADIO_LABEL =
"flex w-full items-start gap-3 rounded-md border p-3 text-left cursor-pointer transition-colors";
const RADIO_ITEM = "mt-0.5 border border-muted-foreground/80 data-[state=checked]:border-primary";
export function GitIdentityAccordionItem(props: GitIdentityFieldsProps) {
const { t } = useTranslation();
const isLocal = props.mode === "local" && props.localGitIdentity.detected;
const modeDirty = props.mode !== props.baselineMode;
const nameDirty = props.mode === "override" && props.gitUserName !== props.baselineGitUserName;
const emailDirty = props.mode === "override" && props.gitUserEmail !== props.baselineGitUserEmail;
const description = props.localGitIdentity.detected
? `${props.localGitIdentity.userName} <${props.localGitIdentity.userEmail}>`
: t("executors:localGitIdentityNotDetected");
const badgeLabel = t(gitIdentityBadgeLabelKey(props.mode, isLocal));
return (
<AccordionItem value="git_identity" data-settings-dirty={modeDirty || nameDirty || emailDirty}>
<AccordionTrigger>
<div className="flex flex-1 items-center gap-2">
<span className="text-sm font-medium">{t("executors:gitIdentity")}</span>
<Badge
variant={isLocal ? "default" : "secondary"}
className={
isLocal
? `bg-green-600 px-1.5 py-0 ${SETTINGS_TYPOGRAPHY.meta}`
: `px-1.5 py-0 ${SETTINGS_TYPOGRAPHY.meta}`
}
>
{badgeLabel}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className="h-auto">
<div className="space-y-3 text-sm">
<SettingsFieldDescription>
{t("executors:usedByRemoteExecutorsForCommit")}
</SettingsFieldDescription>
<RadioGroup
value={props.mode}
onValueChange={(value) => props.onModeChange(value as GitIdentityMode)}
className="gap-2"
data-settings-dirty={modeDirty}
>
<IdentityModeOption
value="local"
selected={props.mode === "local"}
baselineSelected={props.baselineMode === "local"}
disabled={!props.localGitIdentity.detected}
title={t("executors:useLocalGitConfig")}
description={description}
/>
<IdentityModeOption
value="override"
selected={props.mode === "override"}
baselineSelected={props.baselineMode === "override"}
title={t("executors:overrideIdentity")}
description={t("executors:setACustomNameAndEmail")}
/>
</RadioGroup>
{props.mode === "override" && <OverrideIdentityFields {...props} />}
</div>
</AccordionContent>
</AccordionItem>
);
}
// Returns a catalog key, not copy: module scope, so a `t()` here would freeze
// the badge at the boot locale.
function gitIdentityBadgeLabelKey(mode: GitIdentityMode, isLocal: boolean): string {
if (isLocal) return "executors:gitIdentityAutoDetect";
return mode === "local" ? "executors:notConfigured" : "executors:gitIdentityCustom";
}
function IdentityModeOption({
value,
selected,
baselineSelected,
disabled,
title,
description,
}: {
value: GitIdentityMode;
selected: boolean;
baselineSelected: boolean;
disabled?: boolean;
title: string;
description: string;
}) {
return (
<label
className={`${RADIO_LABEL} ${selected ? "border-primary bg-primary/5" : "border-border"}`}
data-settings-dirty={selected !== baselineSelected}
>
<RadioGroupItem value={value} disabled={disabled} className={RADIO_ITEM} />
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{title}</span>
<span className={SETTINGS_TYPOGRAPHY.fieldDescription}>{description}</span>
</div>
</label>
);
}
function OverrideIdentityFields(props: GitIdentityFieldsProps) {
const { t } = useTranslation();
return (
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<SettingsFieldLabel htmlFor="remote-git-user-name">
{t("executors:gitUserName")}
</SettingsFieldLabel>
<Input
id="remote-git-user-name"
value={props.gitUserName}
onChange={(event) => props.onGitUserNameChange(event.target.value)}
placeholder={t("executors:janeDeveloper")}
data-settings-dirty={props.gitUserName !== props.baselineGitUserName}
className={settingsControlClassName()}
/>
</div>
<div className="space-y-1.5">
<SettingsFieldLabel htmlFor="remote-git-user-email">
{t("executors:gitUserEmail")}
</SettingsFieldLabel>
<Input
id="remote-git-user-email"
value={props.gitUserEmail}
onChange={(event) => props.onGitUserEmailChange(event.target.value)}
placeholder="jane@example.com"
data-settings-dirty={props.gitUserEmail !== props.baselineGitUserEmail}
className={settingsControlClassName()}
/>
</div>
</div>
);
}