Files
ai-agent/web/app/dashboard/agents/_components/team-edit.tsx
T

362 lines
12 KiB
TypeScript
Raw Normal View History

2026-04-09 10:01:23 +08:00
"use client";
2026-05-25 12:06:15 +08:00
import { useCallback, useEffect, useMemo, useState } from "react";
2026-04-09 10:01:23 +08:00
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
2026-05-25 12:06:15 +08:00
import { Controller, type Resolver, useForm } from "react-hook-form";
2026-04-09 10:01:23 +08:00
import { toast } from "sonner";
import { z } from "zod/v4";
import {
type AdminAgentTeam,
type CreateAdminAgentTeamPayload,
fetchAgentTeam,
fetchUsersAll,
type AdminUser,
} from "@/lib/api/admin";
2026-05-25 12:06:15 +08:00
import { OptionCombobox } from "@/components/option-combobox";
2026-04-09 10:01:23 +08:00
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Field,
FieldContent,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
2026-05-25 12:06:15 +08:00
import { useI18n } from "@/i18n/provider";
import { Status } from "@/lib/generated/enums";
type TFunction = (key: string, values?: Record<string, string | number>) => string;
2026-04-09 10:01:23 +08:00
type TeamEditDialogProps = {
open: boolean;
saving: boolean;
itemId: number | null;
onOpenChange: (open: boolean) => void;
onSubmit: (payload: CreateAdminAgentTeamPayload) => Promise<void>;
};
const emptyForm: EditForm = {
name: "",
leaderUserId: "0",
status: String(Status.Ok),
description: "",
remark: "",
};
2026-05-25 12:06:15 +08:00
type EditForm = {
name: string;
leaderUserId: string;
status: string;
description: string;
remark: string;
};
function createEditFormSchema(t: TFunction) {
return z.object({
name: z.string().trim().min(1, t("agentProfile.teamNameRequired")),
leaderUserId: z.string().trim().regex(/^\d+$/, t("agentProfile.leaderInvalid")),
2026-04-09 10:01:23 +08:00
status: z.enum([String(Status.Ok), String(Status.Disabled)], {
2026-05-25 12:06:15 +08:00
message: t("agentProfile.teamStatusRequired"),
2026-04-09 10:01:23 +08:00
}),
description: z.string().trim(),
remark: z.string().trim(),
2026-05-25 12:06:15 +08:00
});
}
2026-04-09 10:01:23 +08:00
2026-05-25 12:06:15 +08:00
function getStatusOptions(t: TFunction) {
return [
{ value: String(Status.Ok), label: t("agentProfile.enabled") },
{ value: String(Status.Disabled), label: t("agentProfile.disabled") },
];
}
2026-04-09 10:01:23 +08:00
function buildForm(item: AdminAgentTeam | null): EditForm {
if (!item) {
return emptyForm;
}
return {
name: item.name,
leaderUserId: String(item.leaderUserId),
status: String(item.status),
description: item.description || "",
remark: item.remark || "",
};
}
function buildPayload(form: EditForm): CreateAdminAgentTeamPayload {
return {
name: form.name.trim(),
leaderUserId: Number(form.leaderUserId),
status: Number(form.status),
description: form.description.trim(),
remark: form.remark.trim(),
};
}
export function EditDialog({
open,
saving,
itemId,
onOpenChange,
onSubmit,
}: TeamEditDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{open ? (
<TeamEditDialogBody
key={itemId ? `edit-${itemId}` : "create"}
itemId={itemId}
saving={saving}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
/>
) : null}
</Dialog>
);
}
type TeamEditDialogBodyProps = Omit<TeamEditDialogProps, "open">;
function TeamEditDialogBody({
saving,
itemId,
onOpenChange,
onSubmit,
}: TeamEditDialogBodyProps) {
2026-05-25 12:06:15 +08:00
const t = useI18n();
2026-04-09 10:01:23 +08:00
const [users, setUsers] = useState<AdminUser[]>([]);
const [userSelectOpen, setUserSelectOpen] = useState(false);
const [loading, setLoading] = useState(false);
const userOptions = users.map((user) => ({
value: String(user.id),
label: `${user.nickname || user.username} (${user.username})`,
}));
2026-05-25 12:06:15 +08:00
const statusOptions = useMemo(() => getStatusOptions(t), [t]);
2026-04-09 10:01:23 +08:00
const loadUsers = useCallback(async () => {
try {
const data = await fetchUsersAll();
setUsers(data);
} catch (error) {
2026-05-25 12:06:15 +08:00
toast.error(error instanceof Error ? error.message : t("agentProfile.loadUsersFailed"));
2026-04-09 10:01:23 +08:00
}
2026-05-25 12:06:15 +08:00
}, [t]);
const editFormSchema = useMemo(() => createEditFormSchema(t), [t]);
const editFormResolver = useMemo(
() => zodResolver(editFormSchema) as Resolver<EditForm>,
[editFormSchema],
);
const form = useForm<EditForm>({
2026-04-09 10:01:23 +08:00
resolver: editFormResolver,
defaultValues: emptyForm,
});
const {
control,
handleSubmit,
reset,
register,
formState: { errors },
} = form;
useEffect(() => {
async function loadDetail() {
if (!itemId) {
reset(emptyForm);
return;
}
setLoading(true);
try {
const data = await fetchAgentTeam(itemId);
reset(buildForm(data));
} catch (error) {
2026-05-25 12:06:15 +08:00
toast.error(error instanceof Error ? error.message : t("agentProfile.loadTeamDetailFailed"));
2026-04-09 10:01:23 +08:00
} finally {
setLoading(false);
}
}
void loadDetail();
2026-05-25 12:06:15 +08:00
}, [itemId, reset, t]);
2026-04-09 10:01:23 +08:00
useEffect(() => {
void loadUsers();
}, [loadUsers]);
async function onFormSubmit(values: EditForm) {
await onSubmit(buildPayload(values));
}
return (
<DialogContent className="max-w-xl gap-0 p-0 sm:max-w-xl">
<DialogHeader className="px-6 pt-6">
2026-05-25 12:06:15 +08:00
<DialogTitle>{itemId ? t("agentProfile.teamEditTitle") : t("agentProfile.teamCreateTitle")}</DialogTitle>
2026-04-09 10:01:23 +08:00
</DialogHeader>
{loading ? (
<div className="flex items-center justify-center py-12">
2026-05-25 12:06:15 +08:00
<div className="text-muted-foreground">{t("agentProfile.loading")}</div>
2026-04-09 10:01:23 +08:00
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)}>
<div className="space-y-4 p-6">
<Field data-invalid={!!errors.name}>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="agent-team-name">{t("agentProfile.teamName")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Input
id="agent-team-name"
2026-05-25 12:06:15 +08:00
placeholder={t("agentProfile.teamNamePlaceholder")}
2026-04-09 10:01:23 +08:00
{...register("name")}
/>
<FieldError errors={[errors.name]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.leaderUserId}>
2026-05-25 12:06:15 +08:00
<FieldLabel>{t("agentProfile.leader")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Controller
control={control}
name="leaderUserId"
render={({ field }) => (
<Popover open={userSelectOpen} onOpenChange={setUserSelectOpen}>
<PopoverTrigger
render={
<Button
variant="outline"
role="combobox"
aria-expanded={userSelectOpen}
className="w-full justify-between font-normal"
/>
}
>
<span className="truncate">
{field.value === "0"
2026-05-25 12:06:15 +08:00
? t("agentProfile.noLeader")
: userOptions.find((option) => option.value === field.value)?.label ?? t("agentProfile.selectLeader")}
2026-04-09 10:01:23 +08:00
</span>
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popper-anchor-width)] p-0" align="start">
<Command>
2026-05-25 12:06:15 +08:00
<CommandInput placeholder={t("agentProfile.searchUser")} />
2026-04-09 10:01:23 +08:00
<CommandList>
2026-05-25 12:06:15 +08:00
<CommandEmpty>{t("agentProfile.emptyUser")}</CommandEmpty>
2026-04-09 10:01:23 +08:00
<CommandGroup>
<CommandItem
2026-05-25 12:06:15 +08:00
value={t("agentProfile.noLeader")}
2026-04-09 10:01:23 +08:00
onSelect={() => {
field.onChange("0");
setUserSelectOpen(false);
}}
>
<CheckIcon
className={`mr-2 size-4 ${field.value === "0" ? "opacity-100" : "opacity-0"}`}
/>
2026-05-25 12:06:15 +08:00
{t("agentProfile.noLeader")}
2026-04-09 10:01:23 +08:00
</CommandItem>
{userOptions.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => {
field.onChange(option.value);
setUserSelectOpen(false);
}}
>
<CheckIcon
className={`mr-2 size-4 ${
field.value === option.value ? "opacity-100" : "opacity-0"
}`}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
/>
<FieldError errors={[errors.leaderUserId]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.status}>
2026-05-25 12:06:15 +08:00
<FieldLabel>{t("agentProfile.status")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Controller
control={control}
name="status"
render={({ field }) => (
2026-05-25 12:06:15 +08:00
<OptionCombobox
options={statusOptions}
2026-04-09 10:01:23 +08:00
value={field.value}
2026-05-25 12:06:15 +08:00
onChange={field.onChange}
placeholder={t("agentProfile.selectStatus")}
searchPlaceholder={t("agentProfile.searchStatus")}
emptyText={t("agentProfile.emptyStatus")}
/>
2026-04-09 10:01:23 +08:00
)}
/>
<FieldError errors={[errors.status]} />
</FieldContent>
</Field>
<Field>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="agent-team-description">{t("agentProfile.description")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Input
id="agent-team-description"
2026-05-25 12:06:15 +08:00
placeholder={t("agentProfile.descriptionPlaceholder")}
2026-04-09 10:01:23 +08:00
{...register("description")}
/>
</FieldContent>
</Field>
<Field>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="agent-team-remark">{t("agentProfile.remark")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Textarea
id="agent-team-remark"
rows={4}
2026-05-25 12:06:15 +08:00
placeholder={t("agentProfile.remarkPlaceholder")}
2026-04-09 10:01:23 +08:00
{...register("remark")}
/>
</FieldContent>
</Field>
</div>
<DialogFooter className="mx-0 mb-0 px-6 py-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={saving}
>
2026-05-25 12:06:15 +08:00
{t("agentProfile.cancel")}
2026-04-09 10:01:23 +08:00
</Button>
<Button type="submit" disabled={saving || loading}>
2026-05-25 12:06:15 +08:00
{saving ? t("agentProfile.saving") : t("agentProfile.save")}
2026-04-09 10:01:23 +08:00
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
);
}