mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add recipient roles (#716)
Fixes #705 --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me> Co-authored-by: David Nguyen <davidngu28@gmail.com>
This commit is contained in:
@ -109,7 +109,7 @@ It's similar to the Kanban board for the development backlog.
|
|||||||
While the internal design backlog also existed in Linear, the public design repository is new. Since designing in the open is tricky, we opted to publish the detailed design artifacts with the corresponding feature instead.
|
While the internal design backlog also existed in Linear, the public design repository is new. Since designing in the open is tricky, we opted to publish the detailed design artifacts with the corresponding feature instead.
|
||||||
We already have design.documenso.com housing our general design system. Here, we will publish the specifics of how we applied this to each feature. We will publish the first artifacts here soon, what may be in the cards can be found on the [LIVE Roadmap](https://documen.so/live).
|
We already have design.documenso.com housing our general design system. Here, we will publish the specifics of how we applied this to each feature. We will publish the first artifacts here soon, what may be in the cards can be found on the [LIVE Roadmap](https://documen.so/live).
|
||||||
|
|
||||||
Feel free to connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here to help and would love to hear from you :)
|
Feel free to connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here to help and would love to hear from you :)
|
||||||
|
|
||||||
Best from Hamburg\
|
Best from Hamburg\
|
||||||
Timur
|
Timur
|
||||||
|
|||||||
@ -158,6 +158,7 @@ export const SinglePlayerClient = () => {
|
|||||||
readStatus: 'OPENED',
|
readStatus: 'OPENED',
|
||||||
signingStatus: 'NOT_SIGNED',
|
signingStatus: 'NOT_SIGNED',
|
||||||
sendStatus: 'NOT_SENT',
|
sendStatus: 'NOT_SENT',
|
||||||
|
role: 'SIGNER',
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Download, Edit, Pencil } from 'lucide-react';
|
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -37,6 +37,7 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
const isPending = row.status === DocumentStatus.PENDING;
|
const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
const role = recipient?.role;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
@ -68,6 +69,11 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
||||||
|
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
isOwner,
|
isOwner,
|
||||||
isRecipient,
|
isRecipient,
|
||||||
@ -87,15 +93,32 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||||
<Button className="w-32" asChild>
|
<Button className="w-32" asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
{match(role)
|
||||||
Sign
|
.with(RecipientRole.SIGNER, () => (
|
||||||
|
<>
|
||||||
|
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.with(RecipientRole.APPROVER, () => (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
Approve
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<>
|
||||||
|
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||||
|
View
|
||||||
|
</>
|
||||||
|
))}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isPending: true, isSigned: true }, () => (
|
.with({ isPending: true, isSigned: true }, () => (
|
||||||
<Button className="w-32" disabled={true}>
|
<Button className="w-32" disabled={true}>
|
||||||
<Pencil className="-ml-1 mr-2 inline h-4 w-4" />
|
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||||
Sign
|
View
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import { useState } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CheckCircle,
|
||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
|
EyeIcon,
|
||||||
Loader,
|
Loader,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
@ -19,7 +21,7 @@ import { useSession } from 'next-auth/react';
|
|||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
@ -105,12 +107,32 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
{recipient?.role !== RecipientRole.CC && (
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
Sign
|
{recipient?.role === RecipientRole.VIEWER && (
|
||||||
</Link>
|
<>
|
||||||
</DropdownMenuItem>
|
<EyeIcon className="mr-2 h-4 w-4" />
|
||||||
|
View
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recipient?.role === RecipientRole.SIGNER && (
|
||||||
|
<>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Sign
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recipient?.role === RecipientRole.APPROVER && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="mr-2 h-4 w-4" />
|
||||||
|
Approve
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
|
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
|
||||||
<Link href={`/documents/${row.id}`}>
|
<Link href={`/documents/${row.id}`}>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
|
|||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
@ -94,7 +94,10 @@ export default async function CompletedSigningPage({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
You have signed
|
You have
|
||||||
|
{recipient.role === RecipientRole.SIGNER && ' signed '}
|
||||||
|
{recipient.role === RecipientRole.VIEWER && ' viewed '}
|
||||||
|
{recipient.role === RecipientRole.APPROVER && ' approved '}
|
||||||
<span className="mt-1.5 block">"{truncatedTitle}"</span>
|
<span className="mt-1.5 block">"{truncatedTitle}"</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
import { type Document, type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -96,74 +96,114 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
|
|
||||||
<fieldset
|
<fieldset
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
className={cn(
|
||||||
|
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div className={cn('flex flex-1 flex-col')}>
|
||||||
className={cn(
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
{recipient.role === RecipientRole.VIEWER && 'View Document'}
|
||||||
)}
|
{recipient.role === RecipientRole.SIGNER && 'Sign Document'}
|
||||||
>
|
{recipient.role === RecipientRole.APPROVER && 'Approve Document'}
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
{recipient.role === RecipientRole.VIEWER ? (
|
||||||
Please review the document before signing.
|
<>
|
||||||
</p>
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
Please mark as viewed to complete
|
||||||
|
</p>
|
||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4" />
|
||||||
<div>
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<Label htmlFor="full-name">Full Name</Label>
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Input
|
<SignDialog
|
||||||
type="text"
|
isSubmitting={isSubmitting}
|
||||||
id="full-name"
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
className="bg-background mt-2"
|
document={document}
|
||||||
value={fullName}
|
fields={fields}
|
||||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
fieldsValidated={fieldsValidated}
|
||||||
/>
|
role={recipient.role}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
Please review the document before signing.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div>
|
<hr className="border-border mb-8 mt-4" />
|
||||||
<Label htmlFor="Signature">Signature</Label>
|
|
||||||
|
|
||||||
<Card className="mt-2" gradient degrees={-120}>
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
<CardContent className="p-0">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<SignaturePad
|
<div>
|
||||||
className="h-44 w-full"
|
<Label htmlFor="full-name">Full Name</Label>
|
||||||
disabled={isSubmitting}
|
|
||||||
defaultValue={signature ?? undefined}
|
<Input
|
||||||
onChange={(value) => {
|
type="text"
|
||||||
setSignature(value);
|
id="full-name"
|
||||||
}}
|
className="bg-background mt-2"
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="Signature">Signature</Label>
|
||||||
|
|
||||||
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<SignaturePad
|
||||||
|
className="h-44 w-full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
defaultValue={signature ?? undefined}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSignature(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SignDialog
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
|
document={document}
|
||||||
|
fields={fields}
|
||||||
|
fieldsValidated={fieldsValidated}
|
||||||
|
role={recipient.role}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SignDialog
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
|
||||||
document={document}
|
|
||||||
fields={fields}
|
|
||||||
fieldsValidated={fieldsValidated}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
|||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
@ -110,7 +110,10 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
<div className="mt-2.5 flex items-center gap-x-6">
|
<div className="mt-2.5 flex items-center gap-x-6">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{document.User.name} ({document.User.email}) has invited you to sign this document.
|
{document.User.name} ({document.User.email}) has invited you to{' '}
|
||||||
|
{recipient.role === RecipientRole.VIEWER && 'view'}
|
||||||
|
{recipient.role === RecipientRole.SIGNER && 'sign'}
|
||||||
|
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { Document, Field } from '@documenso/prisma/client';
|
import type { Document, Field } from '@documenso/prisma/client';
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -17,6 +18,7 @@ export type SignDialogProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
fieldsValidated: () => void | Promise<void>;
|
fieldsValidated: () => void | Promise<void>;
|
||||||
onSignatureComplete: () => void | Promise<void>;
|
onSignatureComplete: () => void | Promise<void>;
|
||||||
|
role: RecipientRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDialog = ({
|
export const SignDialog = ({
|
||||||
@ -25,6 +27,7 @@ export const SignDialog = ({
|
|||||||
fields,
|
fields,
|
||||||
fieldsValidated,
|
fieldsValidated,
|
||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
|
role,
|
||||||
}: SignDialogProps) => {
|
}: SignDialogProps) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const truncatedTitle = truncateTitle(document.title);
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
@ -45,9 +48,18 @@ export const SignDialog = ({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-foreground text-xl font-semibold">Sign Document</div>
|
<div className="text-foreground text-xl font-semibold">
|
||||||
|
{role === RecipientRole.VIEWER && 'Mark Document as Viewed'}
|
||||||
|
{role === RecipientRole.SIGNER && 'Sign Document'}
|
||||||
|
{role === RecipientRole.APPROVER && 'Approve Document'}
|
||||||
|
</div>
|
||||||
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
||||||
You are about to finish signing "{truncatedTitle}". Are you sure?
|
{role === RecipientRole.VIEWER &&
|
||||||
|
`You are about to finish viewing "${truncatedTitle}". Are you sure?`}
|
||||||
|
{role === RecipientRole.SIGNER &&
|
||||||
|
`You are about to finish signing "${truncatedTitle}". Are you sure?`}
|
||||||
|
{role === RecipientRole.APPROVER &&
|
||||||
|
`You are about to finish approving "${truncatedTitle}". Are you sure?`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -71,7 +83,9 @@ export const SignDialog = ({
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
onClick={onSignatureComplete}
|
onClick={onSignatureComplete}
|
||||||
>
|
>
|
||||||
Sign
|
{role === RecipientRole.VIEWER && 'Mark as Viewed'}
|
||||||
|
{role === RecipientRole.SIGNER && 'Sign'}
|
||||||
|
{role === RecipientRole.APPROVER && 'Approve'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -47,8 +48,17 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
|||||||
type={getRecipientType(recipient)}
|
type={getRecipientType(recipient)}
|
||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
<span className="text-muted-foreground text-sm">{recipient.email}</span>
|
<div
|
||||||
|
className="text-muted-foreground text-sm"
|
||||||
|
title="Click to copy signing link for sending to recipient"
|
||||||
|
>
|
||||||
|
<p>{recipient.email} </p>
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import {
|
import {
|
||||||
@ -59,7 +60,12 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
type={getRecipientType(recipient)}
|
type={getRecipientType(recipient)}
|
||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground text-sm">{recipient.email}</span>
|
<div className="">
|
||||||
|
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
|
import type { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { Button, Section, Text } from '../components';
|
import { Button, Section, Text } from '../components';
|
||||||
import { TemplateDocumentImage } from './template-document-image';
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
@ -7,6 +10,7 @@ export interface TemplateDocumentInviteProps {
|
|||||||
documentName: string;
|
documentName: string;
|
||||||
signDocumentLink: string;
|
signDocumentLink: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
|
role: RecipientRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateDocumentInvite = ({
|
export const TemplateDocumentInvite = ({
|
||||||
@ -14,19 +18,22 @@ export const TemplateDocumentInvite = ({
|
|||||||
documentName,
|
documentName,
|
||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
|
role,
|
||||||
}: TemplateDocumentInviteProps) => {
|
}: TemplateDocumentInviteProps) => {
|
||||||
|
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
{inviterName} has invited you to sign
|
{inviterName} has invited you to {actionVerb.toLowerCase()}
|
||||||
<br />"{documentName}"
|
<br />"{documentName}"
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
Continue by signing the document.
|
Continue by {progressiveVerb.toLowerCase()} the document.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
<Section className="mb-6 mt-8 text-center">
|
||||||
@ -34,7 +41,7 @@ export const TemplateDocumentInvite = ({
|
|||||||
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||||
href={signDocumentLink}
|
href={signDocumentLink}
|
||||||
>
|
>
|
||||||
Sign Document
|
{actionVerb} Document
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
|
import type { RecipientRole } from '@documenso/prisma/client';
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -19,6 +21,7 @@ import { TemplateFooter } from '../template-components/template-footer';
|
|||||||
|
|
||||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
customBody?: string;
|
customBody?: string;
|
||||||
|
role: RecipientRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentInviteEmailTemplate = ({
|
export const DocumentInviteEmailTemplate = ({
|
||||||
@ -28,8 +31,11 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
signDocumentLink = 'https://documenso.com',
|
signDocumentLink = 'https://documenso.com',
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
customBody,
|
customBody,
|
||||||
|
role,
|
||||||
}: DocumentInviteEmailTemplateProps) => {
|
}: DocumentInviteEmailTemplateProps) => {
|
||||||
const previewText = `${inviterName} has invited you to sign ${documentName}`;
|
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
|
||||||
|
|
||||||
|
const previewText = `${inviterName} has invited you to ${action} ${documentName}`;
|
||||||
|
|
||||||
const getAssetUrl = (path: string) => {
|
const getAssetUrl = (path: string) => {
|
||||||
return new URL(path, assetBaseUrl).toString();
|
return new URL(path, assetBaseUrl).toString();
|
||||||
@ -64,6 +70,7 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
documentName={documentName}
|
documentName={documentName}
|
||||||
signDocumentLink={signDocumentLink}
|
signDocumentLink={signDocumentLink}
|
||||||
assetBaseUrl={assetBaseUrl}
|
assetBaseUrl={assetBaseUrl}
|
||||||
|
role={role}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
@ -81,7 +88,7 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
{customBody ? (
|
{customBody ? (
|
||||||
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
|
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
|
||||||
) : (
|
) : (
|
||||||
`${inviterName} has invited you to sign the document "${documentName}".`
|
`${inviterName} has invited you to ${action} the document "${documentName}".`
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const getRecipientType = (recipient: Recipient) => {
|
export const getRecipientType = (recipient: Recipient) => {
|
||||||
if (
|
if (
|
||||||
recipient.sendStatus === SendStatus.SENT &&
|
recipient.role === RecipientRole.CC ||
|
||||||
recipient.signingStatus === SigningStatus.SIGNED
|
(recipient.sendStatus === SendStatus.SENT && recipient.signingStatus === SigningStatus.SIGNED)
|
||||||
) {
|
) {
|
||||||
return 'completed';
|
return 'completed';
|
||||||
}
|
}
|
||||||
|
|||||||
26
packages/lib/constants/recipient-roles.ts
Normal file
26
packages/lib/constants/recipient-roles.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export const RECIPIENT_ROLES_DESCRIPTION: {
|
||||||
|
[key in RecipientRole]: { actionVerb: string; progressiveVerb: string; roleName: string };
|
||||||
|
} = {
|
||||||
|
[RecipientRole.APPROVER]: {
|
||||||
|
actionVerb: 'Approve',
|
||||||
|
progressiveVerb: 'Approving',
|
||||||
|
roleName: 'Approver',
|
||||||
|
},
|
||||||
|
[RecipientRole.CC]: {
|
||||||
|
actionVerb: 'CC',
|
||||||
|
progressiveVerb: 'CC',
|
||||||
|
roleName: 'CC',
|
||||||
|
},
|
||||||
|
[RecipientRole.SIGNER]: {
|
||||||
|
actionVerb: 'Sign',
|
||||||
|
progressiveVerb: 'Signing',
|
||||||
|
roleName: 'Signer',
|
||||||
|
},
|
||||||
|
[RecipientRole.VIEWER]: {
|
||||||
|
actionVerb: 'View',
|
||||||
|
progressiveVerb: 'Viewing',
|
||||||
|
roleName: 'Viewer',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -3,7 +3,7 @@ import { P, match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { Document, Prisma } from '@documenso/prisma/client';
|
import type { Document, Prisma } from '@documenso/prisma/client';
|
||||||
import { SigningStatus } from '@documenso/prisma/client';
|
import { RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
import type { FindResultSet } from '../../types/find-result-set';
|
import type { FindResultSet } from '../../types/find-result-set';
|
||||||
@ -87,6 +87,9 @@ export const findDocuments = async ({
|
|||||||
some: {
|
some: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
signingStatus: SigningStatus.NOT_SIGNED,
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
@ -109,6 +112,9 @@ export const findDocuments = async ({
|
|||||||
some: {
|
some: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document
|
|||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||||
|
|
||||||
export type ResendDocumentOptions = {
|
export type ResendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -59,6 +61,10 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
|
if (recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
@ -77,8 +83,11 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
|||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||||
|
role: recipient.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
@ -90,7 +99,7 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
|||||||
},
|
},
|
||||||
subject: customEmail?.subject
|
subject: customEmail?.subject
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
: 'Please sign this document',
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { PDFDocument } from 'pdf-lib';
|
|||||||
|
|
||||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { signPdf } from '@documenso/signing';
|
import { signPdf } from '@documenso/signing';
|
||||||
|
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
@ -44,6 +44,9 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
|||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document
|
|||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||||
|
|
||||||
export type SendDocumentOptions = {
|
export type SendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -47,6 +49,10 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
|
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
@ -55,10 +61,6 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
'document.name': document.title,
|
'document.name': document.title,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (recipient.sendStatus === SendStatus.SENT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||||
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
|
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
|
||||||
|
|
||||||
@ -69,8 +71,11 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||||
|
role: recipient.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
@ -82,7 +87,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
},
|
},
|
||||||
subject: customEmail?.subject
|
subject: customEmail?.subject
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
: 'Please sign this document',
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { nanoid } from '../../universal/id';
|
import { nanoid } from '../../universal/id';
|
||||||
@ -10,6 +11,7 @@ export interface SetRecipientsForDocumentOptions {
|
|||||||
id?: number | null;
|
id?: number | null;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
role: RecipientRole;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,13 +81,20 @@ export const setRecipientsForDocument = async ({
|
|||||||
update: {
|
update: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
role: recipient.role,
|
||||||
documentId,
|
documentId,
|
||||||
|
signingStatus:
|
||||||
|
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
role: recipient.role,
|
||||||
token: nanoid(),
|
token: nanoid(),
|
||||||
documentId,
|
documentId,
|
||||||
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||||
|
signingStatus:
|
||||||
|
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RecipientRole" AS ENUM ('CC', 'SIGNER', 'VIEWER', 'APPROVER');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Recipient" ADD COLUMN "role" "RecipientRole" NOT NULL DEFAULT 'SIGNER';
|
||||||
@ -209,6 +209,13 @@ enum SigningStatus {
|
|||||||
SIGNED
|
SIGNED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RecipientRole {
|
||||||
|
CC
|
||||||
|
SIGNER
|
||||||
|
VIEWER
|
||||||
|
APPROVER
|
||||||
|
}
|
||||||
|
|
||||||
model Recipient {
|
model Recipient {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
documentId Int?
|
documentId Int?
|
||||||
@ -218,6 +225,7 @@ model Recipient {
|
|||||||
token String
|
token String
|
||||||
expired DateTime?
|
expired DateTime?
|
||||||
signedAt DateTime?
|
signedAt DateTime?
|
||||||
|
role RecipientRole @default(SIGNER)
|
||||||
readStatus ReadStatus @default(NOT_OPENED)
|
readStatus ReadStatus @default(NOT_OPENED)
|
||||||
signingStatus SigningStatus @default(NOT_SIGNED)
|
signingStatus SigningStatus @default(NOT_SIGNED)
|
||||||
sendStatus SendStatus @default(NOT_SENT)
|
sendStatus SendStatus @default(NOT_SENT)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
id: z.number().min(1),
|
id: z.number().min(1),
|
||||||
@ -35,6 +35,7 @@ export const ZSetRecipientsForDocumentMutationSchema = z.object({
|
|||||||
id: z.number().nullish(),
|
id: z.number().nullish(),
|
||||||
email: z.string().min(1).email(),
|
email: z.string().min(1).email(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export const recipientRouter = router({
|
|||||||
id: signer.nativeId,
|
id: signer.nativeId,
|
||||||
email: signer.email,
|
email: signer.email,
|
||||||
name: signer.name,
|
name: signer.name,
|
||||||
|
role: signer.role,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZAddSignersMutationSchema = z
|
export const ZAddSignersMutationSchema = z
|
||||||
.object({
|
.object({
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
@ -8,6 +10,7 @@ export const ZAddSignersMutationSchema = z
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Caveat } from 'next/font/google';
|
import { Caveat } from 'next/font/google';
|
||||||
|
|
||||||
@ -10,8 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
|||||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { FieldType, SendStatus } from '@documenso/prisma/client';
|
import { FieldType, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
@ -102,6 +104,12 @@ export const AddFieldsFormPartial = ({
|
|||||||
|
|
||||||
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
||||||
|
|
||||||
|
const isFieldsDisabled =
|
||||||
|
!selectedSigner ||
|
||||||
|
hasSelectedSignerBeenSent ||
|
||||||
|
selectedSigner?.role === RecipientRole.VIEWER ||
|
||||||
|
selectedSigner?.role === RecipientRole.CC;
|
||||||
|
|
||||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -281,12 +289,28 @@ export const AddFieldsFormPartial = ({
|
|||||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
|
const recipientsByRole = useMemo(() => {
|
||||||
|
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||||
|
CC: [],
|
||||||
|
VIEWER: [],
|
||||||
|
SIGNER: [],
|
||||||
|
APPROVER: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
recipients.forEach((recipient) => {
|
||||||
|
recipientsByRole[recipient.role].push(recipient);
|
||||||
|
});
|
||||||
|
|
||||||
|
return recipientsByRole;
|
||||||
|
}, [recipients]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
title={documentFlow.title}
|
title={documentFlow.title}
|
||||||
description={documentFlow.description}
|
description={documentFlow.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{selectedField && (
|
{selectedField && (
|
||||||
@ -351,72 +375,94 @@ export const AddFieldsFormPartial = ({
|
|||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent className="p-0" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput />
|
<CommandInput />
|
||||||
|
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<span className="text-muted-foreground inline-block px-4">
|
<span className="text-muted-foreground inline-block px-4">
|
||||||
No recipient matching this description was found.
|
No recipient matching this description was found.
|
||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup>
|
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => (
|
||||||
{recipients.map((recipient, index) => (
|
<CommandGroup key={roleIndex}>
|
||||||
<CommandItem
|
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||||
key={index}
|
{
|
||||||
className={cn({
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
|
||||||
})}
|
}
|
||||||
onSelect={() => {
|
</div>
|
||||||
setSelectedSigner(recipient);
|
|
||||||
setShowRecipientsSelector(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
|
||||||
<Check
|
|
||||||
aria-hidden={recipient !== selectedSigner}
|
|
||||||
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
|
|
||||||
'opacity-0': recipient !== selectedSigner,
|
|
||||||
'opacity-100': recipient === selectedSigner,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info className="mr-2 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
This document has already been sent to this recipient. You can no
|
|
||||||
longer edit this recipient.
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{recipient.name && (
|
{recipients.length === 0 && (
|
||||||
|
<div
|
||||||
|
key={`${role}-empty`}
|
||||||
|
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||||
|
>
|
||||||
|
No recipients with this role
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recipients.map((recipient) => (
|
||||||
|
<CommandItem
|
||||||
|
key={recipient.id}
|
||||||
|
className={cn('!rounded-2xl px-4 last:mb-1 [&:not(:first-child)]:mt-1', {
|
||||||
|
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||||
|
})}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedSigner(recipient);
|
||||||
|
setShowRecipientsSelector(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className="truncate"
|
className={cn('text-foreground/70 truncate', {
|
||||||
title={`${recipient.name} (${recipient.email})`}
|
'text-foreground': recipient === selectedSigner,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name} ({recipient.email})
|
{recipient.name && (
|
||||||
</span>
|
<span title={`${recipient.name} (${recipient.email})`}>
|
||||||
)}
|
{recipient.name} ({recipient.email})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{!recipient.name && (
|
{!recipient.name && (
|
||||||
<span className="truncate" title={recipient.email}>
|
<span title={recipient.email}>{recipient.email}</span>
|
||||||
{recipient.email}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</CommandItem>
|
<div className="ml-auto flex items-center justify-center">
|
||||||
))}
|
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||||
</CommandGroup>
|
<Check
|
||||||
|
aria-hidden={recipient !== selectedSigner}
|
||||||
|
className={cn('h-4 w-4 flex-shrink-0', {
|
||||||
|
'opacity-0': recipient !== selectedSigner,
|
||||||
|
'opacity-100': recipient === selectedSigner,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||||
|
This document has already been sent to this recipient. You can no
|
||||||
|
longer edit this recipient.
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
))}
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
<fieldset disabled={isFieldsDisabled} className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group h-full w-full"
|
className="group h-full w-full"
|
||||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
|
||||||
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
||||||
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
||||||
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
||||||
@ -440,7 +486,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group h-full w-full"
|
className="group h-full w-full"
|
||||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
|
||||||
onClick={() => setSelectedField(FieldType.EMAIL)}
|
onClick={() => setSelectedField(FieldType.EMAIL)}
|
||||||
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
||||||
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
||||||
@ -463,7 +508,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group h-full w-full"
|
className="group h-full w-full"
|
||||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
|
||||||
onClick={() => setSelectedField(FieldType.NAME)}
|
onClick={() => setSelectedField(FieldType.NAME)}
|
||||||
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
||||||
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
||||||
@ -486,7 +530,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group h-full w-full"
|
className="group h-full w-full"
|
||||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
|
||||||
onClick={() => setSelectedField(FieldType.DATE)}
|
onClick={() => setSelectedField(FieldType.DATE)}
|
||||||
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
||||||
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
||||||
@ -505,7 +548,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|||||||
@ -4,19 +4,20 @@ import React, { useId } 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 { BadgeCheck, Copy, Eye, PencilLine, Plus, Trash } from 'lucide-react';
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
import { Controller, 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';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
|
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { Label } from '../label';
|
import { Label } from '../label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
import { useToast } from '../use-toast';
|
import { useToast } from '../use-toast';
|
||||||
import type { TAddSignersFormSchema } from './add-signers.types';
|
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||||
@ -31,6 +32,13 @@ import {
|
|||||||
import { ShowFieldItem } from './show-field-item';
|
import { ShowFieldItem } from './show-field-item';
|
||||||
import type { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
|
const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
||||||
|
SIGNER: <PencilLine className="h-4 w-4" />,
|
||||||
|
APPROVER: <BadgeCheck className="h-4 w-4" />,
|
||||||
|
CC: <Copy className="h-4 w-4" />,
|
||||||
|
VIEWER: <Eye className="h-4 w-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
@ -67,12 +75,14 @@ export const AddSignersFormPartial = ({
|
|||||||
formId: String(recipient.id),
|
formId: String(recipient.id),
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
role: recipient.role,
|
||||||
}))
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
formId: initialId,
|
formId: initialId,
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -104,6 +114,7 @@ export const AddSignersFormPartial = ({
|
|||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,6 +200,48 @@ export const AddSignersFormPartial = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-[60px]">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`signers.${index}.role`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Select value={value} onValueChange={(x) => onChange(x)}>
|
||||||
|
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent className="" align="end">
|
||||||
|
<SelectItem value={RecipientRole.SIGNER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||||
|
Signer
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.CC}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||||
|
Receives copy
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.APPROVER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||||
|
Approver
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.VIEWER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||||
|
Viewer
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { RecipientRole } from '.prisma/client';
|
||||||
|
|
||||||
export const ZAddSignersFormSchema = z
|
export const ZAddSignersFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
signers: z.array(
|
signers: z.array(
|
||||||
@ -8,6 +10,7 @@ export const ZAddSignersFormSchema = z
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -110,7 +110,6 @@ export const AddSubjectFormPartial = ({
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="subject"
|
id="subject"
|
||||||
// placeholder="Subject"
|
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('meta.subject')}
|
{...register('meta.subject')}
|
||||||
|
|||||||
Reference in New Issue
Block a user