diff --git a/apps/marketing/content/blog/launch-week-2-day-2.mdx b/apps/marketing/content/blog/launch-week-2-day-2.mdx new file mode 100644 index 000000000..3a67977ec --- /dev/null +++ b/apps/marketing/content/blog/launch-week-2-day-2.mdx @@ -0,0 +1,76 @@ +--- +title: Launch Week II - Day 2 - Templates +description: Templates help you prepare regular documents faster. And you can share them with your team! +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2024-02-27 +tags: + - Launch Week + - Templates +--- + + + +> TLDR; You can now reuse documents via templates. More field types coming soon as well. + +## Introducing Templates + +It's day 2 of Launch Week, everybody 🙌 After introducing [Teams](https://documenso.com/blog/launch-week-2-day-1) yesterday, today we are looking at making Documenso faster for daily use: +We are launching templates for Documenso! Templates are an easy way to reuse documents you send out often with just a few clicks. With templates, you can: + +
+ + +
+ Quickly fill out recipients, when creating from a template +
+
+ +- Save often-uploaded documents for reuse +- Pre-define fields, so you just have to send the document +- Quickly fill out recipients and roles for new documents +- Share templates with your team to make working together even easier + +
+ + +
+ POV: You are a diligent german and create custom receipts with Documenso +
+
+ +## Pricing + +Templates are **included in all Documenso Plans!** That includes our free tier: The limit of 5 documents per month still applies, but you are free to reach it with less friction using templates. Sharing templates with other users is only possible with the teams plan. If you want to share templates with people not in your team, we might have something coming up later this week 👀 + +## What's Next for Templates + +We have a lot of great stuff coming up for templates as well: + +- More Field Types are in the pipeline +- Sharing Templates Externally 👀 + +Check out templates [here](https://documen.so/templates) and let us know what you think and what we can improve. Which field types are you missing? Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :) + +> 🚨 We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀 + +Best from Hamburg\ +Timur diff --git a/apps/marketing/content/blog/launch-week-2-day-3.mdx b/apps/marketing/content/blog/launch-week-2-day-3.mdx new file mode 100644 index 000000000..6ea0db9b9 --- /dev/null +++ b/apps/marketing/content/blog/launch-week-2-day-3.mdx @@ -0,0 +1,53 @@ +--- +title: Launch Week II - Day 3 - API +description: Documenso's mission is to create a plattform developers all around the world can build upon. Today we are releasing the first version of our public API, included in all plans! +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2024-02-28 +tags: + - Launch Week + - API +--- + + + +> TLDR; The public API is now availible for all plans. + +## Introducing the public Documenso API + +Launch. Week. Day. 3 🎉 Documenso's mission is to create a platform that developers all around the world can build upon. Today we are releasing the first version of our public API, and we are pumped. Since this is the first version, we focused on the basics. With the new API you can: + +- Get Documents (Individual or all Accessible) +- Upload Documents +- Delete Documents +- Create Documents from Templates +- Trigger Sending Documents for Singing + +You can check out the detailed API documentation here: + +> API DOCUMENTATION: [https://app.documenso.com/api/v1/openapi](https://app.documenso.com/api/v1/openapi) + +## Pricing + +We are building Documenso to be an open and extendable platform; therefore the API is included in all current plans. The API is authenticated via auth tokens, which every user can create at no extra cost, as can teams. Existing limits still apply (i.e., the number of included documents for the free plan). While we don't have all the details yet, we don't intend to price the API usage in itself (rather the accounts using it) since we want you to build on Documenso without being smothered by API costs. + +> Try the API here for free: [https://documen.so/api](https://documen.so/api) + +## What's next for the API + +You tell us. This is by far the most requested feature, so we would like to hear from you. What should we add? How can we integrate even better? + +Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :) + +> 🚨 We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀 + +Best from Hamburg\ +Timur diff --git a/apps/marketing/public/blog/quickfill.png b/apps/marketing/public/blog/quickfill.png new file mode 100644 index 000000000..17765b643 Binary files /dev/null and b/apps/marketing/public/blog/quickfill.png differ diff --git a/apps/marketing/public/blog/template.png b/apps/marketing/public/blog/template.png new file mode 100644 index 000000000..74dd48de9 Binary files /dev/null and b/apps/marketing/public/blog/template.png differ diff --git a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx index 8951098c4..a62775522 100644 --- a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens'; import { Button } from '@documenso/ui/primitives/button'; @@ -18,7 +19,15 @@ export default async function ApiTokensPage() {

API Tokens

- On this page, you can create new API tokens and manage the existing ones. + On this page, you can create new API tokens and manage the existing ones.
+ You can view our swagger docs{' '} + + here +


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..53ec24827 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx @@ -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; + +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({ + 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 ( +
+ + + {isLoading && ( +
+ +
+ )} + +
+ +
+
+ ( + + Webhook URL + + + + + + The URL for Documenso to send webhook events to. + + + + + )} + /> + + ( + + Enabled + +
+ + + +
+ + +
+ )} + /> +
+ + ( + + Triggers + + { + onChange(values); + }} + /> + + + + The events that will trigger a webhook to be sent to your URL. + + + + + )} + /> + + ( + + Secret + + + + + + A secret that will be sent to your URL so you can verify that the request has + been sent by Documenso. + + + + )} + /> + +
+ +
+
+
+ +
+ ); +} 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..01196544d --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx @@ -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 ( +
+ + + + + {isLoading && ( +
+ +
+ )} + + {webhooks && 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 && webhooks.length > 0 && ( +
+ {webhooks?.map((webhook) => ( +
+
+
+
{webhook.id}
+ +
+
+ {webhook.webhookUrl} +
+ + + {webhook.enabled ? 'Enabled' : 'Disabled'} + +
+ +

+ Listening to{' '} + {webhook.eventTriggers + .map((trigger) => toFriendlyWebhookEventName(trigger)) + .join(', ')} +

+ +

+ Created on{' '} + +

+
+ +
+ + + + +
+
+
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx index 2b775d32c..eedae29d1 100644 --- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx +++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens'; import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; @@ -29,7 +30,15 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {

API Tokens

- On this page, you can create new API tokens and manage the existing ones. + On this page, you can create new API tokens and manage the existing ones.
+ You can view our swagger docs{' '} + + here +


diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx new file mode 100644 index 000000000..cc7261bda --- /dev/null +++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx @@ -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; + +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({ + 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 ( +
+ + + {isLoading && ( +
+ +
+ )} + +
+ +
+
+ ( + + Webhook URL + + + + + + The URL for Documenso to send webhook events to. + + + + + )} + /> + + ( + + Enabled + +
+ + + +
+ + +
+ )} + /> +
+ + ( + + Triggers + + { + onChange(values); + }} + /> + + + + The events that will trigger a webhook to be sent to your URL. + + + + + )} + /> + + ( + + Secret + + + + + + A secret that will be sent to your URL so you can verify that the request has + been sent by Documenso. + + + + )} + /> + +
+ +
+
+
+ +
+ ); +} diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx new file mode 100644 index 000000000..054664624 --- /dev/null +++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx @@ -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 ( +
+ + + + + {isLoading && ( +
+ +
+ )} + + {webhooks && 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 && webhooks.length > 0 && ( +
+ {webhooks?.map((webhook) => ( +
+
+
+
{webhook.id}
+ +
+
+ {webhook.webhookUrl} +
+ + + {webhook.enabled ? 'Enabled' : 'Disabled'} + +
+ +

+ Listening to{' '} + {webhook.eventTriggers + .map((trigger) => toFriendlyWebhookEventName(trigger)) + .join(', ')} +

+ +

+ Created on{' '} + +

+
+ +
+ + + + +
+
+
+ ))} +
+ )} +
+ ); +} 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 e87c47b67..6109d1f3d 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 { 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) => { + + + + {isBillingEnabled && ( + + + + {isBillingEnabled && ( } + + + + + Create webhook + On this page, you can create a new webhook. + + +
+ +
+
+ ( + + Webhook URL + + + + + + The URL for Documenso to send webhook events to. + + + + + )} + /> + + ( + + Enabled + +
+ + + +
+ + +
+ )} + /> +
+ + ( + + Triggers + + { + onChange(values); + }} + /> + + + + The events that will trigger a webhook to be sent to your URL. + + + + + )} + /> + + ( + + Secret + + + + + + A secret that will be sent to your URL so you can verify that the request has + been sent by Documenso. + + + + )} + /> + + +
+ + +
+
+
+
+ +
+ + ); +}; 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 new file mode 100644 index 000000000..e65ae78b8 --- /dev/null +++ b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx @@ -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; + 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; + + const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhook.useMutation(); + + const form = useForm({ + 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 ( + !form.formState.isSubmitting && setOpen(value)}> + + {children ?? ( + + )} + + + + + 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/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx new file mode 100644 index 000000000..5636f1931 --- /dev/null +++ b/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx @@ -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([]); + + 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 ( + + + + + + + toFriendlyWebhookEventName(v)).join(', '), + 15, + )} + /> + No value found. + + {allEvents.map((value: string, i: number) => ( + handleSelect(value)}> + + {toFriendlyWebhookEventName(value)} + + ))} + + + + + ); +}; diff --git a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx index 20fe8cb2e..6964b2cee 100644 --- a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx +++ b/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx @@ -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) => { + + + + {IS_BILLING_ENABLED() && ( + + + + {IS_BILLING_ENABLED() && (