Introduces the ability for users with the **Assistant** role to prefill fields on behalf of other signers. Assistants can fill in various field types such as text, checkboxes, dates, and more, streamlining the document preparation process before it reaches the final signers.
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Loader } from 'lucide-react';
|
|
import { useRevalidator } from 'react-router';
|
|
|
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
|
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import type {
|
|
TRemovedSignedFieldWithTokenMutationSchema,
|
|
TSignFieldWithTokenMutationSchema,
|
|
} from '@documenso/trpc/server/field-router/schema';
|
|
import { Label } from '@documenso/ui/primitives/label';
|
|
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
|
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
|
|
|
export type DocumentSigningRadioFieldProps = {
|
|
field: FieldWithSignatureAndFieldMeta;
|
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
|
};
|
|
|
|
export const DocumentSigningRadioField = ({
|
|
field,
|
|
onSignField,
|
|
onUnsignField,
|
|
}: DocumentSigningRadioFieldProps) => {
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
const { revalidate } = useRevalidator();
|
|
|
|
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
|
|
|
|
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
|
const values = parsedFieldMeta.values?.map((item) => ({
|
|
...item,
|
|
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
|
}));
|
|
const checkedItem = values?.find((item) => item.checked);
|
|
const defaultValue = !field.inserted && !!checkedItem ? checkedItem.value : '';
|
|
|
|
const [selectedOption, setSelectedOption] = useState(defaultValue);
|
|
|
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
|
|
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
|
|
|
const {
|
|
mutateAsync: removeSignedFieldWithToken,
|
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
|
|
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
|
const shouldAutoSignField =
|
|
(!field.inserted && selectedOption) ||
|
|
(!field.inserted && defaultValue) ||
|
|
(!field.inserted && parsedFieldMeta.readOnly && defaultValue);
|
|
|
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
|
try {
|
|
if (isAssistantMode && !targetSigner) {
|
|
return;
|
|
}
|
|
|
|
if (!selectedOption) {
|
|
return;
|
|
}
|
|
|
|
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
|
|
|
const payload: TSignFieldWithTokenMutationSchema = {
|
|
token: signingRecipient.token,
|
|
fieldId: field.id,
|
|
value: selectedOption,
|
|
isBase64: true,
|
|
authOptions,
|
|
...(isAssistantMode && {
|
|
isAssistantPrefill: true,
|
|
assistantId: recipient.id,
|
|
}),
|
|
};
|
|
|
|
if (onSignField) {
|
|
await onSignField(payload);
|
|
} else {
|
|
await signFieldWithToken(payload);
|
|
}
|
|
|
|
setSelectedOption('');
|
|
|
|
await revalidate();
|
|
} catch (err) {
|
|
const error = AppError.parseError(err);
|
|
|
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
|
throw error;
|
|
}
|
|
|
|
console.error(err);
|
|
|
|
toast({
|
|
title: _(msg`Error`),
|
|
description: isAssistantMode
|
|
? _(msg`An error occurred while signing as assistant.`)
|
|
: _(msg`An error occurred while signing the document.`),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
const onRemove = async () => {
|
|
try {
|
|
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
|
token: recipient.token,
|
|
fieldId: field.id,
|
|
};
|
|
|
|
if (onUnsignField) {
|
|
await onUnsignField(payload);
|
|
} else {
|
|
await removeSignedFieldWithToken(payload);
|
|
}
|
|
|
|
setSelectedOption('');
|
|
|
|
await revalidate();
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
toast({
|
|
title: _(msg`Error`),
|
|
description: _(msg`An error occurred while removing the selection.`),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSelectItem = (selectedOption: string) => {
|
|
setSelectedOption(selectedOption);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (shouldAutoSignField) {
|
|
void executeActionAuthProcedure({
|
|
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
|
actionTarget: field.type,
|
|
});
|
|
}
|
|
}, [selectedOption, field]);
|
|
|
|
return (
|
|
<DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Radio">
|
|
{isLoading && (
|
|
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
|
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
|
</div>
|
|
)}
|
|
|
|
{!field.inserted && (
|
|
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10">
|
|
{values?.map((item, index) => (
|
|
<div key={index} className="flex items-center gap-x-1.5">
|
|
<RadioGroupItem
|
|
className="h-4 w-4 shrink-0"
|
|
value={item.value}
|
|
id={`option-${index}`}
|
|
checked={item.checked}
|
|
/>
|
|
|
|
<Label htmlFor={`option-${index}`}>
|
|
{item.value.includes('empty-value-') ? '' : item.value}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</RadioGroup>
|
|
)}
|
|
|
|
{field.inserted && (
|
|
<RadioGroup className="gap-y-1">
|
|
{values?.map((item, index) => (
|
|
<div key={index} className="flex items-center gap-x-1.5">
|
|
<RadioGroupItem
|
|
className="h-3 w-3"
|
|
value={item.value}
|
|
id={`option-${index}`}
|
|
checked={item.value === field.customText}
|
|
/>
|
|
<Label htmlFor={`option-${index}`} className="text-xs">
|
|
{item.value.includes('empty-value-') ? '' : item.value}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</RadioGroup>
|
|
)}
|
|
</DocumentSigningFieldContainer>
|
|
);
|
|
};
|