Merge branch 'main' into chore/admin-org-nav-links

This commit is contained in:
Ephraim Duncan
2025-11-25 11:05:27 +00:00
committed by GitHub
153 changed files with 18936 additions and 73005 deletions

View File

@@ -13,7 +13,7 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
@@ -96,7 +96,7 @@ export const DocumentDuplicateDialog = ({
</div>
) : (
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
<PDFViewer
<PDFViewerLazy
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={undefined}

View File

@@ -38,7 +38,7 @@ import { useCurrentTeam } from '~/providers/team';
import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
const ZCreateWebhookFormSchema = ZCreateWebhookRequestSchema.omit({ teamId: true });
const ZCreateWebhookFormSchema = ZCreateWebhookRequestSchema;
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
@@ -78,7 +78,6 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
eventTriggers,
secret,
webhookUrl,
teamId: team.id,
});
setOpen(false);

View File

@@ -67,7 +67,7 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
const onSubmit = async () => {
try {
await deleteWebhook({ id: webhook.id, teamId: team.id });
await deleteWebhook({ id: webhook.id });
toast({
title: _(msg`Webhook deleted`),
@@ -146,26 +146,18 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
/>
<DialogFooter>
<div className="flex w-full flex-nowrap gap-4">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => setOpen(false)}
>
<Trans>Cancel</Trans>
</Button>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
</Button>
<Button
type="submit"
variant="destructive"
className="flex-1"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
<Trans>I'm sure! Delete it</Trans>
</Button>
</div>
<Button
type="submit"
variant="destructive"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
<Trans>Delete</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>

View File

@@ -0,0 +1,225 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { Webhook } from '@prisma/client';
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 { ZEditWebhookRequestSchema } from '@documenso/trpc/server/webhook-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
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 { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
const ZEditWebhookFormSchema = ZEditWebhookRequestSchema.omit({ id: true });
type TEditWebhookFormSchema = z.infer<typeof ZEditWebhookFormSchema>;
export type WebhookEditDialogProps = {
trigger?: React.ReactNode;
webhook: Webhook;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const WebhookEditDialog = ({ trigger, webhook, ...props }: WebhookEditDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const [open, setOpen] = useState(false);
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: webhook.id,
...data,
});
toast({
title: t`Webhook updated`,
description: t`The webhook has been updated successfully.`,
duration: 5000,
});
} catch (err) {
toast({
title: t`Failed to update webhook`,
description: t`We encountered an error while updating the webhook. Please try again later.`,
variant: 'destructive',
});
}
};
return (
<Dialog
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
{...props}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger}
</DialogTrigger>
<DialogContent className="max-w-lg" position="center">
<DialogHeader>
<DialogTitle>
<Trans>Edit webhook</Trans>
</DialogTitle>
<DialogDescription>{webhook.id}</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset
className="flex h-full 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>
<Trans>The URL for Documenso to send webhook events to.</Trans>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Enabled</Trans>
</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>
<Trans>Triggers</Trans>
</FormLabel>
<FormControl>
<WebhookMultiSelectCombobox
listValues={value}
onChange={(values: string[]) => {
onChange(values);
}}
/>
</FormControl>
<FormDescription>
<Trans>The events that will trigger a webhook to be sent to your URL.</Trans>
</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>
<Trans>
A secret that will be sent to your URL so you can verify that the request
has been sent by Documenso.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary">
<Trans>Close</Trans>
</Button>
</DialogClose>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -36,8 +36,6 @@ import {
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export type WebhookTestDialogProps = {
webhook: Pick<Webhook, 'id' | 'webhookUrl' | 'eventTriggers'>;
children: React.ReactNode;
@@ -53,8 +51,6 @@ export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps)
const { t } = useLingui();
const { toast } = useToast();
const team = useCurrentTeam();
const [open, setOpen] = useState(false);
const { mutateAsync: testWebhook } = trpc.webhook.testWebhook.useMutation();
@@ -71,7 +67,6 @@ export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps)
await testWebhook({
id: webhook.id,
event,
teamId: team.id,
});
toast({
@@ -150,11 +145,11 @@ export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps)
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
<Trans>Close</Trans>
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Send Test Webhook</Trans>
<Trans>Send</Trans>
</Button>
</DialogFooter>
</fieldset>

View File

@@ -23,7 +23,7 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { FieldSelector } from '@documenso/ui/primitives/field-selector';
import { Form } from '@documenso/ui/primitives/form/form';
import PDFViewer from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -463,12 +463,12 @@ export const ConfigureFieldsView = ({
{/* Desktop sidebar */}
{!isMobile && (
<div className="order-2 col-span-12 md:order-1 md:col-span-4">
<div className="bg-widget border-border sticky top-4 max-h-[calc(100vh-2rem)] rounded-lg border p-4 pb-6">
<div className="sticky top-4 max-h-[calc(100vh-2rem)] rounded-lg border border-border bg-widget p-4 pb-6">
<h2 className="mb-1 text-lg font-medium">
<Trans>Configure Fields</Trans>
</h2>
<p className="text-muted-foreground mb-6 text-sm">
<p className="mb-6 text-sm text-muted-foreground">
<Trans>Configure the fields you want to place on the document.</Trans>
</p>
@@ -522,7 +522,7 @@ export const ConfigureFieldsView = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
'dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white text-muted-foreground transition duration-200 [container-type:size]',
selectedRecipientStyles.base,
{
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
@@ -545,7 +545,7 @@ export const ConfigureFieldsView = ({
<Form {...form}>
<div>
<PDFViewer
<PDFViewerLazy
presignToken={presignToken}
overrideData={normalizedDocumentData}
envelopeItem={normalizedEnvelopeItem}
@@ -597,14 +597,14 @@ export const ConfigureFieldsView = ({
{isMobile && (
<Sheet open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<SheetTrigger asChild>
<div className="bg-widget border-border fixed bottom-6 left-6 right-6 z-50 flex items-center justify-between gap-2 rounded-lg border p-4">
<div className="fixed bottom-6 left-6 right-6 z-50 flex items-center justify-between gap-2 rounded-lg border border-border bg-widget p-4">
<span className="text-lg font-medium">
<Trans>Configure Fields</Trans>
</span>
<button
type="button"
className="border-border text-muted-foreground inline-flex h-10 w-10 items-center justify-center rounded-lg border"
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border text-muted-foreground"
>
<ChevronsUpDown className="h-6 w-6" />
</button>
@@ -614,13 +614,13 @@ export const ConfigureFieldsView = ({
<SheetContent
position="bottom"
size="xl"
className="bg-widget h-fit max-h-[80vh] overflow-y-auto rounded-t-xl p-4"
className="h-fit max-h-[80vh] overflow-y-auto rounded-t-xl bg-widget p-4"
>
<h2 className="mb-1 text-lg font-medium">
<Trans>Configure Fields</Trans>
</h2>
<p className="text-muted-foreground mb-6 text-sm">
<p className="mb-6 text-sm text-muted-foreground">
<Trans>Configure the fields you want to place on the document.</Trans>
</p>

View File

@@ -28,7 +28,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -334,7 +334,7 @@ export const EmbedDirectTemplateClientPage = ({
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<PDFViewer
<PDFViewerLazy
envelopeItem={envelopeItems[0]}
token={recipient.token}
version="signed"
@@ -348,11 +348,11 @@ export const EmbedDirectTemplateClientPage = ({
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
<div className="flex h-fit w-full flex-col rounded-xl border border-border bg-widget px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<h3 className="text-xl font-semibold text-foreground md:text-2xl">
<Trans>Sign document</Trans>
</h3>
@@ -362,7 +362,7 @@ export const EmbedDirectTemplateClientPage = ({
className="h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(false)}
>
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
<LucideChevronDown className="h-5 w-5 text-muted-foreground" />
</Button>
) : pendingFields.length > 0 ? (
<Button
@@ -370,7 +370,7 @@ export const EmbedDirectTemplateClientPage = ({
className="h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(true)}
>
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
<LucideChevronUp className="h-5 w-5 text-muted-foreground" />
</Button>
) : (
<Button
@@ -388,11 +388,11 @@ export const EmbedDirectTemplateClientPage = ({
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<p className="mt-2 text-sm text-muted-foreground">
<Trans>Sign the document to complete the process.</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
<hr className="mb-8 mt-4 border-border" />
</div>
{/* Form */}
@@ -406,7 +406,7 @@ export const EmbedDirectTemplateClientPage = ({
<Input
type="text"
id="full-name"
className="bg-background mt-2"
className="mt-2 bg-background"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
@@ -421,7 +421,7 @@ export const EmbedDirectTemplateClientPage = ({
<Input
type="email"
id="email"
className="bg-background mt-2"
className="mt-2 bg-background"
disabled={isEmailLocked}
value={email}
onChange={(e) => !isEmailLocked && setEmail(e.target.value.trim())}
@@ -490,7 +490,7 @@ export const EmbedDirectTemplateClientPage = ({
</div>
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>

View File

@@ -22,7 +22,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -286,7 +286,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="embed--DocumentViewer flex-1">
<PDFViewer
<PDFViewerLazy
envelopeItem={envelopeItems[0]}
token={token}
version="signed"
@@ -300,11 +300,11 @@ export const EmbedSignDocumentV1ClientPage = ({
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="embed--DocumentWidget flex w-full flex-col rounded-xl border border-border bg-widget px-4 py-4 md:py-6">
{/* Header */}
<div className="embed--DocumentWidgetHeader">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<h3 className="text-xl font-semibold text-foreground md:text-2xl">
{isAssistantMode ? (
<Trans>Assist with signing</Trans>
) : (
@@ -315,18 +315,18 @@ export const EmbedSignDocumentV1ClientPage = ({
{isExpanded ? (
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
className="h-8 w-8 bg-background p-0 md:hidden dark:bg-foreground"
onClick={() => setIsExpanded(false)}
>
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
<LucideChevronDown className="h-5 w-5 text-muted-foreground dark:text-background" />
</Button>
) : pendingFields.length > 0 ? (
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
className="h-8 w-8 bg-background p-0 md:hidden dark:bg-foreground"
onClick={() => setIsExpanded(true)}
>
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
<LucideChevronUp className="h-5 w-5 text-muted-foreground dark:text-background" />
</Button>
) : (
<Button
@@ -346,7 +346,7 @@ export const EmbedSignDocumentV1ClientPage = ({
</div>
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<p className="mt-2 text-sm text-muted-foreground">
{isAssistantMode ? (
<Trans>Help complete the document for other signers.</Trans>
) : (
@@ -354,7 +354,7 @@ export const EmbedSignDocumentV1ClientPage = ({
)}
</p>
<hr className="border-border mb-8 mt-4" />
<hr className="mb-8 mt-4 border-border" />
</div>
{/* Form */}
@@ -366,7 +366,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<Trans>Signing for</Trans>
</Label>
<fieldset className="dark:bg-background border-border mt-2 rounded-2xl border bg-white p-3">
<fieldset className="mt-2 rounded-2xl border border-border bg-white p-3 dark:bg-background">
<RadioGroup
className="gap-0 space-y-3 shadow-none"
value={selectedSignerId?.toString()}
@@ -377,7 +377,7 @@ export const EmbedSignDocumentV1ClientPage = ({
.map((r) => (
<div
key={`${assistantSignersId}-${r.id}`}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
className="relative flex flex-col gap-4 rounded-lg border border-border bg-widget p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -395,15 +395,15 @@ export const EmbedSignDocumentV1ClientPage = ({
{r.name}
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
<span className="ml-2 text-muted-foreground">
{_(msg`(You)`)}
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
<p className="text-xs text-muted-foreground">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
<div className="text-xs leading-[inherit] text-muted-foreground">
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
</div>
</div>
@@ -424,7 +424,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<Input
type="text"
id="full-name"
className="bg-background mt-2"
className="mt-2 bg-background"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
@@ -439,7 +439,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<Input
type="email"
id="email"
className="bg-background mt-2"
className="mt-2 bg-background"
value={email}
disabled
/>
@@ -507,7 +507,7 @@ export const EmbedSignDocumentV1ClientPage = ({
</div>
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>

View File

@@ -21,7 +21,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import PDFViewer from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -177,14 +177,14 @@ export const MultiSignDocumentSigningView = ({
};
return (
<div className="bg-background min-h-screen overflow-hidden">
<div className="min-h-screen overflow-hidden bg-background">
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
{match({ isLoading, document })
.with({ isLoading: true }, () => (
<div className="flex min-h-[400px] w-full items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Loader className="text-primary h-8 w-8 animate-spin" />
<p className="text-muted-foreground text-sm">
<Loader className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">
<Trans>Loading document...</Trans>
</p>
</div>
@@ -192,7 +192,7 @@ export const MultiSignDocumentSigningView = ({
))
.with({ isLoading: false, document: undefined }, () => (
<div className="flex min-h-[400px] w-full items-center justify-center">
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>Failed to load document</Trans>
</p>
</div>
@@ -225,7 +225,7 @@ export const MultiSignDocumentSigningView = ({
'md:mx-auto md:max-w-2xl': document.status === DocumentStatus.COMPLETED,
})}
>
<PDFViewer
<PDFViewerLazy
envelopeItem={document.envelopeItems[0]}
token={token}
version="signed"
@@ -243,23 +243,23 @@ export const MultiSignDocumentSigningView = ({
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-0 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="embed--DocumentWidget flex w-full flex-col rounded-xl border border-border bg-widget px-4 py-4 md:py-6">
{/* Header */}
<div className="embed--DocumentWidgetHeader">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<h3 className="text-xl font-semibold text-foreground md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="text-muted-foreground h-5 w-5"
className="h-5 w-5 text-muted-foreground"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
className="h-5 w-5 text-muted-foreground"
onClick={() => setIsExpanded(true)}
/>
)}
@@ -268,11 +268,11 @@ export const MultiSignDocumentSigningView = ({
</div>
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<p className="mt-2 text-sm text-muted-foreground">
<Trans>Sign the document to complete the process.</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
<hr className="mb-8 mt-4 border-border" />
</div>
{/* Form */}
@@ -288,7 +288,7 @@ export const MultiSignDocumentSigningView = ({
<Input
type="text"
id="full-name"
className="bg-background mt-2"
className="mt-2 bg-background"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
@@ -303,7 +303,7 @@ export const MultiSignDocumentSigningView = ({
<Input
type="email"
id="email"
className="bg-background mt-2"
className="mt-2 bg-background"
value={email}
disabled
/>

View File

@@ -42,7 +42,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { ZCreateOrganisationFormSchema } from '../dialogs/organisation-create-dialog';
const MotionCard = motion(Card);
const MotionCard = motion.create(Card);
export type BillingPlansProps = {
plans: InternalClaimPlans;
@@ -101,7 +101,7 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
<CardContent className="flex h-full flex-col p-6">
<CardTitle>{price.product.name}</CardTitle>
<div className="text-muted-foreground mt-2 text-lg font-medium">
<div className="mt-2 text-lg font-medium text-muted-foreground">
{price.friendlyPrice + ' '}
<span className="text-xs">
{interval === 'monthlyPrice' ? (
@@ -112,12 +112,12 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
</span>
</div>
<div className="text-muted-foreground mt-1.5 text-sm">
<div className="mt-1.5 text-sm text-muted-foreground">
{price.product.description}
</div>
{price.product.features && price.product.features.length > 0 && (
<div className="text-muted-foreground mt-4">
<div className="mt-4 text-muted-foreground">
<div className="text-sm font-medium">Includes:</div>
<ul className="mt-1 divide-y text-sm">
@@ -261,7 +261,7 @@ const BillingDialog = ({
<Building2Icon className="h-4 w-4" />
<Trans>Update current organisation</Trans>
</Label>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>
Upgrade <strong>{organisation.name}</strong> to {planName}
</Trans>
@@ -276,7 +276,7 @@ const BillingDialog = ({
<PlusIcon className="h-4 w-4" />
<Trans>Create separate organisation</Trans>
</Label>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>
Create a new organisation with {planName} plan. Keep your current organisation
on it's current plan

View File

@@ -13,7 +13,7 @@ import { trpc } from '@documenso/trpc/react';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -151,7 +151,7 @@ export const DirectTemplatePageView = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<PDFViewerLazy
key={template.id}
envelopeItem={template.envelopeItems[0]}
token={directTemplateRecipient.token}

View File

@@ -30,7 +30,7 @@ import { DocumentReadOnlyFields } from '@documenso/ui/components/document/docume
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { DocumentSigningAttachmentsPopover } from '~/components/general/document-signing/document-signing-attachments-popover';
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
@@ -187,7 +187,7 @@ export const DocumentSigningPageViewV1 = ({
<div className="mt-1.5 flex flex-wrap items-center justify-between gap-y-2 sm:mt-2.5 sm:gap-y-0">
<div className="max-w-[50ch]">
<span className="text-muted-foreground truncate" title={senderName}>
<span className="truncate text-muted-foreground" title={senderName}>
{senderName} {senderEmail}
</span>{' '}
<span className="text-muted-foreground">
@@ -245,7 +245,7 @@ export const DocumentSigningPageViewV1 = ({
<div className="flex-1">
<Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2">
<PDFViewer
<PDFViewerLazy
key={document.envelopeItems[0].id}
envelopeItem={document.envelopeItems[0]}
token={recipient.token}
@@ -260,9 +260,9 @@ export const DocumentSigningPageViewV1 = ({
className="group/document-widget fixed bottom-6 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-4 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="flex w-full flex-col rounded-xl border border-border bg-widget px-4 py-4 md:py-6">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<h3 className="text-xl font-semibold text-foreground md:text-2xl">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
@@ -305,25 +305,25 @@ export const DocumentSigningPageViewV1 = ({
.with({ isExpanded: true }, () => (
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
className="h-8 w-8 bg-background p-0 md:hidden dark:bg-foreground"
onClick={() => setIsExpanded(false)}
>
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
<LucideChevronDown className="h-5 w-5 text-muted-foreground dark:text-background" />
</Button>
))
.otherwise(() => (
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
className="h-8 w-8 bg-background p-0 md:hidden dark:bg-foreground"
onClick={() => setIsExpanded(true)}
>
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
<LucideChevronUp className="h-5 w-5 text-muted-foreground dark:text-background" />
</Button>
))}
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<p className="mt-2 text-sm text-muted-foreground">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => (
<Trans>Please mark as viewed to complete.</Trans>
@@ -340,7 +340,7 @@ export const DocumentSigningPageViewV1 = ({
.otherwise(() => null)}
</p>
<hr className="border-border mb-8 mt-4" />
<hr className="mb-8 mt-4 border-border" />
</div>
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">

View File

@@ -34,7 +34,7 @@ import { DocumentSigningRejectDialog } from './document-signing-reject-dialog';
import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';
const EnvelopeSignerPageRenderer = lazy(
async () => import('../envelope-signing/envelope-signer-page-renderer'),
async () => import('~/components/general/envelope-signing/envelope-signer-page-renderer'),
);
export const DocumentSigningPageViewV2 = () => {
@@ -71,7 +71,7 @@ export const DocumentSigningPageViewV2 = () => {
}, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]);
return (
<div className="dark:bg-background min-h-screen w-screen bg-gray-50">
<div className="min-h-screen w-screen bg-gray-50 dark:bg-background">
<SignFieldEmailDialog.Root />
<SignFieldTextDialog.Root />
<SignFieldNumberDialog.Root />
@@ -86,9 +86,9 @@ export const DocumentSigningPageViewV2 = () => {
{/* Main Content Area */}
<div className="flex h-[calc(100vh-4rem)] w-screen">
{/* Left Section - Step Navigation */}
<div className="embed--DocumentWidgetContainer bg-background border-border hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4 lg:flex">
<div className="embed--DocumentWidgetContainer hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r border-border bg-background py-4 lg:flex">
<div className="px-4">
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
<h3 className="flex items-end justify-between text-sm font-semibold text-foreground">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
@@ -96,7 +96,7 @@ export const DocumentSigningPageViewV2 = () => {
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
.otherwise(() => null)}
<span className="text-muted-foreground bg-muted/50 ml-2 rounded border px-2 py-0.5 text-xs">
<span className="ml-2 rounded border bg-muted/50 px-2 py-0.5 text-xs text-muted-foreground">
<Plural
value={recipientFieldsRemaining.length}
one="1 Field Remaining"
@@ -105,11 +105,11 @@ export const DocumentSigningPageViewV2 = () => {
</span>
</h3>
<div className="bg-muted relative my-4 h-[4px] rounded-md">
<div className="relative my-4 h-[4px] rounded-md bg-muted">
<motion.div
layout="size"
layoutId="document-flow-container-step"
className="bg-documenso absolute inset-y-0 left-0"
className="absolute inset-y-0 left-0 bg-documenso"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
@@ -126,7 +126,7 @@ export const DocumentSigningPageViewV2 = () => {
{/* Quick Actions. */}
{!isDirectTemplate && (
<div className="embed--Actions space-y-3 px-4">
<h4 className="text-foreground text-sm font-semibold">
<h4 className="text-sm font-semibold text-foreground">
<Trans>Actions</Trans>
</h4>
@@ -173,7 +173,7 @@ export const DocumentSigningPageViewV2 = () => {
<Button
variant="ghost"
size="sm"
className="hover:text-destructive w-full justify-start"
className="w-full justify-start hover:text-destructive"
>
<BanIcon className="mr-2 h-4 w-4" />
<Trans>Reject Document</Trans>
@@ -235,7 +235,7 @@ export const DocumentSigningPageViewV2 = () => {
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
<p className="text-foreground text-sm">
<p className="text-sm text-foreground">
<Trans>No documents found</Trans>
</p>
</div>
@@ -250,7 +250,7 @@ export const DocumentSigningPageViewV2 = () => {
<a
href="https://documenso.com"
target="_blank"
className="bg-primary text-primary-foreground fixed bottom-0 right-0 z-40 hidden cursor-pointer rounded-tl px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100 lg:block"
className="fixed bottom-0 right-0 z-40 hidden cursor-pointer rounded-tl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:block"
>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { lazy, useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
@@ -21,12 +21,15 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector';
import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer';
const EnvelopeGenericPageRenderer = lazy(
async () => import('~/components/general/envelope-editor/envelope-generic-page-renderer'),
);
export type DocumentCertificateQRViewProps = {
documentId: number;
@@ -120,7 +123,7 @@ export const DocumentCertificateQRView = ({
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="space-y-1">
<h1 className="text-xl font-medium">{title}</h1>
<div className="text-muted-foreground flex flex-col gap-0.5 text-sm">
<div className="flex flex-col gap-0.5 text-sm text-muted-foreground">
<p>
<Trans>{recipientCount} recipients</Trans>
</p>
@@ -146,7 +149,7 @@ export const DocumentCertificateQRView = ({
</div>
<div className="mt-12 w-full">
<PDFViewer
<PDFViewerLazy
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={token}
@@ -179,7 +182,7 @@ const DocumentCertificateQrV2 = ({
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="space-y-1">
<h1 className="text-xl font-medium">{title}</h1>
<div className="text-muted-foreground flex flex-col gap-0.5 text-sm">
<div className="flex flex-col gap-0.5 text-sm text-muted-foreground">
<p>
<Trans>{recipientCount} recipients</Trans>
</p>

View File

@@ -27,7 +27,7 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -440,7 +440,7 @@ export const DocumentEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<PDFViewerLazy
key={document.envelopeItems[0].id}
envelopeItem={document.envelopeItems[0]}
token={undefined}

View File

@@ -47,7 +47,7 @@ import { EnvelopeRendererFileSelector } from './envelope-file-selector';
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
const EnvelopeEditorFieldsPageRenderer = lazy(
async () => import('./envelope-editor-fields-page-renderer'),
async () => import('~/components/general/envelope-editor/envelope-editor-fields-page-renderer'),
);
const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
@@ -119,7 +119,7 @@ export const EnvelopeEditorFieldsPage = () => {
{envelope.recipients.length === 0 && (
<Alert
variant="neutral"
className="border-border bg-background mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border"
className="mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border border-border bg-background"
>
<div className="flex flex-col gap-1">
<AlertTitle>
@@ -145,11 +145,11 @@ export const EnvelopeEditorFieldsPage = () => {
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
<FileTextIcon className="text-muted-foreground h-10 w-10" />
<p className="text-foreground mt-1 text-sm">
<FileTextIcon className="h-10 w-10 text-muted-foreground" />
<p className="mt-1 text-sm text-foreground">
<Trans>No documents found</Trans>
</p>
<p className="text-muted-foreground mt-1 text-sm">
<p className="mt-1 text-sm text-muted-foreground">
<Trans>Please upload a document to continue</Trans>
</p>
</div>
@@ -159,10 +159,10 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && envelope.recipients.length > 0 && (
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-border bg-background py-4">
{/* Recipient selector section. */}
<section className="px-4">
<h3 className="text-foreground mb-2 text-sm font-semibold">
<h3 className="mb-2 text-sm font-semibold text-foreground">
<Trans>Selected Recipient</Trans>
</h3>
@@ -194,7 +194,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Add fields section. */}
<section className="px-4">
<h3 className="text-foreground mb-2 text-sm font-semibold">
<h3 className="mb-2 text-sm font-semibold text-foreground">
<Trans>Add Fields</Trans>
</h3>
@@ -213,25 +213,25 @@ export const EnvelopeEditorFieldsPage = () => {
{searchParams.get('devmode') && (
<>
<div className="px-4">
<h3 className="text-foreground mb-3 text-sm font-semibold">
<h3 className="mb-3 text-sm font-semibold text-foreground">
<Trans>Developer Mode</Trans>
</h3>
<div className="bg-muted/50 border-border text-foreground space-y-2 rounded-md border p-3 text-sm">
<div className="space-y-2 rounded-md border border-border bg-muted/50 p-3 text-sm text-foreground">
<p>
<span className="text-muted-foreground min-w-12">Pos X:&nbsp;</span>
<span className="min-w-12 text-muted-foreground">Pos X:&nbsp;</span>
{selectedField.positionX.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Pos Y:&nbsp;</span>
<span className="min-w-12 text-muted-foreground">Pos Y:&nbsp;</span>
{selectedField.positionY.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Width:&nbsp;</span>
<span className="min-w-12 text-muted-foreground">Width:&nbsp;</span>
{selectedField.width.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Height:&nbsp;</span>
<span className="min-w-12 text-muted-foreground">Height:&nbsp;</span>
{selectedField.height.toFixed(2)}
</p>
</div>
@@ -241,7 +241,7 @@ export const EnvelopeEditorFieldsPage = () => {
</>
)}
<div className="[&_label]:text-foreground/70 px-4 [&_label]:text-xs">
<div className="px-4 [&_label]:text-xs [&_label]:text-foreground/70">
<h3 className="text-sm font-semibold">
{t(FieldSettingsTypeTranslations[selectedField.type])}
</h3>

View File

@@ -23,7 +23,9 @@ import { Separator } from '@documenso/ui/primitives/separator';
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
const EnvelopeGenericPageRenderer = lazy(
async () => import('~/components/general/envelope-editor/envelope-generic-page-renderer'),
);
// Todo: Envelopes - Dynamically import faker
export const EnvelopeEditorPreviewPage = () => {
@@ -232,11 +234,11 @@ export const EnvelopeEditorPreviewPage = () => {
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
<FileTextIcon className="text-muted-foreground h-10 w-10" />
<p className="text-foreground mt-1 text-sm">
<FileTextIcon className="h-10 w-10 text-muted-foreground" />
<p className="mt-1 text-sm text-foreground">
<Trans>No documents found</Trans>
</p>
<p className="text-muted-foreground mt-1 text-sm">
<p className="mt-1 text-sm text-muted-foreground">
<Trans>Please upload a document to continue</Trans>
</p>
</div>

View File

@@ -171,7 +171,7 @@ export default function EnvelopeSignerPageRenderer() {
const handleFieldGroupClick = (e: KonvaEventObject<Event>) => {
const currentTarget = e.currentTarget as Konva.Group;
const target = e.target;
const target = e.target as Konva.Shape;
const { width: fieldWidth, height: fieldHeight } = fieldGroup.getClientRect();

View File

@@ -18,7 +18,7 @@ import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields';
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
@@ -312,7 +312,7 @@ export const TemplateEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<PDFViewerLazy
key={template.envelopeItems[0].id}
envelopeItem={template.envelopeItems[0]}
token={undefined}

View File

@@ -0,0 +1,198 @@
import { useMemo, useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { WebhookCallStatus } from '@prisma/client';
import { RotateCwIcon } from 'lucide-react';
import { createCallable } from 'react-call';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
import { trpc } from '@documenso/trpc/react';
import type { TFindWebhookCallsResponse } from '@documenso/trpc/server/webhook-router/find-webhook-calls.types';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Sheet, SheetContent, SheetTitle } from '@documenso/ui/primitives/sheet';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type WebhookLogsSheetProps = {
webhookCall: TFindWebhookCallsResponse['data'][number];
};
export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | null>(
({ call, webhookCall: initialWebhookCall }) => {
const { t } = useLingui();
const { toast } = useToast();
const [webhookCall, setWebhookCall] = useState(initialWebhookCall);
const [activeTab, setActiveTab] = useState<'request' | 'response'>('request');
const { mutateAsync: resendWebhookCall, isPending: isResending } =
trpc.webhook.calls.resend.useMutation({
onSuccess: (result) => {
toast({ title: t`Webhook successfully sent` });
setWebhookCall(result);
},
onError: () => {
toast({ title: t`Something went wrong` });
},
});
const generalWebhookDetails = useMemo(() => {
return [
{
header: t`Status`,
value: webhookCall.status === WebhookCallStatus.SUCCESS ? t`Success` : t`Failed`,
},
{
header: t`Event`,
value: toFriendlyWebhookEventName(webhookCall.event),
},
{
header: t`Sent`,
value: new Date(webhookCall.createdAt).toLocaleString(),
},
{
header: t`Response Code`,
value: webhookCall.responseCode,
},
{
header: t`Destination`,
value: webhookCall.url,
},
];
}, [webhookCall]);
return (
<Sheet open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<SheetContent position="right" size="lg" className="max-w-2xl overflow-y-auto">
<SheetTitle>
<h2 className="text-lg font-semibold">
<Trans>Webhook Details</Trans>
</h2>
<p className="text-muted-foreground font-mono text-xs">{webhookCall.id}</p>
</SheetTitle>
{/* Content */}
<div className="flex-1 overflow-y-auto">
<div className="mt-6">
<div className="flex items-end justify-between">
<h4 className="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider">
<Trans>Details</Trans>
</h4>
<Button
onClick={() =>
resendWebhookCall({
webhookId: webhookCall.webhookId,
webhookCallId: webhookCall.id,
})
}
tabIndex={-1}
loading={isResending}
size="sm"
className="mb-2"
>
{!isResending && <RotateCwIcon className="mr-2 h-3.5 w-3.5" />}
<Trans>Resend</Trans>
</Button>
</div>
<div className="border-border overflow-hidden rounded-lg border">
<table className="w-full text-left text-sm">
<tbody className="divide-border bg-muted/30 divide-y">
{generalWebhookDetails.map(({ header, value }, index) => (
<tr key={index}>
<td className="text-muted-foreground border-border w-1/3 border-r px-4 py-2 font-mono text-xs">
{header}
</td>
<td className="text-foreground break-all px-4 py-2 font-mono text-xs">
{value}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Payload Tabs */}
<div className="py-6">
<div className="border-border mb-4 flex items-center gap-4 border-b">
<button
onClick={() => setActiveTab('request')}
className={cn(
'relative pb-2 text-sm font-medium transition-colors',
activeTab === 'request'
? 'text-foreground after:bg-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5'
: 'text-muted-foreground hover:text-foreground',
)}
>
<Trans>Request</Trans>
</button>
<button
onClick={() => setActiveTab('response')}
className={cn(
'relative pb-2 text-sm font-medium transition-colors',
activeTab === 'response'
? 'text-foreground after:bg-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5'
: 'text-muted-foreground hover:text-foreground',
)}
>
<Trans>Response</Trans>
</button>
</div>
<div className="group relative">
<div className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100">
<CopyTextButton
value={JSON.stringify(
activeTab === 'request' ? webhookCall.requestBody : webhookCall.responseBody,
null,
2,
)}
onCopySuccess={() => toast({ title: t`Copied to clipboard` })}
/>
</div>
<pre className="bg-muted/50 border-border text-foreground overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed">
{JSON.stringify(
activeTab === 'request' ? webhookCall.requestBody : webhookCall.responseBody,
null,
2,
)}
</pre>
</div>
{activeTab === 'response' && (
<div className="mt-6">
<h4 className="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider">
<Trans>Response Headers</Trans>
</h4>
<div className="border-border overflow-hidden rounded-lg border">
<table className="w-full text-left text-sm">
<tbody className="divide-border bg-muted/30 divide-y">
{Object.entries(webhookCall.responseHeaders as Record<string, string>).map(
([key, value]) => (
<tr key={key}>
<td className="text-muted-foreground border-border w-1/3 border-r px-4 py-2 font-mono text-xs">
{key}
</td>
<td className="text-foreground break-all px-4 py-2 font-mono text-xs">
{value as string}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</SheetContent>
</Sheet>
);
},
);

View File

@@ -1,6 +1,3 @@
import { useEffect } from 'react';
import Plausible from 'plausible-tracker';
import {
Links,
Meta,
@@ -17,7 +14,7 @@ import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes'
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { SessionProvider } from '@documenso/lib/client-only/providers/session';
import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { createPublicEnv, env } from '@documenso/lib/utils/env';
import { createPublicEnv } from '@documenso/lib/utils/env';
import { extractLocaleData } from '@documenso/lib/utils/i18n';
import { TrpcProvider } from '@documenso/trpc/react';
import { getOrganisationSession } from '@documenso/trpc/server/organisation-router/get-organisation-session';
@@ -31,11 +28,6 @@ import { langCookie } from './storage/lang-cookie.server';
import { themeSessionResolver } from './storage/theme-session.server';
import { appMetaTags } from './utils/meta';
const { trackPageview } = Plausible({
domain: 'documenso.com',
trackLocalhost: false,
});
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }];
export function meta() {
@@ -92,12 +84,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
const location = useLocation();
useEffect(() => {
if (env('NODE_ENV') === 'production') {
trackPageview();
}
}, [location.pathname]);
return (
<ThemeProvider specifiedTheme={theme} themeAction="/api/theme">
<LayoutContent>{children}</LayoutContent>

View File

@@ -1,4 +1,4 @@
import WebhookPage, { meta } from '../../t.$teamUrl+/settings.webhooks.$id';
import WebhookPage, { meta } from '../../t.$teamUrl+/settings.webhooks.$id._index';
export { meta };

View File

@@ -1,3 +1,5 @@
import { lazy } from 'react';
import { msg } from '@lingui/core/macro';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { DocumentStatus } from '@prisma/client';
@@ -19,7 +21,7 @@ import { cn } from '@documenso/ui/lib/utils';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
@@ -33,13 +35,16 @@ import {
FRIENDLY_STATUS_MAP,
} from '~/components/general/document/document-status';
import { EnvelopeRendererFileSelector } from '~/components/general/envelope-editor/envelope-file-selector';
import EnvelopeGenericPageRenderer from '~/components/general/envelope-editor/envelope-generic-page-renderer';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
import { useCurrentTeam } from '~/providers/team';
import type { Route } from './+types/documents.$id._index';
const EnvelopeGenericPageRenderer = lazy(
async () => import('~/components/general/envelope-editor/envelope-generic-page-renderer'),
);
export default function DocumentPage({ params }: Route.ComponentProps) {
const { t } = useLingui();
const { user } = useSession();
@@ -56,7 +61,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
if (isLoadingEnvelope) {
return (
<div className="text-foreground flex w-screen flex-col items-center justify-center gap-2 py-64">
<div className="flex w-screen flex-col items-center justify-center gap-2 py-64 text-foreground">
<Spinner />
<Trans>Loading</Trans>
</div>
@@ -117,7 +122,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
/>
{envelope.recipients.length > 0 && (
<div className="text-muted-foreground flex items-center">
<div className="flex items-center text-muted-foreground">
<Users2 className="mr-2 h-5 w-5" />
<StackAvatarsWithTooltip
@@ -188,7 +193,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
/>
)}
<PDFViewer
<PDFViewerLazy
envelopeItem={envelope.envelopeItems[0]}
token={undefined}
key={envelope.envelopeItems[0].id}
@@ -202,16 +207,16 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
className={cn('col-span-12 lg:col-span-6 xl:col-span-5', isMultiEnvelopeItem && 'mt-20')}
>
<div className="space-y-6">
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
<section className="flex flex-col rounded-xl border border-border bg-widget pb-4 pt-6">
<div className="flex flex-row items-center justify-between px-4">
<h3 className="text-foreground text-2xl font-semibold">
<h3 className="text-2xl font-semibold text-foreground">
{t(FRIENDLY_STATUS_MAP[envelope.status].labelExtended)}
</h3>
<DocumentPageViewDropdown envelope={envelope} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm">
<p className="mt-2 px-4 text-sm text-muted-foreground">
{match(envelope.status)
.with(DocumentStatus.COMPLETED, () => (
<Trans>This document has been signed by all recipients</Trans>

View File

@@ -0,0 +1,392 @@
import { useMemo, useState } from 'react';
import { useEffect } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
import {
CheckCircle2Icon,
ChevronRightIcon,
PencilIcon,
TerminalIcon,
XCircleIcon,
} from 'lucide-react';
import { useNavigate } from 'react-router';
import { useLocation, useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { z } from 'zod';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
import { trpc } from '@documenso/trpc/react';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { TableCell } from '@documenso/ui/primitives/table';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { WebhookEditDialog } from '~/components/dialogs/webhook-edit-dialog';
import { WebhookTestDialog } from '~/components/dialogs/webhook-test-dialog';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { SettingsHeader } from '~/components/general/settings-header';
import { WebhookLogsSheet } from '~/components/general/webhook-logs-sheet';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/settings.webhooks.$id._index';
const WebhookSearchParamsSchema = ZUrlSearchParamsSchema.extend({
status: z.nativeEnum(WebhookCallStatus).optional(),
events: z.preprocess(
(value) => (typeof value === 'string' && value.length > 0 ? value.split(',') : []),
z.array(z.nativeEnum(WebhookTriggerEvents)).optional(),
),
});
export function meta() {
return appMetaTags('Webhooks');
}
export default function WebhookPage({ params }: Route.ComponentProps) {
const { t, i18n } = useLingui();
const { toast } = useToast();
const { pathname } = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const team = useCurrentTeam();
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
const parsedSearchParams = WebhookSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
{
id: params.id,
},
{ enabled: !!params.id, retry: false },
);
const {
data,
isLoading: isLogsLoading,
isLoadingError: isLogsLoadingError,
} = trpc.webhook.calls.find.useQuery({
webhookId: params.id,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
status: parsedSearchParams.status,
events: parsedSearchParams.events,
query: parsedSearchParams.query,
});
/**
* Handle debouncing the search query.
*/
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
params.set('query', debouncedSearchQuery);
if (debouncedSearchQuery === '') {
params.delete('query');
}
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
page,
perPage,
});
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: t`Status`,
accessorKey: 'status',
cell: ({ row }) => (
<Badge variant={row.original.status === 'SUCCESS' ? 'default' : 'destructive'}>
{row.original.status === 'SUCCESS' ? (
<CheckCircle2Icon className="mr-2 h-4 w-4" />
) : (
<XCircleIcon className="mr-2 h-4 w-4" />
)}
{row.original.responseCode}
</Badge>
),
},
{
header: t`Event`,
accessorKey: 'event',
cell: ({ row }) => (
<div>
<p className="text-foreground text-sm font-semibold">
{toFriendlyWebhookEventName(row.original.event)}
</p>
<p className="text-muted-foreground text-xs">{row.original.id}</p>
</div>
),
},
{
header: t`Sent`,
accessorKey: 'createdAt',
cell: ({ row }) => (
<div className="flex items-center justify-between gap-2">
<p>
{i18n.date(row.original.createdAt, {
timeStyle: 'short',
dateStyle: 'short',
})}
</p>
<div className="data-state-selected:block opacity-0 transition-opacity group-hover:opacity-100">
<ChevronRightIcon className="h-4 w-4" />
</div>
</div>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
const getTabHref = (value: string) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (value === '') {
params.delete('status');
}
if (params.has('page')) {
params.delete('page');
}
let path = pathname;
if (params.toString()) {
path += `?${params.toString()}`;
}
return path;
};
if (isLoading) {
return <SpinnerBox className="py-32" />;
}
// Todo: Update UI, currently out of place.
if (!webhook) {
return (
<GenericErrorLayout
errorCode={404}
errorCodeMap={{
404: {
heading: msg`Webhook not found`,
subHeading: msg`404 Webhook not found`,
message: msg`The webhook you are looking for may have been removed, renamed or may have never existed.`,
},
}}
primaryButton={
<Button asChild>
<Link to={`/t/${team.url}/settings/webhooks`}>
<Trans>Go back</Trans>
</Link>
</Button>
}
secondaryButton={null}
/>
);
}
return (
<div>
<SettingsHeader
title={
<div className="flex items-center gap-2">
<p>
<Trans>Webhook</Trans>
</p>
<Badge variant={webhook.enabled ? 'default' : 'secondary'}>
{webhook.enabled ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
</Badge>
</div>
}
subtitle={webhook.webhookUrl}
>
<div className="flex gap-2">
<WebhookTestDialog webhook={webhook}>
<Button variant="outline">
<TerminalIcon className="mr-2 h-4 w-4" />
<Trans>Test</Trans>
</Button>
</WebhookTestDialog>
<WebhookEditDialog
webhook={webhook}
trigger={
<Button>
<PencilIcon className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Button>
}
/>
</div>
</SettingsHeader>
<div className="mt-4">
<div className="mb-4 flex flex-row items-center justify-between gap-x-4">
<Input
defaultValue={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t`Search by ID`}
/>
<WebhookEventCombobox />
<Tabs value={parsedSearchParams.status || ''} className="flex-shrink-0">
<TabsList>
<TabsTrigger className="hover:text-foreground min-w-[60px]" value="" asChild>
<Link to={getTabHref('')}>
<Trans>All</Trans>
</Link>
</TabsTrigger>
<TabsTrigger className="hover:text-foreground min-w-[60px]" value="SUCCESS" asChild>
<Link to={getTabHref(WebhookCallStatus.SUCCESS)}>
<Trans>Success</Trans>
</Link>
</TabsTrigger>
<TabsTrigger className="hover:text-foreground min-w-[60px]" value="FAILED" asChild>
<Link to={getTabHref(WebhookCallStatus.FAILED)}>
<Trans>Failed</Trans>
</Link>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<DataTable
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
onRowClick={(row) =>
WebhookLogsSheet.call({
webhookCall: row,
})
}
rowClassName="cursor-pointer group"
error={{
enable: isLogsLoadingError,
}}
skeleton={{
enable: isLogsLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
</>
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
</DataTable>
</div>
<WebhookLogsSheet.Root />
</div>
);
}
const WebhookEventCombobox = () => {
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const isMounted = useIsMounted();
const events = (searchParams?.get('events') ?? '').split(',').filter((value) => value !== '');
const comboBoxOptions = Object.values(WebhookTriggerEvents).map((event) => ({
label: toFriendlyWebhookEventName(event),
value: event,
}));
const onChange = (newEvents: string[]) => {
if (!pathname) {
return;
}
const params = new URLSearchParams(searchParams?.toString());
params.set('events', newEvents.join(','));
if (newEvents.length === 0) {
params.delete('events');
}
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
};
return (
<MultiSelectCombobox
emptySelectionPlaceholder={
<p className="text-muted-foreground font-normal">
<Trans>
<span className="text-muted-foreground/70">Events:</span> All
</Trans>
</p>
}
enableClearAllButton={true}
inputPlaceholder={msg`Search`}
loading={!isMounted}
options={comboBoxOptions}
selectedValues={events}
onChange={onChange}
/>
);
};

View File

@@ -1,263 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { Link } from 'react-router';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { ZEditWebhookRequestSchema } from '@documenso/trpc/server/webhook-router/schema';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
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 { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Switch } from '@documenso/ui/primitives/switch';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { WebhookTestDialog } from '~/components/dialogs/webhook-test-dialog';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { SettingsHeader } from '~/components/general/settings-header';
import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/settings.webhooks.$id';
const ZEditWebhookFormSchema = ZEditWebhookRequestSchema.omit({ id: true, teamId: true });
type TEditWebhookFormSchema = z.infer<typeof ZEditWebhookFormSchema>;
export function meta() {
return appMetaTags('Webhooks');
}
export default function WebhookPage({ params }: Route.ComponentProps) {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
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: _(msg`Webhook updated`),
description: _(msg`The webhook has been updated successfully.`),
duration: 5000,
});
await revalidate();
} catch (err) {
toast({
title: _(msg`Failed to update webhook`),
description: _(
msg`We encountered an error while updating the webhook. Please try again later.`,
),
variant: 'destructive',
});
}
};
if (isLoading) {
return <SpinnerBox className="py-32" />;
}
// Todo: Update UI, currently out of place.
if (!webhook) {
return (
<GenericErrorLayout
errorCode={404}
errorCodeMap={{
404: {
heading: msg`Webhook not found`,
subHeading: msg`404 Webhook not found`,
message: msg`The webhook you are looking for may have been removed, renamed or may have never existed.`,
},
}}
primaryButton={
<Button asChild>
<Link to={`/t/${team.url}/settings/webhooks`}>
<Trans>Go back</Trans>
</Link>
</Button>
}
secondaryButton={null}
/>
);
}
return (
<div className="max-w-2xl">
<SettingsHeader
title={_(msg`Edit webhook`)}
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
/>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset className="flex h-full 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>
<Trans>The URL for Documenso to send webhook events to.</Trans>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Enabled</Trans>
</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>
<Trans>Triggers</Trans>
</FormLabel>
<FormControl>
<WebhookMultiSelectCombobox
listValues={value}
onChange={(values: string[]) => {
onChange(values);
}}
/>
</FormControl>
<FormDescription>
<Trans>The events that will trigger a webhook to be sent to your URL.</Trans>
</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>
<Trans>
A secret that will be sent to your URL so you can verify that the request has
been sent by Documenso.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update webhook</Trans>
</Button>
</div>
</fieldset>
</form>
</Form>
<Alert
className="mt-6 flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>
<Trans>Test Webhook</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Send a test webhook with sample data to verify your integration is working correctly.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<WebhookTestDialog webhook={webhook}>
<Button variant="outline" disabled={!webhook.enabled}>
<Trans>Test Webhook</Trans>
</Button>
</WebhookTestDialog>
</div>
</Alert>
</div>
);
}

View File

@@ -1,7 +1,18 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import type { Webhook } from '@prisma/client';
import {
CheckCircle2Icon,
EditIcon,
Loader,
MoreHorizontalIcon,
ScrollTextIcon,
Trash2Icon,
XCircleIcon,
} from 'lucide-react';
import { DateTime } from 'luxon';
import { Link } from 'react-router';
@@ -10,9 +21,21 @@ 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 { DataTable, type DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { WebhookCreateDialog } from '~/components/dialogs/webhook-create-dialog';
import { WebhookDeleteDialog } from '~/components/dialogs/webhook-delete-dialog';
import { WebhookEditDialog } from '~/components/dialogs/webhook-edit-dialog';
import { SettingsHeader } from '~/components/general/settings-header';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
@@ -22,19 +45,72 @@ export function meta() {
}
export default function WebhookPage() {
const { _, i18n } = useLingui();
const { t, i18n } = useLingui();
const team = useCurrentTeam();
const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
teamId: team.id,
});
const { data, isLoading, isError } = trpc.webhook.getTeamWebhooks.useQuery();
const results = {
data: data ?? [],
perPage: 0,
currentPage: 0,
totalPages: 0,
};
const columns = useMemo(() => {
return [
{
header: t`Webhook`,
cell: ({ row }) => (
<Link to={`/t/${team.url}/settings/webhooks/${row.original.id}`}>
<p className="text-muted-foreground text-xs">{row.original.id}</p>
<p
className="text-foreground max-w-sm truncate text-xs font-semibold"
title={row.original.webhookUrl}
>
{row.original.webhookUrl}
</p>
</Link>
),
},
{
header: t`Status`,
cell: ({ row }) => (
<Badge variant={row.original.enabled ? 'default' : 'neutral'} size="small">
{row.original.enabled ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
</Badge>
),
},
{
header: t`Listening to`,
cell: ({ row }) => (
<p
className="text-foreground"
title={row.original.eventTriggers
.map((event) => toFriendlyWebhookEventName(event))
.join(', ')}
>
<Plural value={row.original.eventTriggers.length} one="# Event" other="# Events" />
</p>
),
},
{
header: t`Created`,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: t`Actions`,
cell: ({ row }) => <WebhookTableActionDropdown webhook={row.original} />,
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<div>
<SettingsHeader
title={_(msg`Webhooks`)}
subtitle={_(msg`On this page, you can create new Webhooks and manage the existing ones.`)}
title={t`Webhooks`}
subtitle={t`On this page, you can create new Webhooks and manage the existing ones.`}
>
<WebhookCreateDialog />
</SettingsHeader>
@@ -43,74 +119,95 @@ export default function WebhookPage() {
<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">
<Trans>
You have no webhooks yet. Your webhooks will be shown here once you create them.
</Trans>
</p>
</div>
)}
{webhooks && webhooks.length > 0 && (
<div className="mt-4 flex max-w-2xl 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 ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
</Badge>
</div>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>
Listening to{' '}
{webhook.eventTriggers
.map((trigger) => toFriendlyWebhookEventName(trigger))
.join(', ')}
</Trans>
</p>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
</p>
</div>
<div className="mt-4 flex flex-shrink-0 gap-4 sm:mt-0">
<Button asChild variant="outline">
<Link to={`/t/${team.url}/settings/webhooks/${webhook.id}`}>
<Trans>Edit</Trans>
</Link>
</Button>
<WebhookDeleteDialog webhook={webhook}>
<Button variant="destructive">
<Trans>Delete</Trans>
</Button>
</WebhookDeleteDialog>
</div>
</div>
</div>
))}
</div>
)}
<DataTable
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
error={{
enable: isError,
}}
emptyState={
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<p>
<Trans>
You have no webhooks yet. Your webhooks will be shown here once you create them.
</Trans>
</p>
</div>
}
skeleton={{
enable: isLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-8 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-6 rounded-full" />
</TableCell>
</>
),
}}
/>
</div>
);
}
const WebhookTableActionDropdown = ({ webhook }: { webhook: Webhook }) => {
const team = useCurrentTeam();
return (
<DropdownMenu>
<DropdownMenuTrigger data-testid="webhook-table-action-btn">
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" forceMount>
<DropdownMenuLabel>
<Trans>Action</Trans>
</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link to={`/t/${team.url}/settings/webhooks/${webhook.id}`}>
<ScrollTextIcon className="mr-2 h-4 w-4" />
<Trans>Logs</Trans>
</Link>
</DropdownMenuItem>
<WebhookEditDialog
webhook={webhook}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<EditIcon className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</div>
</DropdownMenuItem>
}
/>
<WebhookDeleteDialog webhook={webhook}>
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Trash2Icon className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</div>
</DropdownMenuItem>
</WebhookDeleteDialog>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -1,3 +1,5 @@
import { lazy } from 'react';
import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentSigningOrder, SigningStatus } from '@prisma/client';
@@ -14,14 +16,13 @@ import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-k
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog';
import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
import { EnvelopeRendererFileSelector } from '~/components/general/envelope-editor/envelope-file-selector';
import EnvelopeGenericPageRenderer from '~/components/general/envelope-editor/envelope-generic-page-renderer';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table';
@@ -34,6 +35,10 @@ import { useCurrentTeam } from '~/providers/team';
import type { Route } from './+types/templates.$id._index';
const EnvelopeGenericPageRenderer = lazy(
async () => import('~/components/general/envelope-editor/envelope-generic-page-renderer'),
);
export default function TemplatePage({ params }: Route.ComponentProps) {
const { t } = useLingui();
const { user } = useSession();
@@ -51,7 +56,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
if (isLoadingEnvelope) {
return (
<div className="text-foreground flex w-screen flex-col items-center justify-center gap-2 py-64">
<div className="flex w-screen flex-col items-center justify-center gap-2 py-64 text-foreground">
<Spinner />
<Trans>Loading</Trans>
</div>
@@ -205,7 +210,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
documentMeta={mockedDocumentMeta}
/>
<PDFViewer
<PDFViewerLazy
envelopeItem={envelope.envelopeItems[0]}
token={undefined}
version="signed"
@@ -219,9 +224,9 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
className={cn('col-span-12 lg:col-span-6 xl:col-span-5', isMultiEnvelopeItem && 'mt-20')}
>
<div className="space-y-6">
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
<section className="flex flex-col rounded-xl border border-border bg-widget pb-4 pt-6">
<div className="flex flex-row items-center justify-between px-4">
<h3 className="text-foreground text-2xl font-semibold">
<h3 className="text-2xl font-semibold text-foreground">
<Trans>Template</Trans>
</h3>
@@ -239,7 +244,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
</div>
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm">
<p className="mt-2 px-4 text-sm text-muted-foreground">
<Trans>Manage and view template</Trans>
</p>

View File

@@ -179,7 +179,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
{match({ status: signingStatus, deletedAt: document.deletedAt })
.with({ status: 'COMPLETED' }, () => (
<div className="text-documenso-700 mt-4 flex items-center text-center">
<div className="mt-4 flex items-center text-center text-documenso-700">
<CheckCircle2 className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>Everyone has signed</Trans>
@@ -213,29 +213,29 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
{match({ status: signingStatus, deletedAt: document.deletedAt })
.with({ status: 'COMPLETED' }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
<Trans>
Everyone has signed! You will receive an Email copy of the signed document.
Everyone has signed! You will receive an email copy of the signed document.
</Trans>
</p>
))
.with({ status: 'PROCESSING' }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
<Trans>
All recipients have signed. The document is being processed and you will receive
an Email copy shortly.
an email copy shortly.
</Trans>
</p>
))
.with({ deletedAt: null }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
<Trans>
You will receive an Email copy of the signed document once everyone has signed.
You will receive an email copy of the signed document once everyone has signed.
</Trans>
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
<Trans>
This document has been cancelled by the owner and is no longer available for
others to sign.
@@ -282,7 +282,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
<Trans>Need to sign documents?</Trans>
</h2>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
<p className="mt-4 max-w-[55ch] text-center leading-normal text-muted-foreground/60">
<Trans>
Create your account and start using state-of-the-art document signing.
</Trans>

View File

@@ -3,7 +3,7 @@ import sharp from 'sharp';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { prisma } from '@documenso/prisma';
import type { Route } from './+types/branding.logo.team.$teamId';
import type { Route } from './+types/branding.logo.organisation.$orgId';
export async function loader({ params }: Route.LoaderArgs) {
const organisationId = params.orgId;
@@ -69,7 +69,7 @@ export async function loader({ params }: Route.LoaderArgs) {
})
.toBuffer();
return new Response(img, {
return new Response(Buffer.from(img), {
headers: {
'Content-Type': 'image/png',
'Content-Length': img.length.toString(),

View File

@@ -1,3 +1,4 @@
// Todo: [Webhooks] delete file after deployment.
import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trigger/handler';
import type { Route } from './+types/webhook.trigger';