mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Emails, the signing page's title, the sealed PDF's reason, the sign-in refusal and the dialogs still said Hanzo Sign. The catalogs change with the strings, message for message, so every translation keeps its id.
453 lines
17 KiB
TypeScript
453 lines
17 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
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 { useAutoSave } from '@hanzo/esign-lib/client-only/hooks/use-autosave';
|
|
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@hanzo/esign-lib/constants/recipient-roles';
|
|
import type { TDocument } from '@hanzo/esign-lib/types/document';
|
|
import { ZDocumentEmailSettingsSchema } from '@hanzo/esign-lib/types/document-email';
|
|
import { formatSigningLink } from '@hanzo/esign-lib/utils/recipients';
|
|
import type { TFindOrganisationEmailsResponse } from '@hanzo/esign-trpc/server/organisation-router/find-organisation-emails.types';
|
|
import { useZapQuery } from '@hanzo/esign-trpc/zap/react';
|
|
import { DocumentSendEmailMessageHelper } from '@hanzo/esign-ui/components/document/document-send-email-message-helper';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@hanzo/esign-ui/primitives/form/form';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@hanzo/esign-ui/primitives/select';
|
|
import { Tabs, TabsList, TabsTrigger } from '@hanzo/esign-ui/primitives/tabs';
|
|
|
|
import { CopyTextButton } from '../../components/common/copy-text-button';
|
|
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
|
import {
|
|
DocumentReadOnlyFields,
|
|
mapFieldsWithRecipients,
|
|
} from '../../components/document/document-read-only-fields';
|
|
import { AvatarWithText } from '../avatar';
|
|
import { Input } from '../input';
|
|
import { useStep } from '../stepper';
|
|
import { Textarea } from '../textarea';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
|
import { toast } from '../use-toast';
|
|
import {
|
|
type TAddSubjectFormInput,
|
|
type TAddSubjectFormSchema,
|
|
ZAddSubjectFormSchema,
|
|
} from './add-subject.types';
|
|
import {
|
|
DocumentFlowFormContainerActions,
|
|
DocumentFlowFormContainerContent,
|
|
DocumentFlowFormContainerFooter,
|
|
DocumentFlowFormContainerHeader,
|
|
DocumentFlowFormContainerStep,
|
|
} from './document-flow-root';
|
|
import type { DocumentFlowStep } from './types';
|
|
|
|
export type AddSubjectFormProps = {
|
|
documentFlow: DocumentFlowStep;
|
|
recipients: Recipient[];
|
|
fields: Field[];
|
|
document: TDocument;
|
|
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
|
onAutoSave: (_data: TAddSubjectFormInput) => Promise<void>;
|
|
isDocumentPdfLoaded: boolean;
|
|
};
|
|
|
|
export const AddSubjectFormPartial = ({
|
|
documentFlow,
|
|
recipients: recipients,
|
|
fields: fields,
|
|
document,
|
|
onSubmit,
|
|
onAutoSave,
|
|
isDocumentPdfLoaded,
|
|
}: AddSubjectFormProps) => {
|
|
const { _ } = useLingui();
|
|
|
|
const organisation = useCurrentOrganisation();
|
|
|
|
const form = useForm({
|
|
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:
|
|
document.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
|
|
emailSettings: ZDocumentEmailSettingsSchema.parse(document?.documentMeta?.emailSettings),
|
|
},
|
|
},
|
|
resolver: zodResolver(ZAddSubjectFormSchema),
|
|
});
|
|
|
|
const {
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
trigger,
|
|
getValues,
|
|
formState: { isSubmitting },
|
|
} = form;
|
|
|
|
const { data: emailData, isLoading: isLoadingEmails } =
|
|
useZapQuery<TFindOrganisationEmailsResponse>('organisation.email.find', {
|
|
organisationId: organisation.id,
|
|
perPage: 100,
|
|
});
|
|
|
|
const emails = emailData?.data || [];
|
|
|
|
const GoNextLabel = {
|
|
[DocumentDistributionMethod.EMAIL]: {
|
|
[DocumentStatus.DRAFT]: msg`Send`,
|
|
[DocumentStatus.PENDING]: recipients.some((recipient) => recipient.sendStatus === 'SENT')
|
|
? msg`Resend`
|
|
: msg`Send`,
|
|
[DocumentStatus.COMPLETED]: msg`Update`,
|
|
[DocumentStatus.REJECTED]: msg`Update`,
|
|
},
|
|
[DocumentDistributionMethod.NONE]: {
|
|
[DocumentStatus.DRAFT]: msg`Generate Links`,
|
|
[DocumentStatus.PENDING]: msg`View Document`,
|
|
[DocumentStatus.COMPLETED]: msg`View Document`,
|
|
[DocumentStatus.REJECTED]: msg`View Document`,
|
|
},
|
|
};
|
|
|
|
// Unset means email, as the schema's default says.
|
|
const distributionMethod = watch('meta.distributionMethod') ?? DocumentDistributionMethod.EMAIL;
|
|
const emailSettings = watch('meta.emailSettings');
|
|
|
|
const onFormSubmit = handleSubmit(onSubmit);
|
|
const { currentStep, totalSteps, previousStep } = useStep();
|
|
|
|
const { scheduleSave } = useAutoSave(onAutoSave);
|
|
|
|
const handleAutoSave = async () => {
|
|
const isFormValid = await trigger();
|
|
|
|
if (!isFormValid) {
|
|
return;
|
|
}
|
|
|
|
const formData = getValues();
|
|
|
|
scheduleSave(formData);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const container = window.document.getElementById('document-flow-form-container');
|
|
|
|
const handleBlur = () => {
|
|
void handleAutoSave();
|
|
};
|
|
|
|
if (container) {
|
|
container.addEventListener('blur', handleBlur, true);
|
|
return () => {
|
|
container.removeEventListener('blur', handleBlur, true);
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<DocumentFlowFormContainerHeader
|
|
title={documentFlow.title}
|
|
description={documentFlow.description}
|
|
/>
|
|
<DocumentFlowFormContainerContent>
|
|
<div className="flex flex-col">
|
|
{isDocumentPdfLoaded && (
|
|
<DocumentReadOnlyFields
|
|
showRecipientColors={true}
|
|
recipientIds={recipients.map((recipient) => recipient.id)}
|
|
fields={mapFieldsWithRecipients(fields, recipients)}
|
|
/>
|
|
)}
|
|
|
|
<Tabs
|
|
onValueChange={(value) =>
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
setValue('meta.distributionMethod', value as DocumentDistributionMethod)
|
|
}
|
|
value={distributionMethod}
|
|
className="mb-2"
|
|
>
|
|
<TabsList className="w-full">
|
|
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
|
|
<Trans>Email</Trans>
|
|
</TabsTrigger>
|
|
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
|
|
<Trans>None</Trans>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
|
|
<AnimatePresence mode="wait">
|
|
{distributionMethod === DocumentDistributionMethod.EMAIL && (
|
|
<motion.div
|
|
key={'Emails'}
|
|
initial={{ opacity: 0, y: 15 }}
|
|
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
|
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
|
>
|
|
<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>
|
|
<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'}>Hanzo eSign</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="meta.emailReplyTo"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>
|
|
Reply To Email{' '}
|
|
<span className="text-muted-foreground">(Optional)</span>
|
|
</Trans>
|
|
</FormLabel>
|
|
|
|
<FormControl>
|
|
<Input {...field} maxLength={254} />
|
|
</FormControl>
|
|
|
|
<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 <span className="text-muted-foreground">(Optional)</span>
|
|
</Trans>
|
|
</FormLabel>
|
|
|
|
<FormControl>
|
|
<Input {...field} maxLength={255} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="meta.message"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="flex flex-row items-center">
|
|
<Trans>
|
|
Message <span className="text-muted-foreground">(Optional)</span>
|
|
</Trans>
|
|
<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}
|
|
maxLength={5000}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<DocumentEmailCheckboxes
|
|
className="mt-2"
|
|
value={emailSettings}
|
|
onChange={(value) => setValue('meta.emailSettings', value)}
|
|
/>
|
|
</fieldset>
|
|
</Form>
|
|
</motion.div>
|
|
)}
|
|
|
|
{distributionMethod === DocumentDistributionMethod.NONE && (
|
|
<motion.div
|
|
key={'Links'}
|
|
initial={{ opacity: 0, y: 15 }}
|
|
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
|
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
|
className="rounded-lg border"
|
|
>
|
|
{document.status === DocumentStatus.DRAFT ? (
|
|
<div className="text-muted-foreground py-16 text-center text-sm">
|
|
<p>
|
|
<Trans>We won't send anything to notify recipients.</Trans>
|
|
</p>
|
|
|
|
<p className="mt-2">
|
|
<Trans>
|
|
We will generate signing links for you, which you can send to the recipients
|
|
through your method of choice.
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<ul className="text-muted-foreground divide-y">
|
|
{recipients.length === 0 && (
|
|
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
|
<Trans>No recipients</Trans>
|
|
</li>
|
|
)}
|
|
|
|
{recipients.map((recipient) => (
|
|
<li
|
|
key={recipient.id}
|
|
className="flex items-center justify-between px-4 py-3 text-sm"
|
|
>
|
|
<AvatarWithText
|
|
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
|
|
primaryText={
|
|
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
|
}
|
|
secondaryText={
|
|
<p className="text-muted-foreground/70 text-xs">
|
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
|
</p>
|
|
}
|
|
/>
|
|
|
|
{recipient.role !== RecipientRole.CC && (
|
|
<CopyTextButton
|
|
value={formatSigningLink(recipient.token)}
|
|
onCopySuccess={() => {
|
|
toast({
|
|
title: _(msg`Copied to clipboard`),
|
|
description: _(
|
|
msg`The signing link has been copied to your clipboard.`,
|
|
),
|
|
});
|
|
}}
|
|
badgeContentUncopied={
|
|
<p className="ml-1 text-xs">
|
|
<Trans>Copy</Trans>
|
|
</p>
|
|
}
|
|
badgeContentCopied={
|
|
<p className="ml-1 text-xs">
|
|
<Trans>Copied</Trans>
|
|
</p>
|
|
}
|
|
/>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</DocumentFlowFormContainerContent>
|
|
|
|
<DocumentFlowFormContainerFooter>
|
|
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
|
|
|
|
<DocumentFlowFormContainerActions
|
|
loading={isSubmitting}
|
|
disabled={isSubmitting}
|
|
goNextLabel={GoNextLabel[distributionMethod][document.status]}
|
|
onGoBackClick={previousStep}
|
|
onGoNextClick={() => void onFormSubmit()}
|
|
/>
|
|
</DocumentFlowFormContainerFooter>
|
|
</>
|
|
);
|
|
};
|