mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
feat: implement webhooks (#913)
This commit is contained in:
201
apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
Normal file
201
apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
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,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
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';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
|
||||
|
||||
const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
|
||||
|
||||
type TEditWebhookFormSchema = z.infer<typeof ZEditWebhookFormSchema>;
|
||||
|
||||
export type WebhookPageOptions = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
|
||||
{
|
||||
id: params.id,
|
||||
},
|
||||
{ enabled: !!params.id },
|
||||
);
|
||||
|
||||
const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
|
||||
|
||||
const form = useForm<TEditWebhookFormSchema>({
|
||||
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: 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 (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Edit webhook"
|
||||
subtitle="On this page, you can edit the webhook and its settings."
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full max-w-xl flex-col gap-y-6"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The URL for Documenso to send webhook events to.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enabled</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="bg-background"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="eventTriggers"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel required>Triggers</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerMultiSelectCombobox
|
||||
listValues={value}
|
||||
onChange={(values: string[]) => {
|
||||
onChange(values);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The events that will trigger a webhook to be sent to your URL.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput className="bg-background" {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
A secret that will be sent to your URL so you can verify that the request has
|
||||
been sent by Documenso.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Update webhook
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
Normal file
101
apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
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';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export default function WebhookPage() {
|
||||
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Webhooks"
|
||||
subtitle="On this page, you can create new Webhooks and manage the existing ones."
|
||||
>
|
||||
<CreateWebhookDialog />
|
||||
</SettingsHeader>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{webhooks && webhooks.length === 0 && (
|
||||
// TODO: Perhaps add some illustrations here to make the page more engaging
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
You have no webhooks yet. Your webhooks will be shown here once you create them.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{webhooks && webhooks.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{webhooks?.map((webhook) => (
|
||||
<div
|
||||
key={webhook.id}
|
||||
className={cn(
|
||||
'border-border rounded-lg border p-4',
|
||||
!webhook.enabled && 'bg-muted/40',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-x-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="truncate font-mono text-xs">{webhook.id}</div>
|
||||
|
||||
<div className="mt-1.5 flex items-center gap-4">
|
||||
<h5
|
||||
className="max-w-[30rem] truncate text-sm sm:max-w-[18rem]"
|
||||
title={webhook.webhookUrl}
|
||||
>
|
||||
{webhook.webhookUrl}
|
||||
</h5>
|
||||
|
||||
<Badge variant={webhook.enabled ? 'neutral' : 'warning'} size="small">
|
||||
{webhook.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Listening to{' '}
|
||||
{webhook.eventTriggers
|
||||
.map((trigger) => toFriendlyWebhookEventName(trigger))
|
||||
.join(', ')}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-shrink-0 gap-4 sm:mt-0">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/settings/webhooks/${webhook.id}`}>Edit</Link>
|
||||
</Button>
|
||||
<DeleteWebhookDialog webhook={webhook}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
</DeleteWebhookDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
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,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
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';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
|
||||
|
||||
type TEditWebhookFormSchema = z.infer<typeof ZEditWebhookFormSchema>;
|
||||
|
||||
export type WebhookPageOptions = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
|
||||
{
|
||||
id: params.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
{ enabled: !!params.id && !!team.id },
|
||||
);
|
||||
|
||||
const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
|
||||
|
||||
const form = useForm<TEditWebhookFormSchema>({
|
||||
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: params.id,
|
||||
teamId: team.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 (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Edit webhook"
|
||||
subtitle="On this page, you can edit the webhook and its settings."
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full max-w-xl flex-col gap-y-6"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The URL for Documenso to send webhook events to.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enabled</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="bg-background"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="eventTriggers"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel required>Triggers</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerMultiSelectCombobox
|
||||
listValues={value}
|
||||
onChange={(values: string[]) => {
|
||||
onChange(values);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The events that will trigger a webhook to be sent to your URL.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput className="bg-background" {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
A secret that will be sent to your URL so you can verify that the request has
|
||||
been sent by Documenso.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Update webhook
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
Normal file
106
apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
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';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function WebhookPage() {
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Webhooks"
|
||||
subtitle="On this page, you can create new Webhooks and manage the existing ones."
|
||||
>
|
||||
<CreateWebhookDialog />
|
||||
</SettingsHeader>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{webhooks && webhooks.length === 0 && (
|
||||
// TODO: Perhaps add some illustrations here to make the page more engaging
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
You have no webhooks yet. Your webhooks will be shown here once you create them.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{webhooks && webhooks.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{webhooks?.map((webhook) => (
|
||||
<div
|
||||
key={webhook.id}
|
||||
className={cn(
|
||||
'border-border rounded-lg border p-4',
|
||||
!webhook.enabled && 'bg-muted/40',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-x-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="truncate font-mono text-xs">{webhook.id}</div>
|
||||
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<h5
|
||||
className="max-w-[30rem] truncate text-sm sm:max-w-[18rem]"
|
||||
title={webhook.webhookUrl}
|
||||
>
|
||||
{webhook.webhookUrl}
|
||||
</h5>
|
||||
|
||||
<Badge variant={webhook.enabled ? 'neutral' : 'warning'} size="small">
|
||||
{webhook.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Listening to{' '}
|
||||
{webhook.eventTriggers
|
||||
.map((trigger) => toFriendlyWebhookEventName(trigger))
|
||||
.join(', ')}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-shrink-0 gap-4 sm:mt-0">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/t/${team.url}/settings/webhooks/${webhook.id}`}>Edit</Link>
|
||||
</Button>
|
||||
<DeleteWebhookDialog webhook={webhook}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
</DeleteWebhookDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { Braces, 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';
|
||||
@ -77,6 +77,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/webhooks') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
Webhooks
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isBillingEnabled && (
|
||||
<Link href="/settings/billing">
|
||||
<Button
|
||||
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { Braces, 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';
|
||||
@ -80,6 +80,19 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/webhooks') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
Webhooks
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isBillingEnabled && (
|
||||
<Link href="/settings/billing">
|
||||
<Button
|
||||
|
||||
@ -0,0 +1,232 @@
|
||||
'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 { ZCreateWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
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';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
|
||||
|
||||
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
||||
|
||||
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
||||
|
||||
export type CreateWebhookDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<TCreateWebhookFormSchema>({
|
||||
resolver: zodResolver(ZCreateWebhookFormSchema),
|
||||
values: {
|
||||
webhookUrl: '',
|
||||
eventTriggers: [],
|
||||
secret: '',
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createWebhook } = trpc.webhook.createWebhook.useMutation();
|
||||
|
||||
const onSubmit = async ({
|
||||
enabled,
|
||||
eventTriggers,
|
||||
secret,
|
||||
webhookUrl,
|
||||
}: TCreateWebhookFormSchema) => {
|
||||
try {
|
||||
await createWebhook({
|
||||
enabled,
|
||||
eventTriggers,
|
||||
secret,
|
||||
webhookUrl,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
|
||||
{...props}
|
||||
>
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
|
||||
{trigger ?? <Button className="flex-shrink-0">Create Webhook</Button>}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-lg" position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create webhook</DialogTitle>
|
||||
<DialogDescription>On this page, you can create a new webhook.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full flex-col space-y-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The URL for Documenso to send webhook events to.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enabled</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="bg-background"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="eventTriggers"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel required>Triggers</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerMultiSelectCombobox
|
||||
listValues={value}
|
||||
onChange={(values: string[]) => {
|
||||
onChange(values);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The events that will trigger a webhook to be sent to your URL.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Secret</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
className="bg-background"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
A secret that will be sent to your URL so you can verify that the request has
|
||||
been sent by Documenso.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-nowrap gap-4">
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,172 @@
|
||||
'use effect';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Webhook } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DeleteWebhookDialogProps = {
|
||||
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
||||
onDelete?: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const deleteMessage = `delete ${webhook.webhookUrl}`;
|
||||
|
||||
const ZDeleteWebhookFormSchema = z.object({
|
||||
webhookUrl: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
|
||||
}),
|
||||
});
|
||||
|
||||
type TDeleteWebhookFormSchema = z.infer<typeof ZDeleteWebhookFormSchema>;
|
||||
|
||||
const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhook.useMutation();
|
||||
|
||||
const form = useForm<TDeleteWebhookFormSchema>({
|
||||
resolver: zodResolver(ZDeleteWebhookFormSchema),
|
||||
values: {
|
||||
webhookUrl: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await deleteWebhook({ id: webhook.id, teamId: team?.id });
|
||||
|
||||
toast({
|
||||
title: 'Webhook deleted',
|
||||
duration: 5000,
|
||||
description: 'The webhook has been successfully deleted.',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
description:
|
||||
'We encountered an unknown error while attempting to delete it. Please try again later.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
{children ?? (
|
||||
<Button className="mr-4" variant="destructive">
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Webhook</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
Please note that this action is irreversible. Once confirmed, your webhook will be
|
||||
permanently deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full flex-col space-y-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Confirm by typing:{' '}
|
||||
<span className="font-sm text-destructive font-semibold">
|
||||
{deleteMessage}
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-nowrap gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
disabled={!form.formState.isValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
I'm sure! Delete it
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { WebhookTriggerEvents } from '@prisma/client/';
|
||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||
|
||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||
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';
|
||||
|
||||
import { truncateTitle } from '~/helpers/truncate-title';
|
||||
|
||||
type TriggerMultiSelectComboboxProps = {
|
||||
listValues: string[];
|
||||
onChange: (_values: string[]) => void;
|
||||
};
|
||||
|
||||
export const TriggerMultiSelectCombobox = ({
|
||||
listValues,
|
||||
onChange,
|
||||
}: TriggerMultiSelectComboboxProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const triggerEvents = Object.values(WebhookTriggerEvents);
|
||||
|
||||
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 (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={isOpen}
|
||||
className="w-[200px] justify-between"
|
||||
>
|
||||
{selectedValues.length > 0 ? selectedValues.length + ' selected...' : 'Select values...'}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-9999 w-full max-w-[280px] p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={truncateTitle(
|
||||
selectedValues.map((v) => toFriendlyWebhookEventName(v)).join(', '),
|
||||
15,
|
||||
)}
|
||||
/>
|
||||
<CommandEmpty>No value found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{allEvents.map((value: string, i: number) => (
|
||||
<CommandItem key={i} onSelect={() => handleSelect(value)}>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{toFriendlyWebhookEventName(value)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { Braces, CreditCard, Settings, Users } from 'lucide-react';
|
||||
import { Braces, CreditCard, Settings, Users, Webhook } from 'lucide-react';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -22,6 +22,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
const settingsPath = `/t/${teamUrl}/settings`;
|
||||
const membersPath = `/t/${teamUrl}/settings/members`;
|
||||
const tokensPath = `/t/${teamUrl}/settings/tokens`;
|
||||
const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
|
||||
const billingPath = `/t/${teamUrl}/settings/billing`;
|
||||
|
||||
return (
|
||||
@ -59,6 +60,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={webhooksPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith(webhooksPath) && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
Webhooks
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
|
||||
@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { Braces, CreditCard, Key, User } from 'lucide-react';
|
||||
import { Braces, CreditCard, Key, User, Webhook } from 'lucide-react';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -22,6 +22,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
const settingsPath = `/t/${teamUrl}/settings`;
|
||||
const membersPath = `/t/${teamUrl}/settings/members`;
|
||||
const tokensPath = `/t/${teamUrl}/settings/tokens`;
|
||||
const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
|
||||
const billingPath = `/t/${teamUrl}/settings/billing`;
|
||||
|
||||
return (
|
||||
@ -67,6 +68,19 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={webhooksPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith(webhooksPath) && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
Webhooks
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
|
||||
0
apps/web/src/components/forms/webhook.tsx
Normal file
0
apps/web/src/components/forms/webhook.tsx
Normal file
3
apps/web/src/pages/api/v1/me/index.ts
Normal file
3
apps/web/src/pages/api/v1/me/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/test-credentials';
|
||||
|
||||
export default testCredentialsHandler;
|
||||
3
apps/web/src/pages/api/v1/zapier/list-documents/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/list-documents/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents';
|
||||
|
||||
export default listDocumentsHandler;
|
||||
3
apps/web/src/pages/api/v1/zapier/subscribe/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/subscribe/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe';
|
||||
|
||||
export default subscribeHandler;
|
||||
3
apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
Normal file
3
apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
|
||||
|
||||
export default unsubscribeHandler;
|
||||
12
apps/web/src/pages/api/webhook/trigger.ts
Normal file
12
apps/web/src/pages/api/webhook/trigger.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trigger/handler';
|
||||
|
||||
export const config = {
|
||||
maxDuration: 300,
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '50mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default handlerTriggerWebhooks;
|
||||
@ -12,7 +12,7 @@ interface TeamProviderProps {
|
||||
|
||||
const TeamContext = createContext<Team | null>(null);
|
||||
|
||||
export const useCurrentTeam = (): Team | null => {
|
||||
export const useCurrentTeam = () => {
|
||||
const context = useContext(TeamContext);
|
||||
|
||||
if (!context) {
|
||||
@ -22,7 +22,7 @@ export const useCurrentTeam = (): Team | null => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useOptionalCurrentTeam = (): Team | null => {
|
||||
export const useOptionalCurrentTeam = () => {
|
||||
return useContext(TeamContext);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user