From edeeaa56518f9b9b5c5b4fe8f39bb64856e9558b Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:00:28 +0200 Subject: [PATCH 1/9] feat: implement webhooks --- .../(dashboard)/settings/webhooks/page.tsx | 71 ++++++++ .../settings/layout/desktop-nav.tsx | 15 +- .../settings/layout/mobile-nav.tsx | 15 +- .../webhooks/create-webhook-dialog.tsx | 3 + .../webhooks/delete-webhook-dialog.tsx | 167 ++++++++++++++++++ apps/web/src/components/forms/webhook.tsx | 0 .../migration.sql | 19 ++ packages/prisma/schema.prisma | 18 ++ 8 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/settings/webhooks/page.tsx create mode 100644 apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx create mode 100644 apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx create mode 100644 apps/web/src/components/forms/webhook.tsx create mode 100644 packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx new file mode 100644 index 000000000..e7445c1d9 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { Zap } from 'lucide-react'; + +import { Button } from '@documenso/ui/primitives/button'; + +import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; +import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; + +export default function WebhookPage() { + // TODO: Fetch webhooks from the DB after implementing the backend + const webhooks = [ + { + id: 1, + secret: 'my-secret', + webhookUrl: 'https://example.com/webhook', + eventTriggers: ['document.created', 'document.signed'], + enabled: true, + userID: 1, + }, + ]; + + return ( +
+ + + + + {webhooks.length === 0 && ( + // TODO: Perhaps add some illustrations here to make the page more engaging +
+

+ You have no webhooks yet. Your webhooks will be shown here once you create them. +

+
+ )} + + {webhooks.length > 0 && ( +
+ {webhooks.map((webhook) => ( +
+
+
+

Webhook URL

+

{webhook.webhookUrl}

+

Event triggers

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

+ {trigger} +

+ ))} +
+
+
+ + + + +
+
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx index c7ab61d8a..5b7a1b739 100644 --- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx @@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { CreditCard, Lock, User, Users } from 'lucide-react'; +import { CreditCard, Lock, User, Users, Webhook } from 'lucide-react'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; @@ -48,6 +48,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { + + + + + + + + + )} + + + + + Delete Webhook + + + Please note that this action is irreversible. Once confirmed, your webhook will be + permanently deleted. + + + +
+ +
+ ( + + + Confirm by typing:{' '} + + {deleteMessage} + + + + + + + + )} + /> + + +
+ + + +
+
+
+
+ +
+ + ); +}; diff --git a/apps/web/src/components/forms/webhook.tsx b/apps/web/src/components/forms/webhook.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql b/packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql new file mode 100644 index 000000000..7bf4e190f --- /dev/null +++ b/packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql @@ -0,0 +1,19 @@ +-- CreateEnum +CREATE TYPE "WebhookTriggerEvents" AS ENUM ('DOCUMENT_CREATED', 'DOCUMENT_SIGNED'); + +-- CreateTable +CREATE TABLE "Webhook" ( + "id" SERIAL NOT NULL, + "webhookUrl" TEXT NOT NULL, + "eventTriggers" "WebhookTriggerEvents"[], + "secret" TEXT, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + + CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 79dcdf6aa..f2ce12cab 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -47,6 +47,7 @@ model User { VerificationToken VerificationToken[] Template Template[] securityAuditLogs UserSecurityAuditLog[] + Webhooks Webhook[] @@index([email]) } @@ -94,6 +95,23 @@ model VerificationToken { user User @relation(fields: [userId], references: [id], onDelete: Cascade) } +enum WebhookTriggerEvents { + DOCUMENT_CREATED + DOCUMENT_SIGNED +} + +model Webhook { + id Int @id @default(autoincrement()) + webhookUrl String + eventTriggers WebhookTriggerEvents[] + secret String? + enabled Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + enum SubscriptionStatus { ACTIVE PAST_DUE From b3514bd0c7ada00debf38e6609ab18d23a5a62f5 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:04:12 +0200 Subject: [PATCH 2/9] add new webhook dialog --- .../(dashboard)/settings/webhooks/page.tsx | 3 +- .../webhooks/create-webhook-dialog.tsx | 156 +++++++++++++++++- .../webhooks/multiselect-combobox.tsx | 83 ++++++++++ packages/tailwind-config/index.cjs | 3 + 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx index e7445c1d9..9ca4b526e 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -5,6 +5,7 @@ import { Zap } from 'lucide-react'; import { Button } from '@documenso/ui/primitives/button'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; +import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog'; import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; export default function WebhookPage() { @@ -26,7 +27,7 @@ export default function WebhookPage() { title="Webhooks" subtitle="On this page, you can create new Webhooks and manage the existing ones." > - + {webhooks.length === 0 && ( diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx index 2924efc86..ec6d0f152 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx @@ -1,3 +1,157 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useForm } from 'react-hook-form'; + +import { Button } from '@documenso/ui/primitives/button'; +import { Input } from '@documenso/ui/primitives/input'; +import { Switch } from '@documenso/ui/primitives/switch'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; + +import { MultiSelectCombobox } from './multiselect-combobox'; + + +export type CreateWebhookDialogProps = { + trigger?: React.ReactNode; +} & Omit; + export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => { - return

test

; + const router = useRouter(); + const [open, setOpen] = useState(false); + + const form = useForm<>({ + resolver: zodResolver(), + values: { + webhookUrl: '', + eventTriggers: [], + secret: '', + enabled: true, + }, + }); + + const onSubmit = async () => { + console.log('submitted'); + } + + return ( + !form.formState.isSubmitting && setOpen(value)} + {...props} + > + e.stopPropagation()} asChild> + {trigger ?? } + + + + + Create webhook + On this page, you can create a new webhook. + + +
+ +
+ ( + + Webhook URL + + + + + + )} + /> + + ( + + Event triggers + + { + console.log(values); + onChange(values) + }} + /> + + + + )} + /> + + ( + + Secret + + + + + + )} + /> + + ( + + Active + + + + + + )} + /> + + +
+ + +
+
+ +
+
+ +
+
+ ); }; diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx new file mode 100644 index 000000000..269b83449 --- /dev/null +++ b/apps/web/src/components/(dashboard)/settings/webhooks/multiselect-combobox.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; + +import { WebhookTriggerEvents } from '@prisma/client/'; +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +type ComboboxProps = { + listValues: string[]; + onChange: (_values: string[]) => void; +}; + +const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => { + const [isOpen, setIsOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + + const triggerEvents = Object.values(WebhookTriggerEvents); + + React.useEffect(() => { + setSelectedValues(listValues); + }, [listValues]); + + const allEvents = [...new Set([...triggerEvents, ...selectedValues])]; + + const handleSelect = (currentValue: string) => { + let newSelectedValues; + if (selectedValues.includes(currentValue)) { + newSelectedValues = selectedValues.filter((value) => value !== currentValue); + } else { + newSelectedValues = [...selectedValues, currentValue]; + } + + setSelectedValues(newSelectedValues); + onChange(newSelectedValues); + setIsOpen(false); + }; + + return ( + + + + + + + + No value found. + + {allEvents.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + + ); +}; + +export { MultiSelectCombobox }; diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index 1564454d8..6dfa7d5c2 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -11,6 +11,9 @@ module.exports = { sans: ['var(--font-sans)', ...fontFamily.sans], signature: ['var(--font-signature)'], }, + zIndex: { + 9999: '9999', + }, colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))', From ddb9dd11d7824c50825d772abcb943f0a641dbc1 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:07:33 +0200 Subject: [PATCH 3/9] feat: added backend stuff --- .../webhooks/create-webhook-dialog.tsx | 69 ++++++++++++++----- .../server-only/webhooks/create-webhook.ts | 28 ++++++++ .../webhooks/get-webhooks-by-user-id.ts | 9 +++ packages/trpc/server/router.ts | 2 + packages/trpc/server/webhook-router/router.ts | 35 ++++++++++ packages/trpc/server/webhook-router/schema.ts | 14 ++++ 6 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 packages/lib/server-only/webhooks/create-webhook.ts create mode 100644 packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts create mode 100644 packages/trpc/server/webhook-router/router.ts create mode 100644 packages/trpc/server/webhook-router/schema.ts diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx index ec6d0f152..7d4003fb5 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx @@ -1,16 +1,26 @@ 'use client'; import { useState } from 'react'; + import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { useForm } from 'react-hook-form'; +import type { z } from 'zod'; +import { trpc } from '@documenso/trpc/react'; +import { ZCreateWebhookFormSchema } from '@documenso/trpc/server/webhook-router/schema'; import { Button } from '@documenso/ui/primitives/button'; -import { Input } from '@documenso/ui/primitives/input'; -import { Switch } from '@documenso/ui/primitives/switch'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@documenso/ui/primitives/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; import { Form, FormControl, @@ -19,9 +29,13 @@ import { 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 { MultiSelectCombobox } from './multiselect-combobox'; +type TCreateWebhookFormSchema = z.infer; export type CreateWebhookDialogProps = { trigger?: React.ReactNode; @@ -29,10 +43,11 @@ export type CreateWebhookDialogProps = { export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => { const router = useRouter(); + const { toast } = useToast(); const [open, setOpen] = useState(false); - const form = useForm<>({ - resolver: zodResolver(), + const form = useForm({ + resolver: zodResolver(ZCreateWebhookFormSchema), values: { webhookUrl: '', eventTriggers: [], @@ -41,9 +56,30 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr }, }); - const onSubmit = async () => { - console.log('submitted'); - } + const { mutateAsync: createWebhook } = trpc.webhook.createWebhook.useMutation(); + + const onSubmit = async (values: TCreateWebhookFormSchema) => { + try { + await createWebhook(values); + + setOpen(false); + + toast({ + title: 'Webhook created', + description: 'The webhook was successfully created.', + }); + + form.reset(); + + router.refresh(); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while creating the webhook. Please try again.', + variant: 'destructive', + }); + } + }; return ( { console.log(values); - onChange(values) + onChange(values); }} /> @@ -101,28 +137,28 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr )} /> - ( Secret - + )} /> - ( Active -
-
- diff --git a/packages/lib/server-only/webhooks/create-webhook.ts b/packages/lib/server-only/webhooks/create-webhook.ts new file mode 100644 index 000000000..ee352c49f --- /dev/null +++ b/packages/lib/server-only/webhooks/create-webhook.ts @@ -0,0 +1,28 @@ +import { prisma } from '@documenso/prisma'; +import type { WebhookTriggerEvents } from '@documenso/prisma/client'; + +export interface CreateWebhookOptions { + webhookUrl: string; + eventTriggers: WebhookTriggerEvents[]; + secret: string | null; + enabled: boolean; + userId: number; +} + +export const createWebhook = async ({ + webhookUrl, + eventTriggers, + secret, + enabled, + userId, +}: CreateWebhookOptions) => { + return await prisma.webhook.create({ + data: { + webhookUrl, + eventTriggers, + secret, + enabled, + userId, + }, + }); +}; diff --git a/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts new file mode 100644 index 000000000..a775ac30c --- /dev/null +++ b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts @@ -0,0 +1,9 @@ +import { prisma } from '@documenso/prisma'; + +export const getWebhooksByUserId = async (userId: number) => { + return await prisma.webhook.findMany({ + where: { + userId, + }, + }); +}; diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index aec70fd63..571d43669 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -11,6 +11,7 @@ import { teamRouter } from './team-router/router'; import { templateRouter } from './template-router/router'; import { router } from './trpc'; import { twoFactorAuthenticationRouter } from './two-factor-authentication-router/router'; +import { webhookRouter } from './webhook-router/router'; export const appRouter = router({ auth: authRouter, @@ -24,6 +25,7 @@ export const appRouter = router({ singleplayer: singleplayerRouter, team: teamRouter, template: templateRouter, + webhook: webhookRouter, twoFactorAuthentication: twoFactorAuthenticationRouter, }); diff --git a/packages/trpc/server/webhook-router/router.ts b/packages/trpc/server/webhook-router/router.ts new file mode 100644 index 000000000..ffdd7c4bf --- /dev/null +++ b/packages/trpc/server/webhook-router/router.ts @@ -0,0 +1,35 @@ +import { TRPCError } from '@trpc/server'; + +import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook'; +import { getWebhooksByUserId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-user-id'; + +import { authenticatedProcedure, router } from '../trpc'; +import { ZCreateWebhookFormSchema } from './schema'; + +export const webhookRouter = router({ + getWebhooks: authenticatedProcedure.query(async ({ ctx }) => { + try { + return await getWebhooksByUserId(ctx.user.id); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to fetch your webhooks. Please try again later.', + }); + } + }), + createWebhook: authenticatedProcedure + .input(ZCreateWebhookFormSchema) + .mutation(async ({ input, ctx }) => { + try { + return await createWebhook({ + ...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 new file mode 100644 index 000000000..feceac054 --- /dev/null +++ b/packages/trpc/server/webhook-router/schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +import { WebhookTriggerEvents } from '@documenso/prisma/client'; + +export const ZCreateWebhookFormSchema = z.object({ + webhookUrl: z.string().url(), + eventTriggers: z + .array(z.nativeEnum(WebhookTriggerEvents)) + .min(1, { message: 'At least one event trigger is required' }), + secret: z.string().nullable(), + enabled: z.boolean(), +}); + +export type TCreateWebhookFormSchema = z.infer; From 0209127136556074cc026fb621b513b1c258e186 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:28:18 +0200 Subject: [PATCH 4/9] feat: delete webhook functionality --- .../(dashboard)/settings/webhooks/page.tsx | 29 ++++++++++--------- .../webhooks/create-webhook-dialog.tsx | 1 - .../webhooks/delete-webhook-dialog.tsx | 2 +- .../webhooks/delete-webhook-by-id.ts | 15 ++++++++++ packages/trpc/server/webhook-router/router.ts | 19 ++++++++++++ packages/trpc/server/webhook-router/schema.ts | 6 ++++ 6 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 packages/lib/server-only/webhooks/delete-webhook-by-id.ts diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx index 9ca4b526e..060257d72 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -1,7 +1,9 @@ 'use client'; import { Zap } from 'lucide-react'; +import { ToggleLeft, ToggleRight } from 'lucide-react'; +import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; @@ -9,17 +11,7 @@ import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/ import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; export default function WebhookPage() { - // TODO: Fetch webhooks from the DB after implementing the backend - const webhooks = [ - { - id: 1, - secret: 'my-secret', - webhookUrl: 'https://example.com/webhook', - eventTriggers: ['document.created', 'document.signed'], - enabled: true, - userID: 1, - }, - ]; + const { data: webhooks } = trpc.webhook.getWebhooks.useQuery(); return (
@@ -30,7 +22,7 @@ export default function WebhookPage() { - {webhooks.length === 0 && ( + {webhooks?.length === 0 && ( // TODO: Perhaps add some illustrations here to make the page more engaging

@@ -39,9 +31,9 @@ export default function WebhookPage() {

)} - {webhooks.length > 0 && ( + {webhooks?.length > 0 && (
- {webhooks.map((webhook) => ( + {webhooks?.map((webhook) => (
@@ -53,6 +45,15 @@ export default function WebhookPage() { {trigger}

))} + {webhook.enabled ? ( +

+ Active +

+ ) : ( +

+ Inactive +

+ )}
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx index 7d4003fb5..0e24b04a7 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx @@ -127,7 +127,6 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr { - console.log(values); onChange(values); }} /> diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx index 540bcf657..8f4a4008f 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx @@ -52,7 +52,7 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr type TDeleteWebhookFormSchema = z.infer; - const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhookById.useMutation(); + const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhook.useMutation(); const form = useForm({ resolver: zodResolver(ZDeleteWebhookFormSchema), diff --git a/packages/lib/server-only/webhooks/delete-webhook-by-id.ts b/packages/lib/server-only/webhooks/delete-webhook-by-id.ts new file mode 100644 index 000000000..306d0ca9c --- /dev/null +++ b/packages/lib/server-only/webhooks/delete-webhook-by-id.ts @@ -0,0 +1,15 @@ +import { prisma } from '@documenso/prisma'; + +export type DeleteWebhookByIdOptions = { + id: number; + userId: number; +}; + +export const deleteWebhookById = async ({ id, userId }: DeleteWebhookByIdOptions) => { + return await prisma.webhook.delete({ + where: { + id, + userId, + }, + }); +}; diff --git a/packages/trpc/server/webhook-router/router.ts b/packages/trpc/server/webhook-router/router.ts index ffdd7c4bf..6598b856a 100644 --- a/packages/trpc/server/webhook-router/router.ts +++ b/packages/trpc/server/webhook-router/router.ts @@ -1,10 +1,12 @@ 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 { 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'; export const webhookRouter = router({ getWebhooks: authenticatedProcedure.query(async ({ ctx }) => { @@ -32,4 +34,21 @@ export const webhookRouter = router({ }); } }), + deleteWebhook: authenticatedProcedure + .input(ZDeleteWebhookSchema) + .mutation(async ({ input, ctx }) => { + try { + const { id } = input; + + return await deleteWebhookById({ + id, + 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 feceac054..aba409c2f 100644 --- a/packages/trpc/server/webhook-router/schema.ts +++ b/packages/trpc/server/webhook-router/schema.ts @@ -11,4 +11,10 @@ export const ZCreateWebhookFormSchema = z.object({ enabled: z.boolean(), }); +export const ZDeleteWebhookSchema = z.object({ + id: z.number(), +}); + export type TCreateWebhookFormSchema = z.infer; + +export type TDeleteWebhookSchema = z.infer; From 61958989b46db31efcc6fe6b6c9d711d089d26d4 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Wed, 14 Feb 2024 14:38:58 +0200 Subject: [PATCH 5/9] feat: more webhook functionality --- .../settings/webhooks/[id]/page.tsx | 162 ++++++++++++++++++ .../(dashboard)/settings/webhooks/page.tsx | 16 +- .../webhooks/multiselect-combobox.tsx | 6 +- .../lib/server-only/user/get-user-webhooks.ts | 17 ++ .../lib/server-only/webhooks/edit-webhook.ts | 21 +++ .../server-only/webhooks/get-webhook-by-id.ts | 15 ++ packages/trpc/server/webhook-router/router.ts | 48 +++++- packages/trpc/server/webhook-router/schema.ts | 16 +- 8 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx create mode 100644 packages/lib/server-only/user/get-user-webhooks.ts create mode 100644 packages/lib/server-only/webhooks/edit-webhook.ts create mode 100644 packages/lib/server-only/webhooks/get-webhook-by-id.ts 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; From 019db27b1d4b040cdbacf2df791f37efbd362478 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:04:11 +0200 Subject: [PATCH 6/9] feat: trigger webhook functionality --- .../document/complete-document-with-token.ts | 27 +++++++++---- .../server-only/document/create-document.ts | 12 +++++- .../server-only/webhooks/get-all-webhooks.ts | 17 ++++++++ .../lib/universal/post-webhook-payload.ts | 39 +++++++++++++++++++ packages/lib/universal/trigger-webhook.ts | 30 ++++++++++++++ 5 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 packages/lib/server-only/webhooks/get-all-webhooks.ts create mode 100644 packages/lib/universal/post-webhook-payload.ts create mode 100644 packages/lib/universal/trigger-webhook.ts diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 62db516fa..b1438f0ce 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -2,7 +2,9 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; +import { WebhookTriggerEvents } from '@documenso/prisma/client'; +import { triggerWebhook } from '../../universal/trigger-webhook'; import { sealDocument } from './seal-document'; import { sendPendingEmail } from './send-pending-email'; @@ -11,13 +13,8 @@ export type CompleteDocumentWithTokenOptions = { documentId: number; }; -export const completeDocumentWithToken = async ({ - token, - documentId, -}: CompleteDocumentWithTokenOptions) => { - 'use server'; - - const document = await prisma.document.findFirstOrThrow({ +const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => { + return await prisma.document.findFirstOrThrow({ where: { id: documentId, Recipient: { @@ -34,6 +31,15 @@ export const completeDocumentWithToken = async ({ }, }, }); +}; + +export const completeDocumentWithToken = async ({ + token, + documentId, +}: CompleteDocumentWithTokenOptions) => { + 'use server'; + + const document = await getDocument({ token, documentId }); if (document.status === DocumentStatus.COMPLETED) { throw new Error(`Document ${document.id} has already been completed`); @@ -101,4 +107,11 @@ export const completeDocumentWithToken = async ({ if (documents.count > 0) { await sealDocument({ documentId: document.id }); } + + const updatedDocument = await getDocument({ token, documentId }); + + await triggerWebhook({ + eventTrigger: WebhookTriggerEvents.DOCUMENT_SIGNED, + documentData: updatedDocument, + }); }; diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index 93307a7b4..82dacfba7 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -1,6 +1,9 @@ 'use server'; import { prisma } from '@documenso/prisma'; +import { WebhookTriggerEvents } from '@documenso/prisma/client'; + +import { triggerWebhook } from '../../universal/trigger-webhook'; export type CreateDocumentOptions = { title: string; @@ -29,7 +32,7 @@ export const createDocument = async ({ }); } - return await tx.document.create({ + const createdDocument = await tx.document.create({ data: { title, documentDataId, @@ -37,5 +40,12 @@ export const createDocument = async ({ teamId, }, }); + + await triggerWebhook({ + eventTrigger: WebhookTriggerEvents.DOCUMENT_CREATED, + documentData: createdDocument, + }); + + return createdDocument; }); }; diff --git a/packages/lib/server-only/webhooks/get-all-webhooks.ts b/packages/lib/server-only/webhooks/get-all-webhooks.ts new file mode 100644 index 000000000..a6c88a086 --- /dev/null +++ b/packages/lib/server-only/webhooks/get-all-webhooks.ts @@ -0,0 +1,17 @@ +import { prisma } from '@documenso/prisma'; +import type { WebhookTriggerEvents } from '@documenso/prisma/client'; + +export type GetAllWebhooksOptions = { + eventTrigger: WebhookTriggerEvents; +}; + +export const getAllWebhooks = async ({ eventTrigger }: GetAllWebhooksOptions) => { + return prisma.webhook.findMany({ + where: { + eventTriggers: { + has: eventTrigger, + }, + enabled: true, + }, + }); +}; diff --git a/packages/lib/universal/post-webhook-payload.ts b/packages/lib/universal/post-webhook-payload.ts new file mode 100644 index 000000000..80ddea80d --- /dev/null +++ b/packages/lib/universal/post-webhook-payload.ts @@ -0,0 +1,39 @@ +import type { Document, Webhook } from '@documenso/prisma/client'; + +export type PostWebhookPayloadOptions = { + webhookData: Pick; + documentData: Document; +}; + +export const postWebhookPayload = async ({ + webhookData, + documentData, +}: PostWebhookPayloadOptions) => { + const { webhookUrl, secret } = webhookData; + + const payload = { + event: webhookData.eventTriggers.toString(), + createdAt: new Date().toISOString(), + webhookEndpoint: webhookUrl, + payload: documentData, + }; + + const response = await fetch(webhookUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + 'X-Documenso-Secret': secret ?? '', + }, + }); + + if (!response.ok) { + throw new Error(`Webhook failed with the status code ${response.status}`); + } + + return { + status: response.status, + statusText: response.statusText, + message: 'Webhook sent successfully', + }; +}; diff --git a/packages/lib/universal/trigger-webhook.ts b/packages/lib/universal/trigger-webhook.ts new file mode 100644 index 000000000..025a154bc --- /dev/null +++ b/packages/lib/universal/trigger-webhook.ts @@ -0,0 +1,30 @@ +import type { Document, WebhookTriggerEvents } from '@documenso/prisma/client'; + +import { getAllWebhooks } from '../server-only/webhooks/get-all-webhooks'; +import { postWebhookPayload } from './post-webhook-payload'; + +export type TriggerWebhookOptions = { + eventTrigger: WebhookTriggerEvents; + documentData: Document; +}; + +export const triggerWebhook = async ({ eventTrigger, documentData }: TriggerWebhookOptions) => { + try { + const allWebhooks = await getAllWebhooks({ eventTrigger }); + + const webhookPromises = allWebhooks.map((webhook) => { + const { webhookUrl, secret } = webhook; + + postWebhookPayload({ + webhookData: { webhookUrl, secret, eventTriggers: [eventTrigger] }, + documentData, + }).catch((_err) => { + throw new Error(`Failed to send webhook to ${webhookUrl}`); + }); + }); + + return Promise.all(webhookPromises); + } catch (err) { + throw new Error(`Failed to trigger webhook`); + } +}; From 7f3f6f531232619ee574734cca4b8a44c990f733 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:44:03 +0200 Subject: [PATCH 7/9] feat: hide secret field --- .../src/app/(dashboard)/settings/webhooks/[id]/page.tsx | 3 ++- .../settings/webhooks/create-webhook-dialog.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx index 56a1e90a9..785536c2a 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx @@ -18,6 +18,7 @@ import { FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { Switch } from '@documenso/ui/primitives/switch'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -125,7 +126,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) { Secret - + diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx index 0e24b04a7..2d4dc733e 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx @@ -30,6 +30,7 @@ import { FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { Switch } from '@documenso/ui/primitives/switch'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -143,7 +144,11 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr Secret - + From cd240ae8a4e29a0471c76b832891b8c07e1a09e6 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:44:28 +0200 Subject: [PATCH 8/9] chore: loading spinner --- .../src/app/(dashboard)/settings/webhooks/[id]/page.tsx | 8 +++++++- apps/web/src/app/(dashboard)/settings/webhooks/page.tsx | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx index 785536c2a..b9f04790c 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import type { z } from 'zod'; @@ -39,7 +40,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) { const { toast } = useToast(); const router = useRouter(); - const { data: webhook } = trpc.webhook.getWebhookById.useQuery( + const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery( { id: Number(params.id), }, @@ -87,6 +88,11 @@ export default function WebhookPage({ params }: WebhookPageOptions) { title="Edit webhook" subtitle="On this page, you can edit the webhook and its settings." /> + {isLoading && ( +
+ +
+ )}
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx index 638443bf9..d0532c065 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { Zap } from 'lucide-react'; import { ToggleLeft, ToggleRight } from 'lucide-react'; +import { Loader } from 'lucide-react'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; @@ -13,7 +14,7 @@ import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/ import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog'; export default function WebhookPage() { - const { data: webhooks } = trpc.webhook.getWebhooks.useQuery(); + const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery(); return (
@@ -24,6 +25,12 @@ export default function WebhookPage() { + {isLoading && ( +
+ +
+ )} + {webhooks && webhooks.length === 0 && ( // TODO: Perhaps add some illustrations here to make the page more engaging
From 26d4bbf0103b5252d857faaacc345dea237d22cd Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:58:03 +0200 Subject: [PATCH 9/9] chore: ui updates --- apps/web/src/app/(dashboard)/settings/webhooks/page.tsx | 2 +- .../(dashboard)/settings/webhooks/create-webhook-dialog.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx index d0532c065..d36de6726 100644 --- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -65,7 +65,7 @@ export default function WebhookPage() { )}
-
+
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx index 2d4dc733e..c32493c4f 100644 --- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx +++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx @@ -92,7 +92,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr {trigger ?? } - + Create webhook On this page, you can create a new webhook.