Merge branch 'main' into reattach-pdf
This commit is contained in:
@@ -151,7 +151,7 @@ export const EditDocumentForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
const { subject, message, timezone, dateFormat } = data.meta;
|
const { subject, message, timezone, dateFormat, redirectUrl } = data.meta;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendDocument({
|
await sendDocument({
|
||||||
@@ -159,8 +159,9 @@ export const EditDocumentForm = ({
|
|||||||
meta: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
timezone,
|
|
||||||
dateFormat,
|
dateFormat,
|
||||||
|
timezone,
|
||||||
|
redirectUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
{recipient?.role !== RecipientRole.CC && (
|
{recipient && recipient?.role !== RecipientRole.CC && (
|
||||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
{recipient?.role === RecipientRole.VIEWER && (
|
{recipient?.role === RecipientRole.VIEWER && (
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ export type SigningFormProps = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
|
redirectUrl?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -74,7 +75,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`/sign/${recipient.token}/complete`);
|
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
|
|||||||
<span className="text-muted-foreground">({recipient.email})</span>
|
<span className="text-muted-foreground">({recipient.email})</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<div className="py-4">
|
<div>
|
||||||
<Label htmlFor="signature">Full Name</Label>
|
<Label htmlFor="signature">Full Name</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id';
|
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-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';
|
||||||
@@ -49,15 +48,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
viewedDocument({ token }).catch(() => null),
|
viewedDocument({ token }).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null);
|
|
||||||
|
|
||||||
if (!document || !document.documentData || !recipient) {
|
if (!document || !document.documentData || !recipient) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncatedTitle = truncateTitle(document.title);
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
const { user } = await getServerComponentSession();
|
const { user } = await getServerComponentSession();
|
||||||
|
|
||||||
@@ -65,7 +62,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
document.status === DocumentStatus.COMPLETED ||
|
document.status === DocumentStatus.COMPLETED ||
|
||||||
recipient.signingStatus === SigningStatus.SIGNED
|
recipient.signingStatus === SigningStatus.SIGNED
|
||||||
) {
|
) {
|
||||||
redirect(`/sign/${token}/complete`);
|
documentMeta?.redirectUrl
|
||||||
|
? redirect(documentMeta.redirectUrl)
|
||||||
|
: redirect(`/sign/${token}/complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (documentMeta?.password) {
|
if (documentMeta?.password) {
|
||||||
@@ -133,7 +132,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
||||||
<SigningForm document={document} recipient={recipient} fields={fields} />
|
<SigningForm
|
||||||
|
document={document}
|
||||||
|
recipient={recipient}
|
||||||
|
fields={fields}
|
||||||
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -14503,6 +14503,7 @@
|
|||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
|
||||||
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
|
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@@ -19495,14 +19496,14 @@
|
|||||||
"@react-email/section": "0.0.10",
|
"@react-email/section": "0.0.10",
|
||||||
"@react-email/tailwind": "0.0.9",
|
"@react-email/tailwind": "0.0.9",
|
||||||
"@react-email/text": "0.0.6",
|
"@react-email/text": "0.0.6",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.9",
|
||||||
"react-email": "^1.9.5",
|
"react-email": "^1.9.5",
|
||||||
"resend": "^2.0.0"
|
"resend": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"tsup": "^7.1.0"
|
"tsup": "^7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -19520,6 +19521,14 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/email/node_modules/nodemailer": {
|
||||||
|
"version": "6.9.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz",
|
||||||
|
"integrity": "sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/eslint-config": {
|
"packages/eslint-config": {
|
||||||
"name": "@documenso/eslint-config",
|
"name": "@documenso/eslint-config",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
@@ -35,14 +35,14 @@
|
|||||||
"@react-email/section": "0.0.10",
|
"@react-email/section": "0.0.10",
|
||||||
"@react-email/tailwind": "0.0.9",
|
"@react-email/tailwind": "0.0.9",
|
||||||
"@react-email/text": "0.0.6",
|
"@react-email/text": "0.0.6",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.9",
|
||||||
"react-email": "^1.9.5",
|
"react-email": "^1.9.5",
|
||||||
"resend": "^2.0.0"
|
"resend": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"tsup": "^7.1.0"
|
"tsup": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
packages/lib/constants/url-regex.ts
Normal file
2
packages/lib/constants/url-regex.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const URL_REGEX =
|
||||||
|
/^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
|
||||||
@@ -9,6 +9,7 @@ export type CreateDocumentMetaOptions = {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
documentId,
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
password,
|
password,
|
||||||
|
redirectUrl,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
await prisma.document.findFirstOrThrow({
|
await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -48,17 +50,19 @@ export const upsertDocumentMeta = async ({
|
|||||||
create: {
|
create: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
password,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timezone,
|
timezone,
|
||||||
password,
|
|
||||||
documentId,
|
documentId,
|
||||||
|
redirectUrl,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
dateFormat,
|
|
||||||
password,
|
password,
|
||||||
|
dateFormat,
|
||||||
timezone,
|
timezone,
|
||||||
|
redirectUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const duplicateDocumentById = async ({
|
|||||||
dateFormat: true,
|
dateFormat: true,
|
||||||
password: true,
|
password: true,
|
||||||
timezone: true,
|
timezone: true,
|
||||||
|
redirectUrl: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
include: {
|
include: {
|
||||||
User: true,
|
User: true,
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ export const setFieldsForDocument = async ({
|
|||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.completedAt) {
|
||||||
|
throw new Error('Document already complete');
|
||||||
|
}
|
||||||
|
|
||||||
const existingFields = await prisma.field.findMany({
|
const existingFields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export const setRecipientsForDocument = async ({
|
|||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.completedAt) {
|
||||||
|
throw new Error('Document already complete');
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRecipients = recipients.map((recipient) => ({
|
const normalizedRecipients = recipients.map((recipient) => ({
|
||||||
...recipient,
|
...recipient,
|
||||||
email: recipient.email.toLowerCase(),
|
email: recipient.email.toLowerCase(),
|
||||||
@@ -77,8 +81,9 @@ export const setRecipientsForDocument = async ({
|
|||||||
})
|
})
|
||||||
.filter((recipient) => {
|
.filter((recipient) => {
|
||||||
return (
|
return (
|
||||||
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
recipient._persisted?.role === RecipientRole.CC ||
|
||||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
(recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||||
|
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@ export const setRecipientsForDocument = async ({
|
|||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
documentId,
|
documentId,
|
||||||
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||||
signingStatus:
|
signingStatus:
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta" ADD COLUMN "redirectUrl" TEXT;
|
||||||
@@ -199,6 +199,7 @@ model DocumentMeta {
|
|||||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||||
documentId Int @unique
|
documentId Int @unique
|
||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
|
redirectUrl String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReadStatus {
|
enum ReadStatus {
|
||||||
|
|||||||
@@ -215,13 +215,14 @@ export const documentRouter = router({
|
|||||||
try {
|
try {
|
||||||
const { documentId, meta } = input;
|
const { documentId, meta } = input;
|
||||||
|
|
||||||
if (meta.message || meta.subject || meta.timezone || meta.dateFormat) {
|
if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId,
|
documentId,
|
||||||
subject: meta.subject,
|
subject: meta.subject,
|
||||||
message: meta.message,
|
message: meta.message,
|
||||||
dateFormat: meta.dateFormat,
|
dateFormat: meta.dateFormat,
|
||||||
timezone: meta.timezone,
|
timezone: meta.timezone,
|
||||||
|
redirectUrl: meta.redirectUrl,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
@@ -73,6 +74,12 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
dateFormat: z.string(),
|
dateFormat: z.string(),
|
||||||
|
redirectUrl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((value) => value === undefined || URL_REGEX.test(value), {
|
||||||
|
message: 'Please enter a valid URL',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,13 @@ export const AddFieldsFormPartial = ({
|
|||||||
return recipientsByRole;
|
return recipientsByRole;
|
||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
|
const recipientsByRoleToDisplay = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
||||||
|
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
|
||||||
|
);
|
||||||
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@@ -385,13 +392,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => (
|
{recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
|
||||||
<CommandGroup key={roleIndex}>
|
<CommandGroup key={roleIndex}>
|
||||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||||
{
|
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipients.length === 0 && (
|
{recipients.length === 0 && (
|
||||||
@@ -406,7 +410,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
{recipients.map((recipient) => (
|
{recipients.map((recipient) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
className={cn('px-4 last:mb-1 [&:not(:first-child)]:mt-1', {
|
className={cn('px-2 last:mb-1 [&:not(:first-child)]:mt-1', {
|
||||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||||
})}
|
})}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -416,7 +420,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('text-foreground/70 truncate', {
|
||||||
'text-foreground': recipient === selectedSigner,
|
'text-foreground/80': recipient === selectedSigner,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name && (
|
{recipient.name && (
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ export const AddSignersFormPartial = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return recipients.some(
|
return recipients.some(
|
||||||
(recipient) => recipient.id === id && recipient.sendStatus === SendStatus.SENT,
|
(recipient) =>
|
||||||
|
recipient.id === id &&
|
||||||
|
recipient.sendStatus === SendStatus.SENT &&
|
||||||
|
recipient.role !== RecipientRole.CC,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { Combobox } from '../combobox';
|
import { Combobox } from '../combobox';
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
@@ -30,7 +33,7 @@ import { Input } from '../input';
|
|||||||
import { Label } from '../label';
|
import { Label } from '../label';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
import { Textarea } from '../textarea';
|
import { Textarea } from '../textarea';
|
||||||
import type { TAddSubjectFormSchema } from './add-subject.types';
|
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@@ -69,8 +72,10 @@ export const AddSubjectFormPartial = ({
|
|||||||
message: document.documentMeta?.message ?? '',
|
message: document.documentMeta?.message ?? '',
|
||||||
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
redirectUrl: document.documentMeta?.redirectUrl ?? '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
@@ -163,64 +168,94 @@ export const AddSubjectFormPartial = ({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasDateField && (
|
<Accordion type="multiple" className="mt-8 border-none">
|
||||||
<Accordion type="multiple" className="mt-8 border-none">
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
<AccordionItem value="advanced-options" className="border-none">
|
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
|
||||||
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
|
Advanced Options
|
||||||
Advanced Options
|
</AccordionTrigger>
|
||||||
</AccordionTrigger>
|
|
||||||
|
|
||||||
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 text-sm leading-relaxed">
|
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 pt-2 text-sm leading-relaxed">
|
||||||
<div className="mt-2 flex flex-col">
|
{hasDateField && (
|
||||||
<Label htmlFor="date-format">
|
<>
|
||||||
Date Format <span className="text-muted-foreground">(Optional)</span>
|
<div className="flex flex-col">
|
||||||
</Label>
|
<Label htmlFor="date-format">
|
||||||
|
Date Format <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`meta.dateFormat`}
|
name={`meta.dateFormat`}
|
||||||
disabled={documentHasBeenSent}
|
disabled={documentHasBeenSent}
|
||||||
render={({ field: { value, onChange, disabled } }) => (
|
render={({ field: { value, onChange, disabled } }) => (
|
||||||
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
||||||
<SelectTrigger className="bg-background mt-2">
|
<SelectTrigger className="bg-background mt-2">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{DATE_FORMATS.map((format) => (
|
{DATE_FORMATS.map((format) => (
|
||||||
<SelectItem key={format.key} value={format.value}>
|
<SelectItem key={format.key} value={format.value}>
|
||||||
{format.label}
|
{format.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col">
|
||||||
|
<Label htmlFor="time-zone">
|
||||||
|
Time Zone <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`meta.timezone`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Combobox
|
||||||
|
className="bg-background"
|
||||||
|
options={TIME_ZONES}
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => value && onChange(value)}
|
||||||
|
disabled={documentHasBeenSent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="redirectUrl" className="flex items-center">
|
||||||
|
Redirect URL{' '}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||||
|
Add a URL to redirect the user to once the document is signed
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="redirectUrl"
|
||||||
|
type="url"
|
||||||
|
className="bg-background my-2"
|
||||||
|
{...register('meta.redirectUrl')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormErrorMessage className="mt-2" error={errors.meta?.redirectUrl} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-4 flex flex-col">
|
</AccordionContent>
|
||||||
<Label htmlFor="time-zone">
|
</AccordionItem>
|
||||||
Time Zone <span className="text-muted-foreground">(Optional)</span>
|
</Accordion>
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`meta.timezone`}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Combobox
|
|
||||||
className="bg-background"
|
|
||||||
options={TIME_ZONES}
|
|
||||||
value={value}
|
|
||||||
onChange={(value) => value && onChange(value)}
|
|
||||||
disabled={documentHasBeenSent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
|
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||||
|
|
||||||
export const ZAddSubjectFormSchema = z.object({
|
export const ZAddSubjectFormSchema = z.object({
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
@@ -9,6 +10,12 @@ export const ZAddSubjectFormSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
|
redirectUrl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((value) => value === undefined || URL_REGEX.test(value), {
|
||||||
|
message: 'Please enter a valid URL',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user