diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx index 760b9cad2..a14a3074a 100644 --- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx +++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx @@ -196,6 +196,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp } avatarFallback={formatAvatarFallback(team.name)} primaryText={team.name} + textSectionClassName="w-[200px]" secondaryText={
{ const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role]; @@ -36,6 +40,12 @@ export const TemplateDocumentInvite = ({
{`"${documentName}"`} + ) : isTeamInvite ? ( + <> + {`${inviterName} on behalf of ${teamName} has invited you to ${actionVerb.toLowerCase()}`} +
+ {`"${documentName}"`} + ) : ( <> {`${inviterName} has invited you to ${actionVerb.toLowerCase()}`} diff --git a/packages/email/templates/confirm-team-email.tsx b/packages/email/templates/confirm-team-email.tsx index 5752f806d..552a079f8 100644 --- a/packages/email/templates/confirm-team-email.tsx +++ b/packages/email/templates/confirm-team-email.tsx @@ -91,6 +91,9 @@ export const ConfirmTeamEmailTemplate = ({
  • Allow document recipients to reply directly to this email address
  • +
  • + Send documents on behalf of the team using the email address +
  • diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 52a40d804..7e845126b 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -23,6 +23,9 @@ export type DocumentInviteEmailTemplateProps = Partial { const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase(); const previewText = selfSigner ? `Please ${action} your document ${documentName}` + : isTeamInvite + ? `${inviterName} on behalf of ${teamName} has invited you to ${action} ${documentName}` : `${inviterName} has invited you to ${action} ${documentName}`; const getAssetUrl = (path: string) => { @@ -76,6 +83,8 @@ export const DocumentInviteEmailTemplate = ({ assetBaseUrl={assetBaseUrl} role={role} selfSigner={selfSigner} + isTeamInvite={isTeamInvite} + teamName={teamName} /> diff --git a/packages/lib/jobs/definitions/emails/send-signing-email.ts b/packages/lib/jobs/definitions/emails/send-signing-email.ts index 0244df34f..43ad730c6 100644 --- a/packages/lib/jobs/definitions/emails/send-signing-email.ts +++ b/packages/lib/jobs/definitions/emails/send-signing-email.ts @@ -58,6 +58,12 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = { }, include: { documentMeta: true, + team: { + select: { + teamEmail: true, + name: true, + }, + }, }, }), prisma.recipient.findFirstOrThrow({ @@ -67,7 +73,7 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = { }), ]); - const { documentMeta } = document; + const { documentMeta, team } = document; if (recipient.role === RecipientRole.CC) { return; @@ -75,6 +81,7 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = { const customEmail = document?.documentMeta; const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK; + const isTeamDocument = document.teamId !== null; const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; @@ -96,6 +103,11 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = { emailSubject = `Please ${recipientActionVerb} this document created by your direct template`; } + if (isTeamDocument && team) { + emailSubject = `${team.name} invited you to ${recipientActionVerb} a document`; + emailMessage = `${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`; + } + const customEmailTemplate = { 'signer.name': name, 'signer.email': email, @@ -108,12 +120,15 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = { const template = createElement(DocumentInviteEmailTemplate, { documentName: document.title, inviterName: user.name || undefined, - inviterEmail: user.email, + inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email, assetBaseUrl, signDocumentLink, customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate), role: recipient.role, selfSigner, + isTeamInvite: isTeamDocument, + teamName: team?.name, + teamEmail: team?.teamEmail?.email, }); await io.runTask('send-signing-email', async () => { diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index c8a51cac4..8ea39445c 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -58,10 +58,17 @@ export const resendDocument = async ({ }, }, documentMeta: true, + team: { + select: { + teamEmail: true, + name: true, + }, + }, }, }); const customEmail = document?.documentMeta; + const isTeamDocument = document?.team !== null; if (!document) { throw new Error('Document not found'); @@ -90,9 +97,21 @@ export const resendDocument = async ({ const { email, name } = recipient; const selfSigner = email === user.email; - const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[ - recipient.role - ].actionVerb.toLowerCase()} it.`; + const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; + const recipientActionVerb = actionVerb.toLowerCase(); + + let emailMessage = customEmail?.message || ''; + let emailSubject = `Reminder: Please ${recipientActionVerb} this document`; + + if (selfSigner) { + emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`; + emailSubject = `Reminder: Please ${recipientActionVerb} your document`; + } + + if (isTeamDocument && document.team) { + emailSubject = `Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`; + emailMessage = `${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`; + } const customEmailTemplate = { 'signer.name': name, @@ -106,23 +125,16 @@ export const resendDocument = async ({ const template = createElement(DocumentInviteEmailTemplate, { documentName: document.title, inviterName: user.name || undefined, - inviterEmail: user.email, + inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email, assetBaseUrl, signDocumentLink, - customBody: renderCustomEmailTemplate( - selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '', - customEmailTemplate, - ), + customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate), role: recipient.role, selfSigner, + isTeamInvite: isTeamDocument, + teamName: document.team?.name, }); - const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; - - const emailSubject = selfSigner - ? `Reminder: Please ${actionVerb.toLowerCase()} your document` - : `Reminder: Please ${actionVerb.toLowerCase()} this document`; - await prisma.$transaction( async (tx) => { await mailer.sendMail({ diff --git a/packages/prisma/seed/initial-seed.ts b/packages/prisma/seed/initial-seed.ts index 66c944c9b..38b340a79 100644 --- a/packages/prisma/seed/initial-seed.ts +++ b/packages/prisma/seed/initial-seed.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { hashSync } from '@documenso/lib/server-only/auth/hash'; import { prisma } from '..'; -import { DocumentDataType, DocumentSource, Role } from '../client'; +import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client'; export const seedDatabase = async () => { const examplePdf = fs @@ -67,4 +67,64 @@ export const seedDatabase = async () => { }, }, }); + + const testUsers = [ + 'test@documenso.com', + 'test2@documenso.com', + 'test3@documenso.com', + 'test4@documenso.com', + ]; + + const createdUsers = []; + + for (const email of testUsers) { + const testUser = await prisma.user.upsert({ + where: { + email: email, + }, + create: { + name: 'Test User', + email: email, + emailVerified: new Date(), + password: hashSync('password'), + roles: [Role.USER], + }, + update: {}, + }); + + createdUsers.push(testUser); + } + + const team1 = await prisma.team.create({ + data: { + name: 'Team 1', + url: 'team1', + ownerUserId: createdUsers[0].id, + }, + }); + + const team2 = await prisma.team.create({ + data: { + name: 'Team 2', + url: 'team2', + ownerUserId: createdUsers[1].id, + }, + }); + + for (const team of [team1, team2]) { + await prisma.teamMember.createMany({ + data: [ + { + teamId: team.id, + userId: createdUsers[1].id, + role: TeamMemberRole.ADMIN, + }, + { + teamId: team.id, + userId: createdUsers[2].id, + role: TeamMemberRole.MEMBER, + }, + ], + }); + } };