diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 58f79eab..3abd1800 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2151,6 +2151,15 @@ export type GetScheduleNotificationsResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2184,7 +2193,7 @@ export type GetScheduleNotificationsResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; destinationId: number; @@ -2241,6 +2250,15 @@ export type UpdateScheduleNotificationsResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2274,7 +2292,7 @@ export type UpdateScheduleNotificationsResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; destinationId: number; @@ -2568,6 +2586,15 @@ export type ListNotificationDestinationsResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2601,7 +2628,7 @@ export type ListNotificationDestinationsResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }>; }; @@ -2629,6 +2656,15 @@ export type CreateNotificationDestinationData = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2689,6 +2725,15 @@ export type CreateNotificationDestinationResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2722,7 +2767,7 @@ export type CreateNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -2796,6 +2841,15 @@ export type GetNotificationDestinationResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2829,7 +2883,7 @@ export type GetNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -2857,6 +2911,15 @@ export type UpdateNotificationDestinationData = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2927,6 +2990,15 @@ export type UpdateNotificationDestinationResponses = { useTLS: boolean; password?: string; username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2960,7 +3032,7 @@ export type UpdateNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; diff --git a/app/client/modules/notifications/components/create-notification-form.tsx b/app/client/modules/notifications/components/create-notification-form.tsx index 63697c13..8b913664 100644 --- a/app/client/modules/notifications/components/create-notification-form.tsx +++ b/app/client/modules/notifications/components/create-notification-form.tsx @@ -14,10 +14,19 @@ import { FormMessage, } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; -import { SecretInput } from "~/client/components/ui/secret-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; -import { Checkbox } from "~/client/components/ui/checkbox"; import { notificationConfigSchemaBase } from "~/schemas/notifications"; +import { + CustomForm, + DiscordForm, + EmailForm, + GenericForm, + GotifyForm, + NtfyForm, + PushoverForm, + SlackForm, + TelegramForm, +} from "./notification-forms"; export const formSchema = type({ name: "2<=string<=32", @@ -48,6 +57,9 @@ const defaultValuesForType = { slack: { type: "slack" as const, webhookUrl: "", + channel: "", + username: "", + iconEmoji: "", }, discord: { type: "discord" as const, @@ -75,6 +87,16 @@ const defaultValuesForType = { botToken: "", chatId: "", }, + generic: { + type: "generic" as const, + url: "", + method: "POST" as const, + contentType: "application/json", + headers: [], + useJson: true, + titleKey: "title", + messageKey: "message", + }, custom: { type: "custom" as const, shoutrrrUrl: "", @@ -84,7 +106,9 @@ const defaultValuesForType = { export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValues, formId, className }: Props) => { const form = useForm({ resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), - defaultValues: initialValues, + defaultValues: initialValues || { + name: "", + }, resetOptions: { keepDefaultValues: true, keepDirtyValues: false, @@ -97,7 +121,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue useEffect(() => { if (!initialValues) { form.reset({ - name: form.getValues().name, + name: form.getValues().name || "", ...defaultValuesForType[watchedType as keyof typeof defaultValuesForType], }); } @@ -152,6 +176,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue Ntfy Pushover Telegram + Generic Webhook Custom (Shoutrrr URL) @@ -161,545 +186,15 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue )} /> - {watchedType === "email" && ( - <> - ( - - SMTP Host - - - - - - )} - /> - ( - - SMTP Port - - field.onChange(Number(e.target.value))} - /> - - - - )} - /> - ( - - Username (Optional) - - - - - - )} - /> - ( - - Password (Optional) - - - - - - )} - /> - ( - - From Address - - - - - - )} - /> - ( - - To Addresses - - field.onChange(e.target.value.split(",").map((email) => email.trim()))} - /> - - Comma-separated list of recipient email addresses. - - - )} - /> - ( - - - - -
- Use TLS - Enable TLS encryption for SMTP connection. -
-
- )} - /> - - )} - - {watchedType === "slack" && ( - <> - ( - - Webhook URL - - - - Get this from your Slack app's Incoming Webhooks settings. - - - )} - /> - ( - - Channel (Optional) - - - - Override the default channel (use # for channels, @ for users). - - - )} - /> - ( - - Bot Username (Optional) - - - - - - )} - /> - ( - - Icon Emoji (Optional) - - - - - - )} - /> - - )} - - {watchedType === "discord" && ( - <> - ( - - Webhook URL - - - - Get this from your Discord server's Integrations settings. - - - )} - /> - ( - - Bot Username (Optional) - - - - - - )} - /> - ( - - Avatar URL (Optional) - - - - - - )} - /> - ( - - Thread ID (Optional) - - - - - ID of the thread to post messages in. Leave empty to post in the main channel. - - - - )} - /> - - )} - - {watchedType === "gotify" && ( - <> - ( - - Server URL - - - - Your self-hosted Gotify server URL. - - - )} - /> - ( - - App Token - - - - Application token from Gotify. - - - )} - /> - ( - - Priority - - field.onChange(Number(e.target.value))} - /> - - Priority level (0-10, where 10 is highest). - - - )} - /> - ( - - Path (Optional) - - - - Custom path on the Gotify server, if applicable. - - - )} - /> - - )} - - {watchedType === "ntfy" && ( - <> - ( - - Server URL (Optional) - - - - Leave empty to use ntfy.sh public service. - - - )} - /> - ( - - Topic - - - - The ntfy topic name to publish to. - - - )} - /> - ( - - Username (Optional) - - - - Username for server authentication, if required. - - - )} - /> - ( - - Password (Optional) - - - - Password for server authentication, if required. - - - )} - /> - ( - - Access token (Optional) - - - - - Access token for server authentication. Will take precedence over username/password if set. - - - - )} - /> - ( - - Priority - - - - )} - /> - - )} - - {watchedType === "pushover" && ( - <> - ( - - User Key - - - - Your Pushover user key from the dashboard. - - - )} - /> - ( - - API Token - - - - Application API token from your Pushover application. - - - )} - /> - ( - - Devices (Optional) - - - - Comma-separated list of device names. Leave empty for all devices. - - - )} - /> - ( - - Priority - - Message priority level. - - - )} - /> - - )} - - {watchedType === "telegram" && ( - <> - ( - - Bot Token - - - - - Telegram bot token. Get this from BotFather when you create your bot. - - - - )} - /> - ( - - Chat ID - - - - Telegram chat ID to send notifications to. - - - )} - /> - - )} - - {watchedType === "custom" && ( - ( - - Shoutrrr URL - - - - - Direct Shoutrrr URL for power users. See  - - Shoutrrr documentation - -  for supported services and URL formats. - - - - )} - /> - )} + {watchedType === "email" && } + {watchedType === "slack" && } + {watchedType === "discord" && } + {watchedType === "gotify" && } + {watchedType === "ntfy" && } + {watchedType === "pushover" && } + {watchedType === "telegram" && } + {watchedType === "generic" && } + {watchedType === "custom" && } ); diff --git a/app/client/modules/notifications/components/notification-forms/custom-form.tsx b/app/client/modules/notifications/components/notification-forms/custom-form.tsx new file mode 100644 index 00000000..babd7c40 --- /dev/null +++ b/app/client/modules/notifications/components/notification-forms/custom-form.tsx @@ -0,0 +1,41 @@ +import type { UseFormReturn } from "react-hook-form"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; +import { Input } from "~/client/components/ui/input"; +import type { NotificationFormValues } from "../create-notification-form"; + +type Props = { + form: UseFormReturn; +}; + +export const CustomForm = ({ form }: Props) => { + return ( + ( + + Shoutrrr URL + + + + + Direct Shoutrrr URL for power users. See  + + Shoutrrr documentation + +  for supported services and URL formats. + + + + )} + /> + ); +}; diff --git a/app/client/modules/notifications/components/notification-forms/discord-form.tsx b/app/client/modules/notifications/components/notification-forms/discord-form.tsx new file mode 100644 index 00000000..a8d5fb35 --- /dev/null +++ b/app/client/modules/notifications/components/notification-forms/discord-form.tsx @@ -0,0 +1,71 @@ +import type { UseFormReturn } from "react-hook-form"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; +import { Input } from "~/client/components/ui/input"; +import type { NotificationFormValues } from "../create-notification-form"; + +type Props = { + form: UseFormReturn; +}; + +export const DiscordForm = ({ form }: Props) => { + return ( + <> + ( + + Webhook URL + + + + Get this from your Discord server's Integrations settings. + + + )} + /> + ( + + Bot Username (Optional) + + + + + + )} + /> + ( + + Avatar URL (Optional) + + + + + + )} + /> + ( + + Thread ID (Optional) + + + + + ID of the thread to post messages in. Leave empty to post in the main channel. + + + + )} + /> + + ); +}; diff --git a/app/client/modules/notifications/components/notification-forms/email-form.tsx b/app/client/modules/notifications/components/notification-forms/email-form.tsx new file mode 100644 index 00000000..43a7daf8 --- /dev/null +++ b/app/client/modules/notifications/components/notification-forms/email-form.tsx @@ -0,0 +1,128 @@ +import type { UseFormReturn } from "react-hook-form"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; +import { Input } from "~/client/components/ui/input"; +import { SecretInput } from "~/client/components/ui/secret-input"; +import { Checkbox } from "~/client/components/ui/checkbox"; +import type { NotificationFormValues } from "../create-notification-form"; + +type Props = { + form: UseFormReturn; +}; + +export const EmailForm = ({ form }: Props) => { + return ( + <> + ( + + SMTP Host + + + + + + )} + /> + ( + + SMTP Port + + field.onChange(Number(e.target.value))} + /> + + + + )} + /> + ( + + Username (Optional) + + + + + + )} + /> + ( + + Password (Optional) + + + + + + )} + /> + ( + + From Address + + + + + + )} + /> + ( + + To Addresses + + + field.onChange( + e.target.value + .split(",") + .map((email) => email.trim()) + .filter(Boolean), + ) + } + /> + + Comma-separated list of recipient email addresses. + + + )} + /> + ( + + + + +
+ Use TLS + Enable TLS encryption for SMTP connection. +
+
+ )} + /> + + ); +}; diff --git a/app/client/modules/notifications/components/notification-forms/generic-form.tsx b/app/client/modules/notifications/components/notification-forms/generic-form.tsx new file mode 100644 index 00000000..d969939c --- /dev/null +++ b/app/client/modules/notifications/components/notification-forms/generic-form.tsx @@ -0,0 +1,173 @@ +import type { UseFormReturn } from "react-hook-form"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; +import { Input } from "~/client/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { Checkbox } from "~/client/components/ui/checkbox"; +import { Textarea } from "~/client/components/ui/textarea"; +import { CodeBlock } from "~/client/components/ui/code-block"; +import { Label } from "~/client/components/ui/label"; +import type { NotificationFormValues } from "../create-notification-form"; + +type Props = { + form: UseFormReturn; +}; + +const WebhookPreview = ({ values }: { values: Partial }) => { + if (values.type !== "generic") return null; + + const contentType = values.contentType || "application/json"; + const headers = values.headers || []; + const useJson = values.useJson; + const titleKey = values.titleKey || "title"; + const messageKey = values.messageKey || "message"; + + let body = ""; + if (useJson) { + body = JSON.stringify( + { + [titleKey]: "Notification title", + [messageKey]: "Notification message", + }, + null, + 2, + ); + } else { + body = "Notification message"; + } + + const previewCode = `${values.method} ${values.url}\nContent-Type: ${contentType}${headers.length > 0 ? `\n${headers.join("\n")}` : ""} + +${body}`; + + return ( +
+ + +

This is a preview of the HTTP request that will be sent.

+
+ ); +}; + +export const GenericForm = ({ form }: Props) => { + const watchedValues = form.watch(); + + return ( + <> + ( + + Webhook URL + + + + The target URL for the webhook. + + + )} + /> + ( + + Method + + + + )} + /> + ( + + Content Type + + + + + + )} + /> + ( + + Headers + +