mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +10:00
@ -1,22 +1,25 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type Field, FieldType } from '@prisma/client';
|
||||
import Konva from 'konva';
|
||||
import type { Layer } from 'konva/lib/Layer';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { type Field, FieldType, RecipientRole, type Signature } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
import { usePageContext } from 'react-pdf';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
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 { 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 { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
|
||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
|
||||
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
|
||||
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
|
||||
import { handleEmailFieldClick } from '~/utils/field-signing/email-field';
|
||||
import { handleInitialsFieldClick } from '~/utils/field-signing/initial-field';
|
||||
@ -28,24 +31,13 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||
|
||||
export default function EnvelopeSignerPageRenderer() {
|
||||
const pageContext = usePageContext();
|
||||
|
||||
if (!pageContext) {
|
||||
throw new Error('Unable to find Page context.');
|
||||
}
|
||||
|
||||
const { _className, page, rotate, scale } = pageContext;
|
||||
|
||||
if (!page) {
|
||||
throw new Error('Attempted to render page canvas, but no page was specified.');
|
||||
}
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { i18n } = useLingui();
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
const { sessionData } = useOptionalSession();
|
||||
|
||||
const {
|
||||
envelopeData,
|
||||
recipient,
|
||||
recipientFields,
|
||||
recipientFieldsRemaining,
|
||||
showPendingFieldTooltip,
|
||||
@ -56,71 +48,39 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
setFullName,
|
||||
signature,
|
||||
setSignature,
|
||||
selectedAssistantRecipientFields,
|
||||
selectedAssistantRecipient,
|
||||
isDirectTemplate,
|
||||
} = useRequiredEnvelopeSigningContext();
|
||||
|
||||
console.log({ fullName });
|
||||
const {
|
||||
stage,
|
||||
pageLayer,
|
||||
canvasElement,
|
||||
konvaContainer,
|
||||
pageContext,
|
||||
scaledViewport,
|
||||
unscaledViewport,
|
||||
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
|
||||
|
||||
const { _className, scale } = pageContext;
|
||||
|
||||
const { envelope } = envelopeData;
|
||||
|
||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
||||
const localPageFields = useMemo(() => {
|
||||
let fieldsToRender = recipientFields;
|
||||
|
||||
const stage = useRef<Konva.Stage | null>(null);
|
||||
const pageLayer = useRef<Layer | null>(null);
|
||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||
fieldsToRender = selectedAssistantRecipientFields;
|
||||
}
|
||||
|
||||
const viewport = useMemo(
|
||||
() => page.getViewport({ scale, rotation: rotate }),
|
||||
[page, rotate, scale],
|
||||
);
|
||||
return fieldsToRender.filter(
|
||||
(field) =>
|
||||
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
||||
);
|
||||
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
|
||||
|
||||
const localPageFields = useMemo(
|
||||
() =>
|
||||
recipientFields.filter(
|
||||
(field) =>
|
||||
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
||||
),
|
||||
[recipientFields, pageContext.pageNumber],
|
||||
);
|
||||
|
||||
// Custom renderer from Konva examples.
|
||||
useEffect(
|
||||
function drawPageOnCanvas() {
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { current: canvas } = canvasElement;
|
||||
const { current: container } = konvaContainer;
|
||||
|
||||
if (!canvas || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderContext: RenderParameters = {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
||||
viewport,
|
||||
};
|
||||
|
||||
const cancellable = page.render(renderContext);
|
||||
const runningTask = cancellable;
|
||||
|
||||
cancellable.promise.catch(() => {
|
||||
// Intentionally empty
|
||||
});
|
||||
|
||||
void cancellable.promise.then(() => {
|
||||
createPageCanvas(container);
|
||||
});
|
||||
|
||||
return () => {
|
||||
runningTask.cancel();
|
||||
};
|
||||
},
|
||||
[page, viewport],
|
||||
);
|
||||
|
||||
const renderFieldOnLayer = (unparsedField: Field) => {
|
||||
const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
return;
|
||||
@ -137,6 +97,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
}
|
||||
|
||||
const { fieldGroup } = renderField({
|
||||
scale,
|
||||
pageLayer: pageLayer.current,
|
||||
field: {
|
||||
renderId: fieldToRender.id.toString(),
|
||||
@ -145,9 +106,11 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
height: Number(fieldToRender.height),
|
||||
positionX: Number(fieldToRender.positionX),
|
||||
positionY: Number(fieldToRender.positionY),
|
||||
signature: unparsedField.signature,
|
||||
},
|
||||
pageWidth: viewport.width,
|
||||
pageHeight: viewport.height,
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
pageWidth: unscaledViewport.width,
|
||||
pageHeight: unscaledViewport.height,
|
||||
color,
|
||||
mode: 'sign',
|
||||
});
|
||||
@ -158,19 +121,35 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
|
||||
const { width: fieldWidth, height: fieldHeight } = fieldGroup.getClientRect();
|
||||
|
||||
const foundField = recipientFields.find((f) => f.id === unparsedField.id);
|
||||
const foundField = localPageFields.find((f) => f.id === unparsedField.id);
|
||||
const foundLoadingGroup = currentTarget.findOne('.loading-spinner-group');
|
||||
|
||||
if (!foundField || foundLoadingGroup || foundField.fieldMeta?.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingSpinnerGroup = createSpinner({
|
||||
fieldWidth,
|
||||
fieldHeight,
|
||||
});
|
||||
let localEmail: string | null = email;
|
||||
let localFullName: string | null = fullName;
|
||||
let placeholderEmail: string | null = null;
|
||||
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||
localEmail = selectedAssistantRecipient?.email || null;
|
||||
localFullName = selectedAssistantRecipient?.name || null;
|
||||
}
|
||||
|
||||
// Allows us let the user set a different email than their current logged in email.
|
||||
if (isDirectTemplate) {
|
||||
placeholderEmail = sessionData?.user?.email || email || recipient.email;
|
||||
|
||||
if (!placeholderEmail || placeholderEmail === DIRECT_TEMPLATE_RECIPIENT_EMAIL) {
|
||||
placeholderEmail = null;
|
||||
}
|
||||
}
|
||||
|
||||
const loadingSpinnerGroup = createSpinner({
|
||||
fieldWidth: fieldWidth / scale,
|
||||
fieldHeight: fieldHeight / scale,
|
||||
});
|
||||
|
||||
const parsedFoundField = ZFullFieldSchema.parse(foundField);
|
||||
|
||||
@ -179,34 +158,39 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* CHECKBOX FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.CHECKBOX }, (field) => {
|
||||
const { fieldMeta } = field;
|
||||
const clickedCheckboxIndex = Number(target.getAttr('internalCheckboxIndex'));
|
||||
|
||||
const { values } = fieldMeta;
|
||||
if (Number.isNaN(clickedCheckboxIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkedValues = (values || [])
|
||||
.map((v) => ({
|
||||
...v,
|
||||
checked: v.id === target.getAttr('internalCheckboxId') ? !v.checked : v.checked,
|
||||
}))
|
||||
.filter((v) => v.checked);
|
||||
|
||||
void signField(field.id, {
|
||||
type: FieldType.CHECKBOX,
|
||||
value: checkedValues.map((v) => v.id),
|
||||
}).finally(() => {
|
||||
loadingSpinnerGroup.destroy();
|
||||
});
|
||||
handleCheckboxFieldClick({ field, clickedCheckboxIndex })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingSpinnerGroup.destroy();
|
||||
});
|
||||
})
|
||||
/**
|
||||
* RADIO FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.RADIO }, (field) => {
|
||||
const { fieldMeta } = foundField;
|
||||
const selectedRadioIndex = Number(target.getAttr('internalRadioIndex'));
|
||||
const fieldCustomText = Number(field.customText);
|
||||
|
||||
const checkedValue = target.getAttr('internalRadioValue');
|
||||
if (Number.isNaN(selectedRadioIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
|
||||
// Uncheck the value if it's already pressed.
|
||||
const value = field.inserted && checkedValue === field.customText ? null : checkedValue;
|
||||
const value =
|
||||
field.inserted && selectedRadioIndex === fieldCustomText ? null : selectedRadioIndex;
|
||||
|
||||
void signField(field.id, {
|
||||
type: FieldType.RADIO,
|
||||
@ -222,6 +206,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
handleNumberFieldClick({ field, number: null })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
})
|
||||
@ -236,6 +221,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
handleTextFieldClick({ field, text: null })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
})
|
||||
@ -247,9 +233,10 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* EMAIL FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.EMAIL }, (field) => {
|
||||
handleEmailFieldClick({ field, email })
|
||||
handleEmailFieldClick({ field, email: localEmail, placeholderEmail })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload); // Todo: Envelopes - Handle errors
|
||||
}
|
||||
|
||||
@ -265,11 +252,12 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* INITIALS FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.INITIALS }, (field) => {
|
||||
const initials = fullName ? extractInitials(fullName) : null;
|
||||
const initials = localFullName ? extractInitials(localFullName) : null;
|
||||
|
||||
handleInitialsFieldClick({ field, initials })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
})
|
||||
@ -281,9 +269,10 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* NAME FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.NAME }, (field) => {
|
||||
handleNameFieldClick({ field, name: fullName })
|
||||
handleNameFieldClick({ field, name: localFullName })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
|
||||
@ -302,6 +291,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
handleDropdownFieldClick({ field, text: null })
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
|
||||
@ -315,6 +305,8 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* DATE FIELD.
|
||||
*/
|
||||
.with({ type: FieldType.DATE }, (field) => {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
|
||||
void signField(field.id, {
|
||||
type: FieldType.DATE,
|
||||
value: !field.inserted,
|
||||
@ -336,6 +328,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
})
|
||||
.then(async (payload) => {
|
||||
if (payload) {
|
||||
fieldGroup.add(loadingSpinnerGroup);
|
||||
await signField(field.id, payload);
|
||||
}
|
||||
|
||||
@ -348,38 +341,22 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
});
|
||||
})
|
||||
.exhaustive();
|
||||
|
||||
console.log('Field clicked');
|
||||
};
|
||||
|
||||
fieldGroup.off('click');
|
||||
fieldGroup.on('click', handleFieldGroupClick);
|
||||
fieldGroup.off('pointerdown');
|
||||
fieldGroup.on('pointerdown', handleFieldGroupClick);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the initial Konva page canvas and initialize all fields and interactions.
|
||||
* Initialize the Konva page canvas and all fields and interactions.
|
||||
*/
|
||||
const createPageCanvas = (container: HTMLDivElement) => {
|
||||
stage.current = new Konva.Stage({
|
||||
container,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
|
||||
// Create the main layer for interactive elements.
|
||||
pageLayer.current = new Konva.Layer();
|
||||
stage.current?.add(pageLayer.current);
|
||||
|
||||
console.log({
|
||||
localPageFields,
|
||||
});
|
||||
|
||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
|
||||
}
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
currentPageLayer.batchDraw();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -392,25 +369,61 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
|
||||
localPageFields.forEach((field) => {
|
||||
console.log('Field changed/inserted, rendering on canvas');
|
||||
renderFieldOnLayer(field);
|
||||
renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
|
||||
});
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
||||
|
||||
/**
|
||||
* Rerender the whole page if the selected assistant recipient changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!pageLayer.current || !stage.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rerender the whole page.
|
||||
pageLayer.current.destroyChildren();
|
||||
|
||||
localPageFields.forEach((field) => {
|
||||
renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
|
||||
});
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [selectedAssistantRecipient]);
|
||||
|
||||
if (!currentEnvelopeItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
||||
<div
|
||||
className="relative w-full"
|
||||
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||
>
|
||||
{showPendingFieldTooltip &&
|
||||
recipientFieldsRemaining.length > 0 &&
|
||||
recipientFieldsRemaining[0]?.envelopeItemId === currentEnvelopeItem?.id &&
|
||||
recipientFieldsRemaining[0]?.page === pageContext.pageNumber && (
|
||||
<EnvelopeFieldToolTip
|
||||
key={recipientFieldsRemaining[0].id}
|
||||
field={recipientFieldsRemaining[0]}
|
||||
color="warning"
|
||||
>
|
||||
<Trans>Click to insert field</Trans>
|
||||
</EnvelopeFieldToolTip>
|
||||
)}
|
||||
|
||||
{/* The element Konva will inject it's canvas into. */}
|
||||
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||
|
||||
{/* Canvas the PDF will be rendered on. */}
|
||||
<canvas
|
||||
className={`${_className}__canvas z-0`}
|
||||
height={viewport.height}
|
||||
ref={canvasElement}
|
||||
width={viewport.width}
|
||||
height={scaledViewport.height}
|
||||
width={scaledViewport.width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user