fix: reauth, etc

This commit is contained in:
David Nguyen
2025-11-03 16:16:50 +11:00
parent d2a009d52e
commit bbf44acda3
12 changed files with 201 additions and 90 deletions

View File

@ -152,6 +152,18 @@ export const EditorFieldTextForm = ({
className="h-auto"
placeholder={t`Add text to the field`}
{...field}
onChange={(e) => {
const values = form.getValues();
const characterLimit = values.characterLimit || 0;
let textValue = e.target.value;
if (characterLimit > 0 && textValue.length > characterLimit) {
textValue = textValue.slice(0, characterLimit);
}
e.target.value = textValue;
field.onChange(e);
}}
rows={1}
/>
</FormControl>
@ -175,6 +187,18 @@ export const EditorFieldTextForm = ({
className="bg-background"
placeholder={t`Field character limit`}
{...field}
onChange={(e) => {
field.onChange(e);
const values = form.getValues();
const characterLimit = parseInt(e.target.value, 10) || 0;
const textValue = values.text || '';
if (characterLimit > 0 && textValue.length > characterLimit) {
form.setValue('text', textValue.slice(0, characterLimit));
}
}}
/>
</FormControl>
<FormMessage />

View File

@ -13,6 +13,7 @@ import { prop, sortBy } from 'remeda';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import {
isFieldUnsignedAndRequired,
isRequiredField,
@ -51,7 +52,11 @@ export type EnvelopeSigningContextValue = {
setSelectedAssistantRecipientId: (_value: number | null) => void;
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise<void>;
signField: (
_fieldId: number,
_value: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => Promise<void>;
};
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
@ -284,7 +289,11 @@ export const EnvelopeSigningProvider = ({
: null;
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => {
const signField = async (
fieldId: number,
fieldValue: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
// Set the field locally for direct templates.
if (isDirectTemplate) {
handleDirectTemplateFieldInsertion(fieldId, fieldValue);
@ -295,7 +304,7 @@ export const EnvelopeSigningProvider = ({
token: envelopeData.recipient.token,
fieldId,
fieldValue,
authOptions: undefined,
authOptions,
});
};

View File

@ -103,7 +103,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
fieldUpdates.height = fieldPageHeight;
}
// Todo: envelopes Use id
editorFields.updateFieldByFormId(fieldFormId, fieldUpdates);
// Select the field if it is not already selected.

View File

@ -27,7 +27,8 @@ import type {
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Separator } from '@documenso/ui/primitives/separator';
@ -112,7 +113,29 @@ export const EnvelopeEditorFieldsPage = () => {
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */}
<div className="mt-4 flex h-full justify-center p-4">
<div className="mt-4 flex flex-col items-center justify-center">
{envelope.recipients.length === 0 && (
<Alert
variant="neutral"
className="border-border bg-background mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border"
>
<div className="flex flex-col gap-1">
<AlertTitle>
<Trans>Missing Recipients</Trans>
</AlertTitle>
<AlertDescription>
<Trans>You need at least one recipient to add fields</Trans>
</AlertDescription>
</div>
<Button asChild variant="outline">
<Link to={`${relativePath.editorPath}`}>
<Trans>Add Recipients</Trans>
</Link>
</Button>
</Alert>
)}
{currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
) : (
@ -130,7 +153,7 @@ export const EnvelopeEditorFieldsPage = () => {
</div>
{/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && (
{currentEnvelopeItem && envelope.recipients.length > 0 && (
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
{/* Recipient selector section. */}
<section className="px-4">
@ -138,29 +161,15 @@ export const EnvelopeEditorFieldsPage = () => {
<Trans>Selected Recipient</Trans>
</h3>
{envelope.recipients.length === 0 ? (
<Alert variant="warning">
<AlertDescription className="flex flex-col gap-2">
<Trans>You need at least one recipient to add fields</Trans>
<Link to={`${relativePath.editorPath}`} className="text-sm">
<p>
<Trans>Click here to add a recipient</Trans>
</p>
</Link>
</AlertDescription>
</Alert>
) : (
<RecipientSelector
selectedRecipient={editorFields.selectedRecipient}
onSelectedRecipientChange={(recipient) =>
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
className="w-full"
align="end"
/>
)}
<RecipientSelector
selectedRecipient={editorFields.selectedRecipient}
onSelectedRecipientChange={(recipient) =>
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
className="w-full"
align="end"
/>
{editorFields.selectedRecipient &&
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (

View File

@ -323,7 +323,7 @@ export const EnvelopeEditorSettingsDialog = ({
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
{/* Sidebar. */}
<div className="flex w-80 flex-col border-r bg-gray-50">
<div className="bg-accent/20 flex w-80 flex-col border-r">
<DialogHeader className="p-6 pb-4">
<DialogTitle>Document Settings</DialogTitle>
</DialogHeader>

View File

@ -203,7 +203,6 @@ export const EnvelopeEditorUploadPage = () => {
debouncedUpdateEnvelopeItems(items);
};
// Todo: Envelopes - Sync into envelopes data
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
void updateEnvelopeItems({
envelopeId: envelope.id,

View File

@ -10,14 +10,17 @@ import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-rende
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFullFieldSchema } from '@documenso/lib/types/field';
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
@ -28,20 +31,24 @@ import { handleNumberFieldClick } from '~/utils/field-signing/number-field';
import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field';
import { handleTextFieldClick } from '~/utils/field-signing/text-field';
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
export default function EnvelopeSignerPageRenderer() {
const { i18n } = useLingui();
const { t, i18n } = useLingui();
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const { sessionData } = useOptionalSession();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { toast } = useToast();
const {
envelopeData,
recipient,
recipientFields,
recipientFieldsRemaining,
showPendingFieldTooltip,
signField,
signField: signFieldInternal,
email,
setEmail,
fullName,
@ -318,7 +325,6 @@ export default function EnvelopeSignerPageRenderer() {
* SIGNATURE FIELD.
*/
.with({ type: FieldType.SIGNATURE }, (field) => {
// Todo: Envelopes - Reauth
handleSignatureFieldClick({
field,
signature,
@ -329,11 +335,21 @@ export default function EnvelopeSignerPageRenderer() {
.then(async (payload) => {
if (payload) {
fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload);
}
if (payload?.value) {
setSignature(payload.value);
if (payload.value) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => {
await signField(field.id, payload, authOptions);
loadingSpinnerGroup.destroy();
},
actionTarget: field.type,
});
setSignature(payload.value);
} else {
await signField(field.id, payload);
}
}
})
.finally(() => {
@ -347,6 +363,26 @@ export default function EnvelopeSignerPageRenderer() {
fieldGroup.on('pointerdown', handleFieldGroupClick);
};
const signField = async (
fieldId: number,
payload: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
try {
await signFieldInternal(fieldId, payload, authOptions);
} catch (err) {
console.error(err);
toast({
title: t`Error`,
description: t`An error occurred while signing the field.`,
variant: 'destructive',
});
throw err;
}
};
/**
* Initialize the Konva page canvas and all fields and interactions.
*/

View File

@ -78,7 +78,6 @@ test.describe('Signing Certificate Tests', () => {
},
});
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData);
@ -169,7 +168,6 @@ test.describe('Signing Certificate Tests', () => {
},
});
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData);

View File

@ -62,16 +62,15 @@ export const renderCheckboxFieldElement = (
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
// Todo: Envelopes - check sorting more than 10
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
const squares = fieldGroup
.find('.checkbox-square')
.sort((a, b) => a.id().localeCompare(b.id()));
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
const checkmarks = fieldGroup
.find('.checkbox-checkmark')
.sort((a, b) => a.id().localeCompare(b.id()));
const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
const text = fieldGroup
.find('.checkbox-text')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
const groupedItems = squares.map((square, i) => ({
squareElement: square,

View File

@ -8,9 +8,9 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import type { TFieldMetaSchema } from '../../types/field-meta';
import { renderCheckboxFieldElement } from './render-checkbox-field';
import { renderDropdownFieldElement } from './render-dropdown-field';
import { renderGenericTextFieldElement } from './render-generic-text-field';
import { renderRadioFieldElement } from './render-radio-field';
import { renderSignatureFieldElement } from './render-signature-field';
import { renderTextFieldElement } from './render-text-field';
export const MIN_FIELD_HEIGHT_PX = 12;
export const MIN_FIELD_WIDTH_PX = 36;
@ -43,9 +43,9 @@ type RenderFieldOptions = {
*
* @default 'edit'
*
* - `edit` - The field is rendered in edit mode.
* - `sign` - The field is rendered in sign mode. No interactive elements.
* - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
* - `edit` - The field is rendered in editor page.
* - `sign` - The field is rendered for the signing page.
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
*/
mode: 'edit' | 'sign' | 'export';
@ -76,10 +76,21 @@ export const renderField = ({
};
return match(field.type)
.with(FieldType.TEXT, () => renderTextFieldElement(field, options))
.with(
FieldType.INITIALS,
FieldType.NAME,
FieldType.EMAIL,
FieldType.DATE,
FieldType.TEXT,
FieldType.NUMBER,
() => renderGenericTextFieldElement(field, options),
)
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
.with(FieldType.FREE_SIGNATURE, () => {
throw new Error('Free signature fields are not supported');
})
.exhaustive();
};

View File

@ -12,6 +12,8 @@ import {
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
import { calculateFieldPosition } from './field-renderer';
const DEFAULT_TEXT_ALIGN = 'left';
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
@ -31,8 +33,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Calculate text positioning based on alignment
const textX = 0;
const textY = 0;
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
const textPadding = 10;
@ -40,51 +42,33 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Handle edit mode.
if (mode === 'edit') {
textToRender = fieldTypeName;
textAlign = 'center';
textVerticalAlign = 'middle';
if (textMeta?.label) {
textToRender = textMeta.label;
} else if (textMeta?.text) {
if (textMeta?.text) {
textToRender = textMeta.text;
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
} else if (textMeta?.label) {
textToRender = textMeta.label;
} else {
// Show field name which is centered for the edit mode if no label/text is avaliable.
textToRender = fieldTypeName;
textAlign = 'center';
}
}
// Handle sign mode.
if (mode === 'sign' || mode === 'export') {
textToRender = fieldTypeName;
textAlign = 'center';
textVerticalAlign = 'middle';
if (textMeta?.label) {
textToRender = textMeta.label;
}
if (textMeta?.text) {
textToRender = textMeta.text;
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
if (!field.inserted) {
if (textMeta?.text) {
textToRender = textMeta.text;
} else if (textMeta?.label) {
textToRender = textMeta.label;
} else if (mode === 'sign') {
// Only show the field name in sign mode if no text/label is avaliable.
textToRender = fieldTypeName;
textAlign = 'center';
}
}
if (field.inserted) {
textToRender = field.customText;
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta?.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
}
}
@ -106,7 +90,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
return fieldText;
};
export const renderTextFieldElement = (
export const renderGenericTextFieldElement = (
field: FieldToRender,
options: RenderFieldElementOptions,
) => {

View File

@ -133,6 +133,49 @@ export const signEnvelopeFieldRoute = procedure
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
// Early return for uninserting fields.
if (!insertionValues.inserted) {
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText: '',
inserted: false,
},
});
await tx.signature.deleteMany({
where: {
fieldId: field.id,
},
});
if (recipient.role !== RecipientRole.ASSISTANT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
envelopeId: envelope.id,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata: metadata.requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
}
return {
signedField: updatedField,
};
});
}
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: envelope.authOptions,
recipient,