139 lines
5.1 KiB
TypeScript
139 lines
5.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { toast } from 'sonner';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { PasswordField } from '@/components/ui/PasswordField';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { FormErrorBanner } from '@/components/ui/FormErrorBanner';
|
|
import { FieldError } from '@/components/ui/FieldError';
|
|
import { useAddCmsInstance } from '@/api/useAddCmsInstance';
|
|
import { NetworkError, ProblemDetailsError } from '@/lib/api-client';
|
|
import { addCmsInstanceSchema, type AddCmsInstanceFormData } from '@/lib/schemas/cms';
|
|
|
|
interface AddCmsInstanceDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function AddCmsInstanceDialog({ open, onOpenChange }: AddCmsInstanceDialogProps) {
|
|
const { t } = useTranslation();
|
|
const [serverError, setServerError] = useState<string | null>(null);
|
|
|
|
const addInstance = useAddCmsInstance({
|
|
onError: (err) => {
|
|
if (err instanceof ProblemDetailsError && err.status === 409) {
|
|
setServerError(t('cms.add.errors.conflict'));
|
|
} else if (err instanceof NetworkError) {
|
|
setServerError(t('errors.network'));
|
|
} else {
|
|
setServerError(t('errors.generic'));
|
|
}
|
|
},
|
|
});
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
reset,
|
|
formState: { errors, isSubmitting },
|
|
} = useForm<AddCmsInstanceFormData>({
|
|
resolver: zodResolver(addCmsInstanceSchema),
|
|
mode: 'onTouched',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setServerError(null);
|
|
addInstance.reset();
|
|
reset();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open]);
|
|
|
|
const onSubmit = handleSubmit(async (values) => {
|
|
setServerError(null);
|
|
try {
|
|
await addInstance.mutateAsync(values);
|
|
toast.success(t('cms.add.successToast'));
|
|
onOpenChange(false);
|
|
} catch {
|
|
// error shown via serverError state
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('cms.add.title')}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={onSubmit} noValidate className="space-y-4">
|
|
<FormErrorBanner
|
|
error={serverError !== null ? { message: serverError } : null}
|
|
onDismiss={() => setServerError(null)}
|
|
/>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="add-cms-name">{t('cms.add.nameLabel')}</Label>
|
|
<Input
|
|
id="add-cms-name"
|
|
type="text"
|
|
data-testid="add-cms-name"
|
|
aria-invalid={errors.name !== undefined}
|
|
{...register('name')}
|
|
/>
|
|
{errors.name && (
|
|
<FieldError message={errors.name.message} testId="add-cms-name-error" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="add-cms-url">{t('cms.add.urlLabel')}</Label>
|
|
<Input
|
|
id="add-cms-url"
|
|
type="text"
|
|
placeholder="http://cms.example.com"
|
|
data-testid="add-cms-url"
|
|
aria-invalid={errors.url !== undefined}
|
|
{...register('url')}
|
|
/>
|
|
{errors.url && (
|
|
<FieldError message={errors.url.message} testId="add-cms-url-error" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="add-cms-apikey">{t('cms.add.apiKeyLabel')}</Label>
|
|
<PasswordField
|
|
id="add-cms-apikey"
|
|
{...register('apiKey')}
|
|
/>
|
|
{errors.apiKey && (
|
|
<FieldError message={errors.apiKey.message} testId="add-cms-apikey-error" />
|
|
)}
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
disabled={isSubmitting || addInstance.isPending}
|
|
data-testid="add-cms-submit"
|
|
>
|
|
{isSubmitting || addInstance.isPending ? '…' : t('cms.add.submitButton')}
|
|
</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|