Compare commits

...

12 Commits

Author SHA1 Message Date
2d569292ce fix: refactor 2024-04-15 20:29:42 +07:00
b1474de2da fix: fix UI issue for enterprise 2024-04-15 15:22:58 +03:00
6b8ff4567d chore: merged main 2024-04-15 12:02:04 +03:00
aa4b6f1723 feat: updated mobile header (#1004)
**Description:**

- Updated mobile header with respect to latest designs 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a new `showText` property to the `MenuSwitcher` component to
control text visibility.
- Added a `textSectionClassName` property to the `AvatarWithText`
component for conditional text section styling.
- Updated the `CommandDialog` and `DialogContent` components with new
positioning and styling properties.

- **Style Updates**
- Adjusted text size responsiveness in the `Hero` component for various
screen sizes.
- Modified text truncation and input styling in the `Widget` component.
- Changed the width of the `SheetContent` element in `MobileNavigation`
and adjusted footer layout.

- **Documentation**
  - Added instructions for certificate placement in `SIGNING.md`.

- **Refactor**
- Standardized type imports across various components and utilities for
improved type checking.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Adithya Krishna <adithya@documenso.com>
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
Co-authored-by: David Nguyen <davidngu28@gmail.com>
2024-04-15 15:52:34 +07:00
c8a09099a3 fix: mask recipient token (#1051)
The searchDocuments function is used for the shortcuts commands, afaik.
The function returns the documents that match the user query (if any),
alongside all their recipients.

The reason for that is so it can build the path for the document. E.g.
if you're the document owner, the document path will be
`..../documents/{id}`. But if you're a signer for example, the document
path (link) will be `..../sign/{token}`.

So instead of doing that on the frontend, I moved it to the backend.

At least that's what I understood. If I'm wrong, please correct me.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced the `CommandMenu` component to simplify search result
generation and improve document link management based on user roles.
- **Refactor**
- Updated document search logic to include recipient token masking and
refined document mapping.
- **Style**
	- Minor formatting improvement in document routing code.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-15 14:29:56 +07:00
0f87dc047b fix: swagger documentation authentication (#1037)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit


- **Refactor**
- Enhanced the API specification generation process to include operation
IDs, security schemes, and security definitions more efficiently.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-04-15 14:27:46 +07:00
80c758fb62 chore: audit log menu item label (#1102) 2024-04-12 20:37:08 +07:00
1a81b46859 Merge branch 'main' into feat/add-myself-as-signer 2024-04-11 13:27:14 +03:00
ef1d4ed0fa fix: make sure the user can't add its details more than once 2024-04-11 13:25:54 +03:00
318a77c936 feat: add myself as signer in templates 2024-04-10 10:32:22 +03:00
2bc0407d06 Merge branch 'main' into feat/add-myself-as-signer 2024-04-10 09:56:31 +03:00
80de7d3ea9 feat: add myself as signer 2024-04-10 09:46:15 +03:00
27 changed files with 199 additions and 100 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`
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

View File

@ -1,4 +1,5 @@
import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types';
import type { TClaimPlanRequestSchema } from './types';
import { ZClaimPlanResponseSchema } from './types';
export const claimPlan = async ({
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%)' }}
/>
<Bar
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dataKey={metricKey as string}
maxBarSize={60}
fill="hsl(var(--primary))"

View File

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

View File

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

View File

@ -2,13 +2,14 @@
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 { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
import { TOSSFriendsSchema } from './schema';
import type { TOSSFriendsSchema } from './schema';
const ContainerVariants: Variants = {
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';

View File

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

View File

@ -96,7 +96,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
variants={HeroTitleVariants}
initial="initial"
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,
<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';

View File

@ -346,7 +346,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
{signatureText && (
<p
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}
@ -360,7 +360,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
>
<Input
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"
disabled={isSubmitting}
{...register('signatureText', {

View File

@ -1,5 +1,5 @@
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';

View File

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

View File

@ -3,7 +3,7 @@
import * as React from 'react';
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) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

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

View File

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

View File

@ -93,7 +93,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<Button
data-testid="menu-switcher"
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
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
@ -102,12 +102,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
rightSideComponent={
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
}
textSectionClassName="hidden lg:flex"
/>
</Button>
</DropdownMenuTrigger>
<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"
forceMount
>

View File

@ -46,7 +46,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
return (
<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}>
<Image
src={LogoImage}
@ -87,7 +87,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
</div>
<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>
</div>
</SheetContent>

View File

@ -2,16 +2,34 @@ import { generateOpenApi } from '@ts-rest/open-api';
import { ApiContractV1 } from './contract';
export const OpenAPIV1 = generateOpenApi(
ApiContractV1,
{
info: {
title: 'Documenso API',
version: '1.0.0',
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
export const OpenAPIV1 = Object.assign(
generateOpenApi(
ApiContractV1,
{
info: {
title: 'Documenso API',
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 { DocumentStatus } from '@documenso/prisma/client';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
import type { Document, Recipient, User } from '@documenso/prisma/client';
export type SearchDocumentsWithKeywordOptions = {
query: string;
@ -79,12 +78,19 @@ export const searchDocumentsWithKeyword = async ({
take: limit,
});
const maskedDocuments = documents.map((document) =>
maskRecipientTokensForDocument({
document,
user,
}),
);
const isOwner = (document: Document, user: User) => document.userId === user.id;
const getSigningLink = (recipients: Recipient[], user: User) =>
`/sign/${recipients.find((r) => r.email === user.email)?.token}`;
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;
};

View File

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

View File

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

View File

@ -32,7 +32,11 @@ type CommandDialogProps = DialogProps & {
const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => {
return (
<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
{...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"

View File

@ -54,28 +54,35 @@ const DialogContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
position?: 'start' | 'end' | 'center';
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 />
<DialogPrimitive.Content
ref={ref}
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',
className,
)}
{...props}
>
{children}
{!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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
>(
(
{ className, children, overlayClassName, position = 'start', hideClose = false, ...props },
ref,
) => (
<DialogPortal position={position}>
<DialogOverlay className={cn(overlayClassName)} />
<DialogPrimitive.Content
ref={ref}
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',
className,
)}
{...props}
>
{children}
{!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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
),
);
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 { motion } from 'framer-motion';
import { InfoIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -60,6 +61,8 @@ export const AddSignersFormPartial = ({
}: AddSignersFormProps) => {
const { toast } = useToast();
const { remaining } = useLimits();
const { data: session } = useSession();
const user = session?.user;
const initialId = useId();
@ -135,6 +138,18 @@ export const AddSignersFormPartial = ({
);
};
const onAddSelfSigner = () => {
const newSelfSignerId = nanoid(12);
appendSigner({
formId: newSelfSignerId,
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
actionAuth: undefined,
});
};
const onAddSigner = () => {
appendSigner({
formId: nanoid(12),
@ -209,8 +224,12 @@ export const AddSignersFormPartial = ({
<Input
type="email"
placeholder="Email"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown}
/>
</FormControl>
@ -237,8 +256,12 @@ export const AddSignersFormPartial = ({
<FormControl>
<Input
placeholder="Name"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown}
/>
</FormControl>
@ -403,32 +426,46 @@ export const AddSignersFormPartial = ({
>
<Button
type="button"
className="flex-1"
disabled={isSubmitting || signers.length >= remaining.recipients}
onClick={() => onAddSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Signer
</Button>
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
<div className="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>
)}
<Button
type="button"
variant="secondary"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
disabled={
isSubmitting ||
form.getValues('signers').some((signer) => signer.email === user?.email)
}
onClick={() => onAddSelfSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add myself
</Button>
</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>
</AnimateGenericFadeInOut>
</DocumentFlowFormContainerContent>

View File

@ -5,6 +5,7 @@ import React, { useId, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id';
@ -41,6 +42,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onSubmit,
}: AddTemplatePlaceholderRecipientsFormProps) => {
const initialId = useId();
const { data: session } = useSession();
const user = session?.user;
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
recipients.length > 1 ? recipients.length + 1 : 2,
);
@ -50,6 +53,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const {
control,
handleSubmit,
getValues,
formState: { errors, isSubmitting },
} = useForm<TAddTemplatePlacholderRecipientsFormSchema>({
resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema),
@ -85,6 +89,17 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
name: 'signers',
});
const onAddPlaceholderSelfRecipient = () => {
const newSelfSignerId = nanoid(12);
appendSigner({
formId: newSelfSignerId,
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
});
};
const onAddPlaceholderRecipient = () => {
appendSigner({
formId: nanoid(12),
@ -203,11 +218,27 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
error={'signers__root' in errors && errors['signers__root']}
/>
<div className="mt-4">
<Button type="button" disabled={isSubmitting} onClick={() => onAddPlaceholderRecipient()}>
<div className="mt-4 flex flex-row items-center space-x-4">
<Button
type="button"
className="flex-1"
disabled={isSubmitting}
onClick={() => onAddPlaceholderRecipient()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Placeholder Recipient
</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>
</DocumentFlowFormContainerContent>