mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
feat: add email domains (#1895)
Implemented Email Domains which allows Platform/Enterprise customers to send emails to recipients using their custom emails.
This commit is contained in:
@ -15,8 +15,10 @@ type ComboboxProps = {
|
||||
options: string[];
|
||||
value: string | null;
|
||||
onChange: (_value: string | null) => void;
|
||||
triggerPlaceholder?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
const Combobox = ({
|
||||
@ -25,7 +27,9 @@ const Combobox = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
triggerPlaceholder,
|
||||
placeholder,
|
||||
testId,
|
||||
}: ComboboxProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
@ -47,8 +51,9 @@ const Combobox = ({
|
||||
aria-expanded={open}
|
||||
className={cn('my-2 w-full justify-between', className)}
|
||||
disabled={disabled}
|
||||
data-testid={testId}
|
||||
>
|
||||
{value ? value : placeholderValue}
|
||||
{value ? value : triggerPlaceholder || placeholderValue}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@ -5,13 +5,31 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { CopyTextButton } from '../../components/common/copy-text-button';
|
||||
@ -21,11 +39,10 @@ import {
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { AvatarWithText } from '../avatar';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
import { useStep } from '../stepper';
|
||||
import { Textarea } from '../textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||
import { toast } from '../use-toast';
|
||||
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
||||
import {
|
||||
@ -56,15 +73,14 @@ export const AddSubjectFormPartial = ({
|
||||
}: AddSubjectFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TAddSubjectFormSchema>({
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const form = useForm<TAddSubjectFormSchema>({
|
||||
defaultValues: {
|
||||
meta: {
|
||||
emailId: document.documentMeta?.emailId ?? null,
|
||||
emailReplyTo: document.documentMeta?.emailReplyTo || undefined,
|
||||
// emailReplyName: document.documentMeta?.emailReplyName || undefined,
|
||||
subject: document.documentMeta?.subject ?? '',
|
||||
message: document.documentMeta?.message ?? '',
|
||||
distributionMethod:
|
||||
@ -75,6 +91,21 @@ export const AddSubjectFormPartial = ({
|
||||
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = form;
|
||||
|
||||
const { data: emailData, isLoading: isLoadingEmails } =
|
||||
trpc.enterprise.organisation.email.find.useQuery({
|
||||
organisationId: organisation.id,
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
const emails = emailData?.data || [];
|
||||
|
||||
const GoNextLabel = {
|
||||
[DocumentDistributionMethod.EMAIL]: {
|
||||
[DocumentStatus.DRAFT]: msg`Send`,
|
||||
@ -139,54 +170,141 @@ export const AddSubjectFormPartial = ({
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||
className="flex flex-col gap-y-4 rounded-lg border p-4"
|
||||
>
|
||||
<div>
|
||||
<Label htmlFor="subject">
|
||||
<Trans>
|
||||
Subject <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</Label>
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
className="flex flex-col gap-y-4 rounded-lg border p-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Email Sender</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Input
|
||||
id="subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.subject')}
|
||||
/>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === '-1' ? null : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger loading={isLoadingEmails} className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<FormErrorMessage className="mt-2" error={errors.meta?.subject} />
|
||||
</div>
|
||||
<SelectContent>
|
||||
{emails.map((email) => (
|
||||
<SelectItem key={email.id} value={email.id}>
|
||||
{email.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="message">
|
||||
<Trans>
|
||||
Message <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</Label>
|
||||
<SelectItem value={'-1'}>Documenso</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Textarea
|
||||
id="message"
|
||||
className="bg-background mt-2 h-32 resize-none"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.message')}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormErrorMessage
|
||||
className="mt-2"
|
||||
error={
|
||||
typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailReplyTo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Email</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<DocumentSendEmailMessageHelper />
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
className="mt-2"
|
||||
value={emailSettings}
|
||||
onChange={(value) => setValue('meta.emailSettings', value)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="meta.emailReplyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Name</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Subject</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground p-4">
|
||||
<DocumentSendEmailMessageHelper />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea className="bg-background mt-2 h-16 resize-none" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
className="mt-2"
|
||||
value={emailSettings}
|
||||
onChange={(value) => setValue('meta.emailSettings', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</Form>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
|
||||
@ -5,6 +5,9 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
|
||||
|
||||
export const ZAddSubjectFormSchema = z.object({
|
||||
meta: z.object({
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.string().email().optional(),
|
||||
// emailReplyName: z.string().optional(),
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
distributionMethod: z
|
||||
|
||||
@ -27,7 +27,7 @@ type FieldIconProps = {
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
};
|
||||
documentMeta?: DocumentMeta | TemplateMeta;
|
||||
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { Check, ChevronDown, Loader } from 'lucide-react';
|
||||
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
@ -13,20 +16,33 @@ const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
loading?: boolean;
|
||||
}
|
||||
>(({ className, children, loading, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
disabled={loading || props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
<AnimatePresence>
|
||||
{loading ? (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Loader className="h-5 w-5 animate-spin text-gray-500 dark:text-gray-100" />
|
||||
</div>
|
||||
) : (
|
||||
<AnimateGenericFadeInOut className="flex w-full justify-between">
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</AnimateGenericFadeInOut>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
|
||||
import {
|
||||
DocumentGlobalAuthAccessSelect,
|
||||
@ -120,6 +121,8 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
template.templateMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
|
||||
redirectUrl: template.templateMeta?.redirectUrl ?? '',
|
||||
language: template.templateMeta?.language ?? 'en',
|
||||
emailId: template.templateMeta?.emailId ?? null,
|
||||
emailReplyTo: template.templateMeta?.emailReplyTo ?? undefined,
|
||||
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
|
||||
signatureTypes: extractTeamSignatureSettings(template?.templateMeta),
|
||||
},
|
||||
@ -131,6 +134,14 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
const distributionMethod = form.watch('meta.distributionMethod');
|
||||
const emailSettings = form.watch('meta.emailSettings');
|
||||
|
||||
const { data: emailData, isLoading: isLoadingEmails } =
|
||||
trpc.enterprise.organisation.email.find.useQuery({
|
||||
organisationId: organisation.id,
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
const emails = emailData?.data || [];
|
||||
|
||||
const canUpdateVisibility = match(currentTeamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(
|
||||
@ -403,6 +414,68 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed [&>div]:pb-0">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Email Sender</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === '-1' ? null : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
loading={isLoadingEmails}
|
||||
className="bg-background"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{emails.map((email) => (
|
||||
<SelectItem key={email.id} value={email.id}>
|
||||
{email.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
<SelectItem value={'-1'}>Documenso</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailReplyTo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Email</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.subject"
|
||||
@ -428,14 +501,21 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
name="meta.message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>
|
||||
Message <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground p-4">
|
||||
<DocumentSendEmailMessageHelper />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea className="bg-background h-32 resize-none" {...field} />
|
||||
<Textarea className="bg-background h-16 resize-none" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -443,8 +523,6 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<DocumentSendEmailMessageHelper />
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
value={emailSettings}
|
||||
onChange={(value) => form.setValue('meta.emailSettings', value)}
|
||||
|
||||
@ -48,6 +48,8 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
|
||||
.optional()
|
||||
.default('en'),
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.string().optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema,
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
|
||||
Reference in New Issue
Block a user