import { useEffect, useState, type FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getProviders, createProvider, updateProvider, deleteProvider, testProvider, getTemplates, previewProvider, NotificationProvider, getExternalTemplates, previewExternalTemplate, ExternalTemplate, createExternalTemplate, updateExternalTemplate, deleteExternalTemplate, NotificationTemplate } from '../api/notifications'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { Bell, Plus, Trash2, Edit2, Send, Check, X, Loader2 } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { toast } from '../utils/toast'; // supportsJSONTemplates returns true if the provider type can use JSON templates const supportsJSONTemplates = (providerType: string | undefined): boolean => { if (!providerType) return false; switch (providerType.toLowerCase()) { case 'webhook': case 'discord': case 'slack': case 'gotify': case 'generic': return true; case 'telegram': return false; // Telegram uses URL parameters default: return false; } }; const defaultProviderValues: Partial = { type: 'discord', enabled: true, config: '', template: 'minimal', notify_proxy_hosts: true, notify_remote_servers: true, notify_domains: true, notify_certs: true, notify_uptime: true, }; const ProviderForm: FC<{ initialData?: Partial; onClose: () => void; onSubmit: (data: Partial) => void; }> = ({ initialData, onClose, onSubmit }) => { const { t } = useTranslation(); const { register, handleSubmit, watch, setValue, reset, formState: { errors } } = useForm({ defaultValues: defaultProviderValues, }); const [testStatus, setTestStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [previewContent, setPreviewContent] = useState(null); const [previewError, setPreviewError] = useState(null); useEffect(() => { // Reset form state per open/edit to avoid event checkbox leakage between runs. reset(initialData ? { ...defaultProviderValues, ...initialData } : defaultProviderValues); setTestStatus('idle'); setPreviewContent(null); setPreviewError(null); }, [initialData, reset]); const testMutation = useMutation({ mutationFn: testProvider, onSuccess: () => { setTestStatus('success'); setTimeout(() => setTestStatus('idle'), 3000); }, onError: () => { setTestStatus('error'); setTimeout(() => setTestStatus('idle'), 3000); } }); const handleTest = () => { const formData = watch(); testMutation.mutate(formData as Partial); }; const handlePreview = async () => { const formData = watch(); setPreviewContent(null); setPreviewError(null); try { // If using an external saved template (id), call previewExternalTemplate with template_id if (formData.template && typeof formData.template === 'string' && formData.template.length === 36) { const res = await previewExternalTemplate(formData.template, undefined, undefined); if (res.parsed) setPreviewContent(JSON.stringify(res.parsed, null, 2)); else setPreviewContent(res.rendered); } else { const res = await previewProvider(formData as Partial); if (res.parsed) setPreviewContent(JSON.stringify(res.parsed, null, 2)); else setPreviewContent(res.rendered); } } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); setPreviewError(msg || 'Failed to generate preview'); } }; const type = watch('type'); const { data: builtins } = useQuery({ queryKey: ['notificationTemplates'], queryFn: getTemplates }); const { data: externalTemplates } = useQuery({ queryKey: ['externalTemplates'], queryFn: getExternalTemplates }); const template = watch('template'); const setTemplate = (templateStr: string, templateName?: string) => { // If templateName is provided, set template selection as well if (templateName) setValue('template', templateName); setValue('config', templateStr); }; // Client-side URL validation keeps the form open and prevents navigation on invalid input. const validateUrl = (value: string | undefined) => { if (!value) return true; try { const parsed = new URL(value); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return t('notificationProviders.invalidUrl'); } return true; } catch { return t('notificationProviders.invalidUrl'); } }; return (
{errors.name && {errors.name.message as string}}
{errors.url && ( {errors.url.message as string} )} {!supportsJSONTemplates(type) && (

{t('notificationProviders.shoutrrrHelp')} {t('common.docs')}.

)}
{supportsJSONTemplates(type) && (