190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
import { useState } from "react";
|
|
import { Plus, Loader2 } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { api, humanizeError } from "@/lib/api";
|
|
|
|
interface Props {
|
|
onCreated: (id: string) => void;
|
|
}
|
|
|
|
export function CreateUserDialog({ onCreated }: Props) {
|
|
const { t } = useTranslation("admin");
|
|
const [open, setOpen] = useState(false);
|
|
const [username, setUsername] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [role, setRole] = useState<"user" | "admin">("user");
|
|
const [password, setPassword] = useState("");
|
|
const [mustChange, setMustChange] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
function reset() {
|
|
setUsername("");
|
|
setEmail("");
|
|
setRole("user");
|
|
setPassword("");
|
|
setMustChange(true);
|
|
setErr(null);
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (submitting) return;
|
|
setErr(null);
|
|
|
|
if (!username.trim()) {
|
|
setErr(t("users_create_validation_username"));
|
|
return;
|
|
}
|
|
if (password.length < 8) {
|
|
setErr(t("users_create_validation_password"));
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
try {
|
|
const u = await api.createUser({
|
|
username: username.trim(),
|
|
email: email.trim() || undefined,
|
|
role,
|
|
password,
|
|
must_change_password: mustChange,
|
|
});
|
|
setOpen(false);
|
|
reset();
|
|
onCreated(u.id);
|
|
} catch (e) {
|
|
setErr(humanizeError(e));
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) reset(); }}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" className="gap-1.5">
|
|
<Plus className="h-4 w-4" />
|
|
{t("users_create_btn")}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("users_create_title")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("users_create_desc")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Username */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs font-semibold text-muted-foreground">
|
|
{t("users_field_username")} *
|
|
</Label>
|
|
<Input
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder={t("users_create_username_placeholder")}
|
|
className="h-9 text-sm"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* Email */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs font-semibold text-muted-foreground">
|
|
{t("users_field_email")}
|
|
</Label>
|
|
<Input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="[email protected]"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Role */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs font-semibold text-muted-foreground">
|
|
{t("users_field_role")}
|
|
</Label>
|
|
<Select value={role} onValueChange={(v: "user" | "admin") => setRole(v)}>
|
|
<SelectTrigger className="h-9 text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="user">{t("users_role_user")}</SelectItem>
|
|
<SelectItem value="admin">{t("users_role_admin")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs font-semibold text-muted-foreground">
|
|
{t("users_field_password")} *
|
|
</Label>
|
|
<Input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="min. 8 characters"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Must change password toggle */}
|
|
<label className="flex items-center gap-2 cursor-pointer select-none">
|
|
<input
|
|
type="checkbox"
|
|
checked={mustChange}
|
|
onChange={(e) => setMustChange(e.target.checked)}
|
|
className="h-4 w-4 rounded border-border"
|
|
/>
|
|
<span className="text-sm text-foreground">
|
|
{t("users_create_must_change")}
|
|
</span>
|
|
</label>
|
|
|
|
{err && (
|
|
<p className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
|
|
{err}
|
|
</p>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button type="submit" disabled={submitting} className="gap-1.5">
|
|
{submitting ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Plus className="h-4 w-4" />
|
|
)}
|
|
{t("users_create_btn")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|