diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx new file mode 100644 index 000000000..56a1e90a9 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import type { z } from 'zod'; + +import { trpc } from '@documenso/trpc/react'; +import { ZEditWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { Switch } from '@documenso/ui/primitives/switch'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; +import { MultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/multiselect-combobox'; + +const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true }); + +type TEditWebhookFormSchema = z.infer; + +export type WebhookPageOptions = { + params: { + id: number; + }; +}; + +export default function WebhookPage({ params }: WebhookPageOptions) { + const { toast } = useToast(); + const router = useRouter(); + + const { data: webhook } = trpc.webhook.getWebhookById.useQuery( + { + id: Number(params.id), + }, + { enabled: !!params.id }, + ); + + const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZEditWebhookFormSchema), + values: { + webhookUrl: webhook?.webhookUrl ?? '', + eventTriggers: webhook?.eventTriggers ?? [], + secret: webhook?.secret ?? '', + enabled: webhook?.enabled ?? true, + }, + }); + + const onSubmit = async (data: TEditWebhookFormSchema) => { + try { + await updateWebhook({ + id: Number(params.id), + ...data, + }); + + toast({ + title: 'Webhook updated', + description: 'The webhook has been updated successfully.', + duration: 5000, + }); + + router.refresh(); + } catch (err) { + toast({ + title: 'Failed to update webhook', + description: 'We encountered an error while updating the webhook. Please try again later.', + variant: 'destructive', + }); + } + }; + + return ( +
+ +
+ +
+ ( + + Webhook URL + + + + )} + /> + ( + + Event triggers + + { + onChange(values); + }} + /> + + + + )} + /> + ( + + Secret + + + + + + )} + /> + + ( + + Active + + + + + + )} + /> +
+ +
+
+
+ +
+ ); +} diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx index 060257d72..638443bf9 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -1,5 +1,7 @@ 'use client'; +import Link from 'next/link'; + import { Zap } from 'lucide-react'; import { ToggleLeft, ToggleRight } from 'lucide-react'; @@ -22,7 +24,7 @@ export default function WebhookPage() { - {webhooks?.length === 0 && ( + {webhooks && webhooks.length === 0 && ( // TODO: Perhaps add some illustrations here to make the page more engaging

@@ -31,7 +33,7 @@ export default function WebhookPage() {

)} - {webhooks?.length > 0 && ( + {webhooks && webhooks.length > 0 && (
{webhooks?.map((webhook) => (
@@ -41,9 +43,9 @@ export default function WebhookPage() {

{webhook.webhookUrl}

Event triggers

{webhook.eventTriggers.map((trigger, index) => ( -

- {trigger} -

+ + {trigger} + ))} {webhook.enabled ? (

@@ -57,8 +59,8 @@ export default function WebhookPage() {

- diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx index 269b83449..2adbaeb7a 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx @@ -14,6 +14,8 @@ import { } from '@documenso/ui/primitives/command'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; +import { truncateTitle } from '~/helpers/truncate-title'; + type ComboboxProps = { listValues: string[]; onChange: (_values: string[]) => void; @@ -53,13 +55,13 @@ const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => { aria-expanded={isOpen} className="w-[200px] justify-between" > - {selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'} + {selectedValues.length > 0 ? selectedValues.length + ' selected...' : 'Select values...'} - + No value found. {allEvents.map((value: string, i: number) => ( diff --git a/packages/lib/server-only/user/get-user-webhooks.ts b/packages/lib/server-only/user/get-user-webhooks.ts new file mode 100644 index 000000000..26c47e0f4 --- /dev/null +++ b/packages/lib/server-only/user/get-user-webhooks.ts @@ -0,0 +1,17 @@ +import { prisma } from '@documenso/prisma'; + +export interface GetUserWebhooksByIdOptions { + id: number; +} + +export const getUserWebhooksById = async ({ id }: GetUserWebhooksByIdOptions) => { + return await prisma.user.findFirstOrThrow({ + where: { + id, + }, + select: { + email: true, + Webhooks: true, + }, + }); +}; diff --git a/packages/lib/server-only/webhooks/edit-webhook.ts b/packages/lib/server-only/webhooks/edit-webhook.ts new file mode 100644 index 000000000..4177bb2bf --- /dev/null +++ b/packages/lib/server-only/webhooks/edit-webhook.ts @@ -0,0 +1,21 @@ +import type { Prisma } from '@prisma/client'; + +import { prisma } from '@documenso/prisma'; + +export type EditWebhookOptions = { + id: number; + data: Prisma.WebhookUpdateInput; + userId: number; +}; + +export const editWebhook = async ({ id, data, userId }: EditWebhookOptions) => { + return await prisma.webhook.update({ + where: { + id, + userId, + }, + data: { + ...data, + }, + }); +}; diff --git a/packages/lib/server-only/webhooks/get-webhook-by-id.ts b/packages/lib/server-only/webhooks/get-webhook-by-id.ts new file mode 100644 index 000000000..82dbb70ef --- /dev/null +++ b/packages/lib/server-only/webhooks/get-webhook-by-id.ts @@ -0,0 +1,15 @@ +import { prisma } from '@documenso/prisma'; + +export type GetWebhookByIdOptions = { + id: number; + userId: number; +}; + +export const getWebhookById = async ({ id, userId }: GetWebhookByIdOptions) => { + return await prisma.webhook.findFirstOrThrow({ + where: { + id, + userId, + }, + }); +}; diff --git a/packages/trpc/server/webhook-router/router.ts b/packages/trpc/server/webhook-router/router.ts index 6598b856a..aeb7e6f38 100644 --- a/packages/trpc/server/webhook-router/router.ts +++ b/packages/trpc/server/webhook-router/router.ts @@ -2,11 +2,17 @@ import { TRPCError } from '@trpc/server'; import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook'; import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id'; +import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook'; +import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id'; import { getWebhooksByUserId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-user-id'; import { authenticatedProcedure, router } from '../trpc'; -import { ZCreateWebhookFormSchema } from './schema'; -import { ZDeleteWebhookSchema } from './schema'; +import { + ZCreateWebhookFormSchema, + ZDeleteWebhookMutationSchema, + ZEditWebhookMutationSchema, + ZGetWebhookByIdQuerySchema, +} from './schema'; export const webhookRouter = router({ getWebhooks: authenticatedProcedure.query(async ({ ctx }) => { @@ -19,6 +25,24 @@ export const webhookRouter = router({ }); } }), + getWebhookById: authenticatedProcedure + .input(ZGetWebhookByIdQuerySchema) + .query(async ({ input, ctx }) => { + try { + const { id } = input; + + return await getWebhookById({ + id, + userId: ctx.user.id, + }); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to fetch your webhook. Please try again later.', + }); + } + }), + createWebhook: authenticatedProcedure .input(ZCreateWebhookFormSchema) .mutation(async ({ input, ctx }) => { @@ -35,7 +59,7 @@ export const webhookRouter = router({ } }), deleteWebhook: authenticatedProcedure - .input(ZDeleteWebhookSchema) + .input(ZDeleteWebhookMutationSchema) .mutation(async ({ input, ctx }) => { try { const { id } = input; @@ -51,4 +75,22 @@ export const webhookRouter = router({ }); } }), + editWebhook: authenticatedProcedure + .input(ZEditWebhookMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { id } = input; + + return await editWebhook({ + id, + data: input, + userId: ctx.user.id, + }); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to create this webhook. Please try again later.', + }); + } + }), }); diff --git a/packages/trpc/server/webhook-router/schema.ts b/packages/trpc/server/webhook-router/schema.ts index aba409c2f..def654a70 100644 --- a/packages/trpc/server/webhook-router/schema.ts +++ b/packages/trpc/server/webhook-router/schema.ts @@ -11,10 +11,22 @@ export const ZCreateWebhookFormSchema = z.object({ enabled: z.boolean(), }); -export const ZDeleteWebhookSchema = z.object({ +export const ZGetWebhookByIdQuerySchema = z.object({ + id: z.number(), +}); + +export const ZEditWebhookMutationSchema = ZCreateWebhookFormSchema.extend({ + id: z.number(), +}); + +export const ZDeleteWebhookMutationSchema = z.object({ id: z.number(), }); export type TCreateWebhookFormSchema = z.infer; -export type TDeleteWebhookSchema = z.infer; +export type TGetWebhookByIdQuerySchema = z.infer; + +export type TDeleteWebhookMutationSchema = z.infer; + +export type TEditWebhookMutationSchema = z.infer;