Merge branch 'main' of https://github.com/documenso/documenso into feat/redirect-templates

This commit is contained in:
Adithya Krishna
2024-04-18 17:49:08 +05:30
31 changed files with 395 additions and 191 deletions

View File

@ -17,7 +17,8 @@ For the digital signature of your documents you need a signing certificate in .p
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt` `openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**) 4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate `/apps/web/resources/certificate.p12`
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
## Docker ## Docker

View File

@ -1,4 +1,5 @@
import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types'; import type { TClaimPlanRequestSchema } from './types';
import { ZClaimPlanResponseSchema } from './types';
export const claimPlan = async ({ export const claimPlan = async ({
name, name,

View File

@ -55,6 +55,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
cursor={{ fill: 'hsl(var(--primary) / 10%)' }} cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/> />
<Bar <Bar
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dataKey={metricKey as string} dataKey={metricKey as string}
maxBarSize={60} maxBarSize={60}
fill="hsl(var(--primary))" fill="hsl(var(--primary))"

View File

@ -13,6 +13,7 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => { export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({ const formattedData = data.map((item) => ({
amount: Number(item.amount), amount: Number(item.amount),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
date: formatMonth(item.date as string), date: formatMonth(item.date as string),
})); }));

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { import {

View File

@ -2,13 +2,14 @@
import Link from 'next/link'; import Link from 'next/link';
import { Variants, motion } from 'framer-motion'; import type { Variants } from 'framer-motion';
import { motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card'; import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { TOSSFriendsSchema } from './schema'; import type { TOSSFriendsSchema } from './schema';
const ContainerVariants: Variants = { const ContainerVariants: Variants = {
initial: { initial: {

View File

@ -1,4 +1,4 @@
import { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';

View File

@ -1,4 +1,4 @@
import { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
import { allBlogPosts, allGenericPages } from 'contentlayer/generated'; import { allBlogPosts, allGenericPages } from 'contentlayer/generated';

View File

@ -96,7 +96,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
variants={HeroTitleVariants} variants={HeroTitleVariants}
initial="initial" initial="initial"
animate="animate" animate="animate"
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]" className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]"
> >
Document signing, Document signing,
<span className="block" /> finally open source. <span className="block" /> finally open source.

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import Image from 'next/image'; import Image from 'next/image';

View File

@ -346,7 +346,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
{signatureText && ( {signatureText && (
<p <p
className={cn( className={cn(
'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]', 'text-foreground truncate text-4xl font-semibold [font-family:var(--font-caveat)]',
)} )}
> >
{signatureText} {signatureText}
@ -360,7 +360,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
> >
<Input <Input
id="signatureText" id="signatureText"
className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0" className="text-foreground placeholder:text-muted-foreground truncate border-none p-0 text-sm focus-visible:ring-0"
placeholder="Draw or type name here" placeholder="Draw or type name here"
disabled={isSubmitting} disabled={isSubmitting}
{...register('signatureText', { {...register('signatureText', {

View File

@ -1,5 +1,5 @@
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { FieldError } from 'react-hook-form'; import type { FieldError } from 'react-hook-form';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';

View File

@ -1,4 +1,4 @@
import { SVGAttributes } from 'react'; import type { SVGAttributes } from 'react';
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>; export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;

View File

@ -3,7 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types'; import type { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@ -118,7 +118,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/logs`}> <Link href={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" /> <ScrollTextIcon className="mr-2 h-4 w-4" />
Logs Audit Log
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -18,7 +18,10 @@ import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { import {
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@ -34,7 +37,6 @@ import {
FormMessage, FormMessage,
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
const ZCreateTemplateFormSchema = z.object({ const ZCreateTemplateFormSchema = z.object({
@ -61,8 +63,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
resolver: zodResolver(ZCreateTemplateFormSchema), resolver: zodResolver(ZCreateTemplateFormSchema),
}); });
const { mutateAsync: createTemplate, isLoading: isCreatingTemplate } = const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
trpc.template.createTemplate.useMutation();
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false); const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>(); const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
@ -140,6 +141,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
useEffect(() => { useEffect(() => {
if (!showNewTemplateDialog) { if (!showNewTemplateDialog) {
form.reset(); form.reset();
setUploadedFile(null);
} }
}, [form, showNewTemplateDialog]); }, [form, showNewTemplateDialog]);
@ -154,20 +156,23 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
<DialogContent className="w-full max-w-xl"> <DialogContent className="w-full max-w-xl">
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-4">New Template</DialogTitle> <DialogTitle>New Template</DialogTitle>
<DialogDescription>
Templates allow you to quickly generate documents with pre-filled recipients and fields.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div> <Form {...form}>
<Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4"> <fieldset disabled={form.formState.isSubmitting} className="flex flex-col gap-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name your template</FormLabel> <FormLabel>Template name</FormLabel>
<FormControl> <FormControl>
<Input id="email" type="text" className="bg-background mt-1.5" {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
@ -180,55 +185,57 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
)} )}
/> />
<div> <div className="mt-1.5">
<Label htmlFor="template">Upload a Document</Label> {uploadedFile ? (
<Card gradient className="h-[40vh]">
<CardContent className="flex h-full flex-col items-center justify-center p-2">
<button
onClick={() => resetForm()}
title="Remove Template"
className="text-muted-foreground absolute right-2.5 top-2.5 rounded-sm opacity-60 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<X className="h-6 w-6" />
<span className="sr-only">Remove Template</span>
</button>
<div className="my-3"> <div className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm">
{uploadedFile ? ( <div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<Card gradient className="h-[40vh]"> <div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<CardContent className="flex h-full flex-col items-center justify-center p-2"> <div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<button </div>
onClick={() => resetForm()}
title="Remove Template"
className="text-muted-foreground absolute right-2.5 top-2.5 rounded-sm opacity-60 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<X className="h-6 w-6" />
<span className="sr-only">Remove Template</span>
</button>
<div className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"> <p className="group-hover:text-foreground text-muted-foreground mt-4 font-medium">
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" /> Uploaded Document
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" /> </p>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</div>
<p className="group-hover:text-foreground text-muted-foreground mt-4 font-medium"> <span className="text-muted-foreground/80 mt-1 text-sm">
Uploaded Document {uploadedFile.file.name}
</p> </span>
</CardContent>
<span className="text-muted-foreground/80 mt-1 text-sm"> </Card>
{uploadedFile.file.name} ) : (
</span> <DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
</CardContent> )}
</Card>
) : (
<DocumentDropzone
className="mt-1.5 h-[40vh]"
onDrop={onFileDrop}
type="template"
/>
)}
</div>
</div> </div>
<div className="flex w-full justify-end"> <DialogFooter>
<Button loading={isCreatingTemplate} type="submit"> <DialogClose asChild>
Create Template <Button type="button" variant="secondary">
Cancel
</Button>
</DialogClose>
<Button
loading={form.formState.isSubmitting}
disabled={!uploadedFile}
type="submit"
>
Create template
</Button> </Button>
</div> </DialogFooter>
</form> </fieldset>
</Form> </form>
</div> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -131,7 +131,7 @@ export default async function CompletedSigningPage({
</div> </div>
)) ))
.with({ deletedAt: null }, () => ( .with({ deletedAt: null }, () => (
<div className="flex items-center text-center text-blue-600"> <div className="mt-4 flex items-center text-center text-blue-600">
<Clock8 className="mr-2 h-5 w-5" /> <Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">Waiting for others to sign</span> <span className="text-sm">Waiting for others to sign</span>
</div> </div>

View File

@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Loader, Monitor, Moon, Sun } from 'lucide-react'; import { Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -18,7 +17,6 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION, DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META, SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc'; } from '@documenso/lib/constants/trpc';
import type { Document, Recipient } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { import {
CommandDialog, CommandDialog,
@ -71,7 +69,6 @@ export type CommandMenuProps = {
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const { data: session } = useSession();
const router = useRouter(); const router = useRouter();
@ -93,17 +90,6 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
}, },
); );
const isOwner = useCallback(
(document: Document) => document.userId === session?.user.id,
[session?.user.id],
);
const getSigningLink = useCallback(
(recipients: Recipient[]) =>
`/sign/${recipients.find((r) => r.email === session?.user.email)?.token}`,
[session?.user.email],
);
const searchResults = useMemo(() => { const searchResults = useMemo(() => {
if (!searchDocumentsData) { if (!searchDocumentsData) {
return []; return [];
@ -111,10 +97,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
return searchDocumentsData.map((document) => ({ return searchDocumentsData.map((document) => ({
label: document.title, label: document.title,
path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient), path: document.path,
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '), value: document.value,
})); }));
}, [searchDocumentsData, isOwner, getSigningLink]); }, [searchDocumentsData]);
const currentPage = pages[pages.length - 1]; const currentPage = pages[pages.length - 1];

View File

@ -1,5 +1,3 @@
'use client';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -12,8 +10,6 @@ import { getRootHref } from '@documenso/lib/utils/params';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { CommandMenu } from '../common/command-menu';
const navigationLinks = [ const navigationLinks = [
{ {
href: '/documents', href: '/documents',
@ -25,13 +21,14 @@ const navigationLinks = [
}, },
]; ];
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>; export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
setIsCommandMenuOpen: (value: boolean) => void;
};
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
const pathname = usePathname(); const pathname = usePathname();
const params = useParams(); const params = useParams();
const [open, setOpen] = useState(false);
const [modifierKey, setModifierKey] = useState(() => 'Ctrl'); const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
const rootHref = getRootHref(params, { returnEmptyRootString: true }); const rootHref = getRootHref(params, { returnEmptyRootString: true });
@ -70,12 +67,10 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
))} ))}
</div> </div>
<CommandMenu open={open} onOpenChange={setOpen} />
<Button <Button
variant="outline" variant="outline"
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg" className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
onClick={() => setOpen((open) => !open)} onClick={() => setIsCommandMenuOpen(true)}
> >
<div className="flex items-center"> <div className="flex items-center">
<Search className="mr-2 h-5 w-5" /> <Search className="mr-2 h-5 w-5" />

View File

@ -58,7 +58,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
<Logo className="h-6 w-auto" /> <Logo className="h-6 w-auto" />
</Link> </Link>
<DesktopNav /> <DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
<div className="flex gap-x-4 md:ml-8"> <div className="flex gap-x-4 md:ml-8">
<MenuSwitcher user={user} teams={teams} /> <MenuSwitcher user={user} teams={teams} />

View File

@ -93,7 +93,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<Button <Button
data-testid="menu-switcher" data-testid="menu-switcher"
variant="none" variant="none"
className="relative flex h-12 flex-row items-center px-2 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent" className="relative flex h-12 flex-row items-center px-0 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent md:px-2"
> >
<AvatarWithText <AvatarWithText
avatarFallback={formatAvatarFallback(selectedTeam?.name)} avatarFallback={formatAvatarFallback(selectedTeam?.name)}
@ -102,12 +102,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
rightSideComponent={ rightSideComponent={
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" /> <ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
} }
textSectionClassName="hidden lg:flex"
/> />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className={cn('z-[60] ml-2 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')} className={cn('z-[60] ml-6 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
align="end" align="end"
forceMount forceMount
> >

View File

@ -46,7 +46,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
return ( return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}> <Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="flex w-full max-w-[400px] flex-col"> <SheetContent className="flex w-full max-w-[350px] flex-col">
<Link href="/" onClick={handleMenuItemClick}> <Link href="/" onClick={handleMenuItemClick}>
<Image <Image
src={LogoImage} src={LogoImage}
@ -87,7 +87,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
</div> </div>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved. © {new Date().getFullYear()} Documenso, Inc. <br /> All rights reserved.
</p> </p>
</div> </div>
</SheetContent> </SheetContent>

View File

@ -2,16 +2,34 @@ import { generateOpenApi } from '@ts-rest/open-api';
import { ApiContractV1 } from './contract'; import { ApiContractV1 } from './contract';
export const OpenAPIV1 = generateOpenApi( export const OpenAPIV1 = Object.assign(
ApiContractV1, generateOpenApi(
{ ApiContractV1,
info: { {
title: 'Documenso API', info: {
version: '1.0.0', title: 'Documenso API',
description: 'The Documenso API for retrieving, creating, updating and deleting documents.', version: '1.0.0',
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
},
}, },
}, {
setOperationId: true,
},
),
{ {
setOperationId: true, components: {
securitySchemes: {
authorization: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
},
},
},
security: [
{
authorization: [],
},
],
}, },
); );

View File

@ -1,7 +1,6 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client';
import type { Document, Recipient, User } from '@documenso/prisma/client';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
export type SearchDocumentsWithKeywordOptions = { export type SearchDocumentsWithKeywordOptions = {
query: string; query: string;
@ -79,12 +78,19 @@ export const searchDocumentsWithKeyword = async ({
take: limit, take: limit,
}); });
const maskedDocuments = documents.map((document) => const isOwner = (document: Document, user: User) => document.userId === user.id;
maskRecipientTokensForDocument({ const getSigningLink = (recipients: Recipient[], user: User) =>
document, `/sign/${recipients.find((r) => r.email === user.email)?.token}`;
user,
}), const maskedDocuments = documents.map((document) => {
); const { Recipient, ...documentWithoutRecipient } = document;
return {
...documentWithoutRecipient,
path: isOwner(document, user) ? `/documents/${document.id}` : getSigningLink(Recipient, user),
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
};
});
return maskedDocuments; return maskedDocuments;
}; };

View File

@ -358,6 +358,7 @@ export const documentRouter = router({
query, query,
userId: ctx.user.id, userId: ctx.user.id,
}); });
return documents; return documents;
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -55,6 +55,8 @@ type AvatarWithTextProps = {
primaryText: React.ReactNode; primaryText: React.ReactNode;
secondaryText?: React.ReactNode; secondaryText?: React.ReactNode;
rightSideComponent?: React.ReactNode; rightSideComponent?: React.ReactNode;
// Optional class to hide/show the text beside avatar
textSectionClassName?: string;
}; };
const AvatarWithText = ({ const AvatarWithText = ({
@ -64,6 +66,7 @@ const AvatarWithText = ({
primaryText, primaryText,
secondaryText, secondaryText,
rightSideComponent, rightSideComponent,
textSectionClassName,
}: AvatarWithTextProps) => ( }: AvatarWithTextProps) => (
<div className={cn('flex w-full max-w-xs items-center gap-2', className)}> <div className={cn('flex w-full max-w-xs items-center gap-2', className)}>
<Avatar <Avatar
@ -72,7 +75,7 @@ const AvatarWithText = ({
<AvatarFallback className="text-xs text-gray-400">{avatarFallback}</AvatarFallback> <AvatarFallback className="text-xs text-gray-400">{avatarFallback}</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col text-left text-sm font-normal"> <div className={cn('flex flex-col text-left text-sm font-normal', textSectionClassName)}>
<span className="text-foreground truncate">{primaryText}</span> <span className="text-foreground truncate">{primaryText}</span>
<span className="text-muted-foreground truncate text-xs">{secondaryText}</span> <span className="text-muted-foreground truncate text-xs">{secondaryText}</span>
</div> </div>

View File

@ -32,7 +32,11 @@ type CommandDialogProps = DialogProps & {
const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-2xl"> <DialogContent
className="w-11/12 items-center overflow-hidden rounded-lg p-0 shadow-2xl lg:mt-0"
position="center"
overlayClassName="bg-background/60"
>
<Command <Command
{...commandProps} {...commandProps}
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4" className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"

View File

@ -54,28 +54,35 @@ const DialogContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
position?: 'start' | 'end' | 'center'; position?: 'start' | 'end' | 'center';
hideClose?: boolean; hideClose?: boolean;
/* Below prop is to add additional classes to the overlay */
overlayClassName?: string;
} }
>(({ className, children, position = 'start', hideClose = false, ...props }, ref) => ( >(
<DialogPortal position={position}> (
<DialogOverlay /> { className, children, overlayClassName, position = 'start', hideClose = false, ...props },
<DialogPrimitive.Content ref,
ref={ref} ) => (
className={cn( <DialogPortal position={position}>
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg', <DialogOverlay className={cn(overlayClassName)} />
className, <DialogPrimitive.Content
)} ref={ref}
{...props} className={cn(
> 'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
{children} className,
{!hideClose && ( )}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"> {...props}
<X className="h-4 w-4" /> >
<span className="sr-only">Close</span> {children}
</DialogPrimitive.Close> {!hideClose && (
)} <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
</DialogPrimitive.Content> <X className="h-4 w-4" />
</DialogPortal> <span className="sr-only">Close</span>
)); </DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
),
);
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName;

View File

@ -5,6 +5,7 @@ import React, { useId, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { InfoIcon, Plus, Trash } from 'lucide-react'; import { InfoIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -60,6 +61,8 @@ export const AddSignersFormPartial = ({
}: AddSignersFormProps) => { }: AddSignersFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { remaining } = useLimits(); const { remaining } = useLimits();
const { data: session } = useSession();
const user = session?.user;
const initialId = useId(); const initialId = useId();
@ -135,6 +138,16 @@ export const AddSignersFormPartial = ({
); );
}; };
const onAddSelfSigner = () => {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
actionAuth: undefined,
});
};
const onAddSigner = () => { const onAddSigner = () => {
appendSigner({ appendSigner({
formId: nanoid(12), formId: nanoid(12),
@ -209,8 +222,12 @@ export const AddSignersFormPartial = ({
<Input <Input
type="email" type="email"
placeholder="Email" placeholder="Email"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field} {...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
</FormControl> </FormControl>
@ -237,8 +254,12 @@ export const AddSignersFormPartial = ({
<FormControl> <FormControl>
<Input <Input
placeholder="Name" placeholder="Name"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field} {...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
</FormControl> </FormControl>
@ -340,29 +361,83 @@ export const AddSignersFormPartial = ({
<SelectContent align="end"> <SelectContent align="end">
<SelectItem value={RecipientRole.SIGNER}> <SelectItem value={RecipientRole.SIGNER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span> <div className="flex w-[150px] items-center">
Signer <span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
</div> Needs to sign
</SelectItem> </div>
<Tooltip>
<SelectItem value={RecipientRole.CC}> <TooltipTrigger>
<div className="flex items-center"> <InfoIcon className="h-4 w-4" />
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span> </TooltipTrigger>
Receives copy <TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to sign the document for it to be
completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value={RecipientRole.APPROVER}> <SelectItem value={RecipientRole.APPROVER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span> <div className="flex w-[150px] items-center">
Approver <span className="mr-2">
{ROLE_ICONS[RecipientRole.APPROVER]}
</span>
Needs to approve
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to approve the document for it to
be completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value={RecipientRole.VIEWER}> <SelectItem value={RecipientRole.VIEWER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span> <div className="flex w-[150px] items-center">
Viewer <span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
Needs to view
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to view the document for it to be
completed.
</p>
</TooltipContent>
</Tooltip>
</div>
</SelectItem>
<SelectItem value={RecipientRole.CC}>
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
Receives copy
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is not required to take any action and
receives a copy of the document after it is completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -403,32 +478,46 @@ export const AddSignersFormPartial = ({
> >
<Button <Button
type="button" type="button"
className="flex-1"
disabled={isSubmitting || signers.length >= remaining.recipients} disabled={isSubmitting || signers.length >= remaining.recipients}
onClick={() => onAddSigner()} onClick={() => onAddSigner()}
> >
<Plus className="-ml-1 mr-2 h-5 w-5" /> <Plus className="-ml-1 mr-2 h-5 w-5" />
Add Signer Add Signer
</Button> </Button>
<Button
{!alwaysShowAdvancedSettings && isDocumentEnterprise && ( type="button"
<div className="flex flex-row items-center"> variant="secondary"
<Checkbox className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
id="showAdvancedRecipientSettings" disabled={
className="h-5 w-5" isSubmitting ||
checkClassName="dark:text-white text-primary" form.getValues('signers').some((signer) => signer.email === user?.email)
checked={showAdvancedSettings} }
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))} onClick={() => onAddSelfSigner()}
/> >
<Plus className="-ml-1 mr-2 h-5 w-5" />
<label Add myself
className="text-muted-foreground ml-2 text-sm" </Button>
htmlFor="showAdvancedRecipientSettings"
>
Show advanced settings
</label>
</div>
)}
</div> </div>
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
<div className="mt-4 flex flex-row items-center">
<Checkbox
id="showAdvancedRecipientSettings"
className="h-5 w-5"
checkClassName="dark:text-white text-primary"
checked={showAdvancedSettings}
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))}
/>
<label
className="text-muted-foreground ml-2 text-sm"
htmlFor="showAdvancedRecipientSettings"
>
Show advanced settings
</label>
</div>
)}
</Form> </Form>
</AnimateGenericFadeInOut> </AnimateGenericFadeInOut>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>

View File

@ -4,7 +4,8 @@ import React, { useId, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react'; import { InfoIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
@ -24,6 +25,7 @@ import type { DocumentFlowStep } from '../document-flow/types';
import { ROLE_ICONS } from '../recipient-role-icons'; import { ROLE_ICONS } from '../recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select'; import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
@ -41,6 +43,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onSubmit, onSubmit,
}: AddTemplatePlaceholderRecipientsFormProps) => { }: AddTemplatePlaceholderRecipientsFormProps) => {
const initialId = useId(); const initialId = useId();
const { data: session } = useSession();
const user = session?.user;
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() => const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
recipients.length > 1 ? recipients.length + 1 : 2, recipients.length > 1 ? recipients.length + 1 : 2,
); );
@ -50,6 +54,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const { const {
control, control,
handleSubmit, handleSubmit,
getValues,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<TAddTemplatePlacholderRecipientsFormSchema>({ } = useForm<TAddTemplatePlacholderRecipientsFormSchema>({
resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema), resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema),
@ -86,6 +91,15 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
name: 'signers', name: 'signers',
}); });
const onAddPlaceholderSelfRecipient = () => {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
});
};
const onAddPlaceholderRecipient = () => { const onAddPlaceholderRecipient = () => {
appendSigner({ appendSigner({
formId: nanoid(12), formId: nanoid(12),
@ -155,29 +169,81 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<SelectContent className="" align="end"> <SelectContent className="" align="end">
<SelectItem value={RecipientRole.SIGNER}> <SelectItem value={RecipientRole.SIGNER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span> <div className="flex w-[150px] items-center">
Signer <span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
</div> Needs to sign
</SelectItem> </div>
<Tooltip>
<SelectItem value={RecipientRole.CC}> <TooltipTrigger>
<div className="flex items-center"> <InfoIcon className="h-4 w-4" />
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span> </TooltipTrigger>
Receives copy <TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to sign the document for it to be
completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value={RecipientRole.APPROVER}> <SelectItem value={RecipientRole.APPROVER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span> <div className="flex w-[150px] items-center">
Approver <span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
Needs to approve
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to approve the document for it to be
completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value={RecipientRole.VIEWER}> <SelectItem value={RecipientRole.VIEWER}>
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span> <div className="flex w-[150px] items-center">
Viewer <span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
Needs to view
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is required to view the document for it to be
completed.
</p>
</TooltipContent>
</Tooltip>
</div>
</SelectItem>
<SelectItem value={RecipientRole.CC}>
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
Receives copy
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
The recipient is not required to take any action and receives a
copy of the document after it is completed.
</p>
</TooltipContent>
</Tooltip>
</div> </div>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -212,11 +278,27 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
error={'signers__root' in errors && errors['signers__root']} error={'signers__root' in errors && errors['signers__root']}
/> />
<div className="mt-4"> <div className="mt-4 flex flex-row items-center space-x-4">
<Button type="button" disabled={isSubmitting} onClick={() => onAddPlaceholderRecipient()}> <Button
type="button"
className="flex-1"
disabled={isSubmitting}
onClick={() => onAddPlaceholderRecipient()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" /> <Plus className="-ml-1 mr-2 h-5 w-5" />
Add Placeholder Recipient Add Placeholder Recipient
</Button> </Button>
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
disabled={
isSubmitting || getValues('signers').some((signer) => signer.email === user?.email)
}
onClick={() => onAddPlaceholderSelfRecipient()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Myself
</Button>
</div> </div>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>