mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 10:11:35 +10:00
@ -96,7 +96,7 @@ export const EnvelopeEditorFieldDragDrop = ({
|
||||
selectedRecipientId,
|
||||
selectedEnvelopeItemId,
|
||||
}: EnvelopeEditorFieldDragDropProps) => {
|
||||
const { envelope, editorFields, isTemplate } = useCurrentEnvelopeEditor();
|
||||
const { envelope, editorFields, isTemplate, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@ -262,6 +262,10 @@ export const EnvelopeEditorFieldDragDrop = ({
|
||||
};
|
||||
}, [onMouseClick, onMouseMove, selectedField]);
|
||||
|
||||
const selectedRecipientColor = useMemo(() => {
|
||||
return selectedRecipientId ? getRecipientColorKey(selectedRecipientId) : 'green';
|
||||
}, [selectedRecipientId, getRecipientColorKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-2.5">
|
||||
@ -273,12 +277,23 @@ export const EnvelopeEditorFieldDragDrop = ({
|
||||
onClick={() => setSelectedField(field.type)}
|
||||
onMouseDown={() => setSelectedField(field.type)}
|
||||
data-selected={selectedField === field.type ? true : undefined}
|
||||
className="group flex h-12 cursor-pointer items-center justify-center rounded-lg border border-gray-200 px-4 transition-colors hover:border-blue-300 hover:bg-blue-50"
|
||||
className={cn(
|
||||
'border-border group flex h-12 cursor-pointer items-center justify-center rounded-lg border px-4 transition-colors',
|
||||
RECIPIENT_COLOR_STYLES[selectedRecipientColor].fieldButton,
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'text-muted-foreground font-noto group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
field.className,
|
||||
{
|
||||
'group-hover:text-recipient-green': selectedRecipientColor === 'green',
|
||||
'group-hover:text-recipient-blue': selectedRecipientColor === 'blue',
|
||||
'group-hover:text-recipient-purple': selectedRecipientColor === 'purple',
|
||||
'group-hover:text-recipient-orange': selectedRecipientColor === 'orange',
|
||||
'group-hover:text-recipient-yellow': selectedRecipientColor === 'yellow',
|
||||
'group-hover:text-recipient-pink': selectedRecipientColor === 'pink',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{field.type !== FieldType.SIGNATURE && <field.icon className="h-4 w-4" />}
|
||||
@ -291,9 +306,9 @@ export const EnvelopeEditorFieldDragDrop = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
// selectedSignerStyles?.base,
|
||||
RECIPIENT_COLOR_STYLES.yellow.base, // Todo: Envelopes
|
||||
'text-muted-foreground dark:text-muted-background font-noto pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
RECIPIENT_COLOR_STYLES[selectedRecipientColor].base,
|
||||
selectedField === FieldType.SIGNATURE && 'font-signature',
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
|
||||
@ -3,15 +3,12 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import Konva from 'konva';
|
||||
import type { Layer } from 'konva/lib/Layer';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Transformer } from 'konva/lib/shapes/Transformer';
|
||||
import { CopyPlusIcon, SquareStackIcon, TrashIcon } from 'lucide-react';
|
||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
import { usePageContext } from 'react-pdf';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||
@ -21,32 +18,16 @@ import {
|
||||
convertPixelToPercentage,
|
||||
} from '@documenso/lib/universal/field-renderer/field-renderer';
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
||||
|
||||
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
|
||||
|
||||
export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
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 { t, i18n } = useLingui();
|
||||
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const stage = useRef<Konva.Stage | null>(null);
|
||||
const pageLayer = useRef<Layer | null>(null);
|
||||
const interactiveTransformer = useRef<Transformer | null>(null);
|
||||
|
||||
const [selectedKonvaFieldGroups, setSelectedKonvaFieldGroups] = useState<Konva.Group[]>([]);
|
||||
@ -54,10 +35,17 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
const [isFieldChanging, setIsFieldChanging] = useState(false);
|
||||
const [pendingFieldCreation, setPendingFieldCreation] = useState<Konva.Rect | null>(null);
|
||||
|
||||
const viewport = useMemo(
|
||||
() => page.getViewport({ scale, rotation: rotate }),
|
||||
[page, rotate, scale],
|
||||
);
|
||||
const {
|
||||
stage,
|
||||
pageLayer,
|
||||
canvasElement,
|
||||
konvaContainer,
|
||||
pageContext,
|
||||
scaledViewport,
|
||||
unscaledViewport,
|
||||
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
|
||||
|
||||
const { _className, scale } = pageContext;
|
||||
|
||||
const localPageFields = useMemo(
|
||||
() =>
|
||||
@ -68,44 +56,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
[editorFields.localFields, 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 handleResizeOrMove = (event: KonvaEventObject<Event>) => {
|
||||
console.log('Field resized or moved');
|
||||
|
||||
@ -120,6 +70,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
const fieldGroup = event.target as Konva.Group;
|
||||
const fieldFormId = fieldGroup.id();
|
||||
|
||||
// Note: This values are scaled.
|
||||
const {
|
||||
width: fieldPixelWidth,
|
||||
height: fieldPixelHeight,
|
||||
@ -130,7 +81,8 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
skipShadow: true,
|
||||
});
|
||||
|
||||
const { height: pageHeight, width: pageWidth } = getBoundingClientRect(container);
|
||||
const pageHeight = scaledViewport.height;
|
||||
const pageWidth = scaledViewport.width;
|
||||
|
||||
// Calculate x and y as a percentage of the page width and height
|
||||
const positionPercentX = (fieldX / pageWidth) * 100;
|
||||
@ -165,8 +117,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
};
|
||||
|
||||
const renderFieldOnLayer = (field: TLocalField) => {
|
||||
if (!pageLayer.current || !interactiveTransformer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
if (!pageLayer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,7 +125,8 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
const isFieldEditable =
|
||||
recipient !== undefined && canRecipientFieldsBeModified(recipient, envelope.fields);
|
||||
|
||||
const { fieldGroup, isFirstRender } = renderField({
|
||||
const { fieldGroup } = renderField({
|
||||
scale,
|
||||
pageLayer: pageLayer.current,
|
||||
field: {
|
||||
renderId: field.formId,
|
||||
@ -183,8 +135,9 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
pageWidth: viewport.width,
|
||||
pageHeight: viewport.height,
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
pageWidth: unscaledViewport.width,
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: isFieldEditable,
|
||||
mode: 'edit',
|
||||
@ -210,24 +163,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||
// Initialize snap guides layer
|
||||
// snapGuideLayer.current = initializeSnapGuides(stage.current);
|
||||
|
||||
// Add transformer for resizing and rotating.
|
||||
interactiveTransformer.current = createInteractiveTransformer(stage.current, pageLayer.current);
|
||||
interactiveTransformer.current = createInteractiveTransformer(currentStage, currentPageLayer);
|
||||
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
@ -235,12 +178,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
}
|
||||
|
||||
// Handle stage click to deselect.
|
||||
stage.current?.on('click', (e) => {
|
||||
currentStage.on('mousedown', (e) => {
|
||||
removePendingField();
|
||||
|
||||
if (e.target === stage.current) {
|
||||
setSelectedFields([]);
|
||||
pageLayer.current?.batchDraw();
|
||||
currentPageLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
|
||||
@ -267,12 +210,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
setSelectedFields([e.target]);
|
||||
};
|
||||
|
||||
stage.current?.on('dragstart', onDragStartOrEnd);
|
||||
stage.current?.on('dragend', onDragStartOrEnd);
|
||||
stage.current?.on('transformstart', () => setIsFieldChanging(true));
|
||||
stage.current?.on('transformend', () => setIsFieldChanging(false));
|
||||
currentStage.on('dragstart', onDragStartOrEnd);
|
||||
currentStage.on('dragend', onDragStartOrEnd);
|
||||
currentStage.on('transformstart', () => setIsFieldChanging(true));
|
||||
currentStage.on('transformend', () => setIsFieldChanging(false));
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
currentPageLayer.batchDraw();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -284,7 +227,10 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
* - Selecting multiple fields
|
||||
* - Selecting empty area to create fields
|
||||
*/
|
||||
const createInteractiveTransformer = (stage: Konva.Stage, layer: Konva.Layer) => {
|
||||
const createInteractiveTransformer = (
|
||||
currentStage: Konva.Stage,
|
||||
currentPageLayer: Konva.Layer,
|
||||
) => {
|
||||
const transformer = new Konva.Transformer({
|
||||
rotateEnabled: false,
|
||||
keepRatio: false,
|
||||
@ -301,36 +247,39 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
},
|
||||
});
|
||||
|
||||
layer.add(transformer);
|
||||
currentPageLayer.add(transformer);
|
||||
|
||||
// Add selection rectangle.
|
||||
const selectionRectangle = new Konva.Rect({
|
||||
fill: 'rgba(24, 160, 251, 0.3)',
|
||||
visible: false,
|
||||
});
|
||||
layer.add(selectionRectangle);
|
||||
currentPageLayer.add(selectionRectangle);
|
||||
|
||||
let x1: number;
|
||||
let y1: number;
|
||||
let x2: number;
|
||||
let y2: number;
|
||||
|
||||
stage.on('mousedown touchstart', (e) => {
|
||||
currentStage.on('mousedown touchstart', (e) => {
|
||||
// do nothing if we mousedown on any shape
|
||||
if (e.target !== stage) {
|
||||
if (e.target !== currentStage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointerPosition = stage.getPointerPosition();
|
||||
const pointerPosition = currentStage.getPointerPosition();
|
||||
|
||||
if (!pointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
x1 = pointerPosition.x;
|
||||
y1 = pointerPosition.y;
|
||||
x2 = pointerPosition.x;
|
||||
y2 = pointerPosition.y;
|
||||
console.log(`pointerPosition.x: ${pointerPosition.x}`);
|
||||
console.log(`pointerPosition.y: ${pointerPosition.y}`);
|
||||
|
||||
x1 = pointerPosition.x / scale;
|
||||
y1 = pointerPosition.y / scale;
|
||||
x2 = pointerPosition.x / scale;
|
||||
y2 = pointerPosition.y / scale;
|
||||
|
||||
selectionRectangle.setAttrs({
|
||||
x: x1,
|
||||
@ -341,7 +290,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
});
|
||||
});
|
||||
|
||||
stage.on('mousemove touchmove', () => {
|
||||
currentStage.on('mousemove touchmove', () => {
|
||||
// do nothing if we didn't start selection
|
||||
if (!selectionRectangle.visible()) {
|
||||
return;
|
||||
@ -349,14 +298,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
|
||||
selectionRectangle.moveToTop();
|
||||
|
||||
const pointerPosition = stage.getPointerPosition();
|
||||
const pointerPosition = currentStage.getPointerPosition();
|
||||
|
||||
if (!pointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
x2 = pointerPosition.x;
|
||||
y2 = pointerPosition.y;
|
||||
x2 = pointerPosition.x / scale;
|
||||
y2 = pointerPosition.y / scale;
|
||||
|
||||
selectionRectangle.setAttrs({
|
||||
x: Math.min(x1, x2),
|
||||
@ -366,7 +315,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
});
|
||||
});
|
||||
|
||||
stage.on('mouseup touchend', () => {
|
||||
currentStage.on('mouseup touchend', () => {
|
||||
// do nothing if we didn't start selection
|
||||
if (!selectionRectangle.visible()) {
|
||||
return;
|
||||
@ -377,38 +326,41 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
selectionRectangle.visible(false);
|
||||
});
|
||||
|
||||
const stageFieldGroups = stage.find('.field-group') || [];
|
||||
const stageFieldGroups = currentStage.find('.field-group') || [];
|
||||
const box = selectionRectangle.getClientRect();
|
||||
const selectedFieldGroups = stageFieldGroups.filter(
|
||||
(shape) => Konva.Util.haveIntersection(box, shape.getClientRect()) && shape.draggable(),
|
||||
);
|
||||
setSelectedFields(selectedFieldGroups);
|
||||
|
||||
const unscaledBoxWidth = box.width / scale;
|
||||
const unscaledBoxHeight = box.height / scale;
|
||||
|
||||
// Create a field if no items are selected or the size is too small.
|
||||
if (
|
||||
selectedFieldGroups.length === 0 &&
|
||||
canvasElement.current &&
|
||||
box.width > MIN_FIELD_WIDTH_PX &&
|
||||
box.height > MIN_FIELD_HEIGHT_PX &&
|
||||
unscaledBoxWidth > MIN_FIELD_WIDTH_PX &&
|
||||
unscaledBoxHeight > MIN_FIELD_HEIGHT_PX &&
|
||||
editorFields.selectedRecipient &&
|
||||
canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields)
|
||||
) {
|
||||
const pendingFieldCreation = new Konva.Rect({
|
||||
name: 'pending-field-creation',
|
||||
x: box.x,
|
||||
y: box.y,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
x: box.x / scale,
|
||||
y: box.y / scale,
|
||||
width: unscaledBoxWidth,
|
||||
height: unscaledBoxHeight,
|
||||
fill: 'rgba(24, 160, 251, 0.3)',
|
||||
});
|
||||
|
||||
layer.add(pendingFieldCreation);
|
||||
currentPageLayer.add(pendingFieldCreation);
|
||||
setPendingFieldCreation(pendingFieldCreation);
|
||||
}
|
||||
});
|
||||
|
||||
// Clicks should select/deselect shapes
|
||||
stage.on('click tap', function (e) {
|
||||
currentStage.on('click tap', function (e) {
|
||||
// if we are selecting with rect, do nothing
|
||||
if (
|
||||
selectionRectangle.visible() &&
|
||||
@ -419,7 +371,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
}
|
||||
|
||||
// If empty area clicked, remove all selections
|
||||
if (e.target === stage) {
|
||||
if (e.target === stage.current) {
|
||||
setSelectedFields([]);
|
||||
return;
|
||||
}
|
||||
@ -468,20 +420,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
group.name() === 'field-group' &&
|
||||
!localPageFields.some((field) => field.formId === group.id())
|
||||
) {
|
||||
console.log('Field removed, removing from canvas');
|
||||
group.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// If it exists, rerender.
|
||||
localPageFields.forEach((field) => {
|
||||
console.log('Field created/updated, rendering on canvas');
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
|
||||
// If it doesn't exist, render it.
|
||||
//
|
||||
|
||||
// Rerender the transformer
|
||||
interactiveTransformer.current?.forceUpdate();
|
||||
|
||||
@ -555,15 +502,13 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { height: pageHeight, width: pageWidth } = getBoundingClientRect(canvasElement.current);
|
||||
|
||||
const { fieldX, fieldY, fieldWidth, fieldHeight } = convertPixelToPercentage({
|
||||
width: pixelWidth,
|
||||
height: pixelHeight,
|
||||
positionX: pixelX,
|
||||
positionY: pixelY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
pageWidth: unscaledViewport.width,
|
||||
pageHeight: unscaledViewport.height,
|
||||
});
|
||||
|
||||
editorFields.addField({
|
||||
@ -597,7 +542,10 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
||||
<div
|
||||
className="relative w-full"
|
||||
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||
>
|
||||
{selectedKonvaFieldGroups.length > 0 &&
|
||||
interactiveTransformer.current &&
|
||||
!isFieldChanging && (
|
||||
@ -649,17 +597,23 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Todo: Envelopes - This will not overflow the page when close to edges */}
|
||||
{pendingFieldCreation && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: pendingFieldCreation.y() + pendingFieldCreation.getClientRect().height + 5 + 'px',
|
||||
left: pendingFieldCreation.x() + pendingFieldCreation.getClientRect().width / 2 + 'px',
|
||||
top:
|
||||
pendingFieldCreation.y() * scale +
|
||||
pendingFieldCreation.getClientRect().height +
|
||||
5 +
|
||||
'px',
|
||||
left:
|
||||
pendingFieldCreation.x() * scale +
|
||||
pendingFieldCreation.getClientRect().width / 2 +
|
||||
'px',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 50,
|
||||
}}
|
||||
className="text-muted-foreground grid w-fit grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
|
||||
className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
|
||||
>
|
||||
{fieldButtonList.map((field) => (
|
||||
<button
|
||||
@ -673,13 +627,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
@ -60,7 +60,7 @@ const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
|
||||
[FieldType.DROPDOWN]: msg`Dropdown Settings`,
|
||||
};
|
||||
|
||||
export const EnvelopeEditorPageFields = () => {
|
||||
export const EnvelopeEditorFieldsPage = () => {
|
||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
@ -109,7 +109,7 @@ export const EnvelopeEditorPageFields = () => {
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex justify-center">
|
||||
<div className="mt-4 flex justify-center p-4">
|
||||
{currentEnvelopeItem !== null ? (
|
||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
|
||||
) : (
|
||||
@ -128,10 +128,10 @@ export const EnvelopeEditorPageFields = () => {
|
||||
|
||||
{/* Right Section - Form Fields Panel */}
|
||||
{currentEnvelopeItem && (
|
||||
<div className="sticky top-0 h-[calc(100vh-73px)] w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||
<div className="bg-background border-border sticky top-0 h-[calc(100vh-73px)] w-80 flex-shrink-0 overflow-y-auto border-l py-4">
|
||||
{/* Recipient selector section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<h3 className="text-foreground mb-2 text-sm font-semibold">
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
|
||||
@ -170,7 +170,7 @@ export const EnvelopeEditorPageFields = () => {
|
||||
|
||||
{/* Add fields section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<h3 className="text-foreground mb-2 text-sm font-semibold">
|
||||
<Trans>Add Fields</Trans>
|
||||
</h3>
|
||||
|
||||
@ -13,7 +13,6 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Separator } from '@documenso/ui/primitives/separator';
|
||||
@ -24,7 +23,6 @@ import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { DocumentAttachmentsPopover } from '~/components/general/document/document-attachments-popover';
|
||||
import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { TemplateDirectLinkBadge } from '../template/template-direct-link-badge';
|
||||
import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
||||
@ -32,30 +30,35 @@ import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
||||
export default function EnvelopeEditorHeader() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { envelope, isDocument, isTemplate, updateEnvelope, autosaveError } =
|
||||
useCurrentEnvelopeEditor();
|
||||
|
||||
// Todo: Envelopes this probably won't work with embed? Maybe hide the back items when no team?
|
||||
|
||||
const rootPath = isDocument ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
|
||||
const {
|
||||
envelope,
|
||||
isDocument,
|
||||
isTemplate,
|
||||
updateEnvelope,
|
||||
autosaveError,
|
||||
relativePath,
|
||||
syncEnvelope,
|
||||
editorFields,
|
||||
} = useCurrentEnvelopeEditor();
|
||||
|
||||
return (
|
||||
<nav className="w-full border-b border-gray-200 bg-white px-6 py-3">
|
||||
<nav className="bg-background border-border w-full border-b px-4 py-3 md:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link to="/">
|
||||
<BrandingLogo className="h-6 w-auto" />
|
||||
</Link>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<EnvelopeItemTitleInput
|
||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||
value={envelope.title}
|
||||
onChange={(title) => {
|
||||
updateEnvelope({
|
||||
title,
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder={t`Envelope Title`}
|
||||
@ -132,7 +135,7 @@ export default function EnvelopeEditorHeader() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<DocumentAttachmentsPopover envelopeId={envelope.id} />
|
||||
<DocumentAttachmentsPopover envelopeId={envelope.id} buttonSize="sm" />
|
||||
|
||||
<EnvelopeEditorSettingsDialog
|
||||
trigger={
|
||||
@ -145,7 +148,11 @@ export default function EnvelopeEditorHeader() {
|
||||
{isDocument && (
|
||||
<>
|
||||
<EnvelopeDistributeDialog
|
||||
envelope={envelope}
|
||||
envelope={{
|
||||
...envelope,
|
||||
fields: editorFields.localFields,
|
||||
}}
|
||||
onDistribute={syncEnvelope}
|
||||
trigger={
|
||||
<Button size="sm">
|
||||
<SendIcon className="mr-2 h-4 w-4" />
|
||||
@ -168,10 +175,11 @@ export default function EnvelopeEditorHeader() {
|
||||
|
||||
{isTemplate && (
|
||||
<TemplateUseDialog
|
||||
envelopeId={envelope.id}
|
||||
templateId={mapSecondaryIdToTemplateId(envelope.secondaryId)}
|
||||
templateSigningOrder={envelope.documentMeta?.signingOrder}
|
||||
recipients={envelope.recipients}
|
||||
documentRootPath={rootPath}
|
||||
documentRootPath={relativePath.documentRootPath}
|
||||
trigger={
|
||||
<Button size="sm">
|
||||
<Trans>Use Template</Trans>
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import Konva from 'konva';
|
||||
import type { Layer } from 'konva/lib/Layer';
|
||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
import { usePageContext } from 'react-pdf';
|
||||
|
||||
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
|
||||
export default function EnvelopeEditorPagePreviewRenderer() {
|
||||
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 { editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const stage = useRef<Konva.Stage | null>(null);
|
||||
const pageLayer = useRef<Layer | null>(null);
|
||||
|
||||
const viewport = useMemo(
|
||||
() => page.getViewport({ scale, rotation: rotate }),
|
||||
[page, rotate, scale],
|
||||
);
|
||||
|
||||
const localPageFields = useMemo(
|
||||
() =>
|
||||
editorFields.localFields.filter(
|
||||
(field) =>
|
||||
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
||||
),
|
||||
[editorFields.localFields, 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 = (field: TLocalField) => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
renderField({
|
||||
pageLayer: pageLayer.current,
|
||||
field: {
|
||||
renderId: field.formId,
|
||||
...field,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
pageWidth: viewport.width,
|
||||
pageHeight: viewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: false,
|
||||
mode: 'export',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the initial Konva page canvas and initialize 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);
|
||||
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
}
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
};
|
||||
|
||||
/**
|
||||
* Render fields when they are added or removed from the localFields.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!pageLayer.current || !stage.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If doesn't exist in localFields, destroy it since it's been deleted.
|
||||
pageLayer.current.find('Group').forEach((group) => {
|
||||
if (
|
||||
group.name() === 'field-group' &&
|
||||
!localPageFields.some((field) => field.formId === group.id())
|
||||
) {
|
||||
console.log('Field removed, removing from canvas');
|
||||
group.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// If it exists, rerender.
|
||||
localPageFields.forEach((field) => {
|
||||
console.log('Field created/updated, rendering on canvas');
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [localPageFields]);
|
||||
|
||||
if (!currentEnvelopeItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
||||
|
||||
<canvas
|
||||
className={`${_className}__canvas z-0`}
|
||||
height={viewport.height}
|
||||
ref={canvasElement}
|
||||
width={viewport.width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { lazy, useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { ConstructionIcon, FileTextIcon } from 'lucide-react';
|
||||
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
@ -13,11 +13,9 @@ import { Separator } from '@documenso/ui/primitives/separator';
|
||||
|
||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||
|
||||
const EnvelopeEditorPagePreviewRenderer = lazy(
|
||||
async () => import('./envelope-editor-page-preview-renderer'),
|
||||
);
|
||||
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
|
||||
|
||||
export const EnvelopeEditorPagePreview = () => {
|
||||
export const EnvelopeEditorPreviewPage = () => {
|
||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
@ -50,19 +48,35 @@ export const EnvelopeEditorPagePreview = () => {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{currentEnvelopeItem !== null ? (
|
||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorPagePreviewRenderer} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-32">
|
||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||
<p className="text-foreground mt-1 text-sm">
|
||||
<Trans>No documents found</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<Trans>Please upload a document to continue</Trans>
|
||||
{/* Coming soon section */}
|
||||
<div className="border-border bg-card hover:bg-accent/10 flex w-full max-w-[800px] items-center gap-4 rounded-lg border p-4 transition-colors">
|
||||
<div className="flex w-full flex-col items-center justify-center gap-2 py-32">
|
||||
<ConstructionIcon className="text-muted-foreground h-10 w-10" />
|
||||
<h3 className="text-foreground text-sm font-semibold">
|
||||
<Trans>Coming soon</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>This feature is coming soon</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Todo: Envelopes - Remove div after preview mode is implemented */}
|
||||
<div className="hidden">
|
||||
{currentEnvelopeItem !== null ? (
|
||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-32">
|
||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||
<p className="text-foreground mt-1 text-sm">
|
||||
<Trans>No documents found</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<Trans>Please upload a document to continue</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -75,7 +75,6 @@ const ZEnvelopeRecipientsForm = z.object({
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
// Todo: Envelopes - These aren't synced to the server
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean().default(false),
|
||||
});
|
||||
@ -83,7 +82,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
||||
type TEnvelopeRecipientsForm = z.infer<typeof ZEnvelopeRecipientsForm>;
|
||||
|
||||
export const EnvelopeEditorRecipientForm = () => {
|
||||
const { envelope, setRecipientsDebounced } = useCurrentEnvelopeEditor();
|
||||
const { envelope, setRecipientsDebounced, updateEnvelope } = useCurrentEnvelopeEditor();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
@ -451,6 +450,8 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
void form.trigger();
|
||||
}, [form]);
|
||||
|
||||
// Dupecode/Inefficient: Done because native isValid won't work for our usecase.
|
||||
@ -460,15 +461,39 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||
const formValueSigners = formValues.signers || [];
|
||||
|
||||
// Remove the last signer if it's empty.
|
||||
const recipients = formValueSigners.filter((signer, i) => {
|
||||
if (i === formValueSigners.length - 1 && signer.email === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
|
||||
...formValues,
|
||||
signers: recipients,
|
||||
});
|
||||
|
||||
if (validatedFormValues.success) {
|
||||
console.log('validatedFormValues', validatedFormValues);
|
||||
|
||||
setRecipientsDebounced(validatedFormValues.data.signers);
|
||||
|
||||
// Todo: Envelopes - Need to save the other data as well
|
||||
// setEnvelope
|
||||
if (
|
||||
validatedFormValues.data.signingOrder !== envelope.documentMeta.signingOrder ||
|
||||
validatedFormValues.data.allowDictateNextSigner !==
|
||||
envelope.documentMeta.allowDictateNextSigner
|
||||
) {
|
||||
updateEnvelope({
|
||||
meta: {
|
||||
signingOrder: validatedFormValues.data.signingOrder,
|
||||
allowDictateNextSigner: validatedFormValues.data.allowDictateNextSigner,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [formValues]);
|
||||
|
||||
@ -508,7 +533,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
<CardContent>
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
<div className="-mt-2 mb-2 space-y-4 rounded-md bg-gray-50/80 p-4">
|
||||
<div className="bg-accent/50 -mt-2 mb-2 space-y-4 rounded-md p-4">
|
||||
{!alwaysShowAdvancedSettings && organisation.organisationClaim.flags.cfr21 && (
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
@ -876,6 +901,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
onValueChange={(value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole);
|
||||
field.onChange(value);
|
||||
}}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
|
||||
@ -215,7 +215,6 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
|
||||
const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation();
|
||||
|
||||
// Todo: Envelopes - Extract into provider.
|
||||
const envelopeHasBeenSent =
|
||||
envelope.type === EnvelopeType.DOCUMENT &&
|
||||
envelope.recipients.some((recipient) => recipient.sendStatus === SendStatus.SENT);
|
||||
@ -302,8 +301,6 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
setActiveTab('general');
|
||||
}, [open, form]);
|
||||
|
||||
// Todo: Envelopes - Show error indicator if error is in different tab.
|
||||
|
||||
const selectedTab = tabs.find((tab) => tab.id === activeTab);
|
||||
|
||||
if (!selectedTab) {
|
||||
|
||||
@ -7,12 +7,16 @@ import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { FileWarningIcon, GripVerticalIcon, Loader2 } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection } from 'react-dropzone';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import {
|
||||
useCurrentEnvelopeEditor,
|
||||
useDebounceFunction,
|
||||
} from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
@ -26,9 +30,9 @@ import {
|
||||
CardTitle,
|
||||
} from '@documenso/ui/primitives/card';
|
||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { EnvelopeItemDeleteDialog } from '~/components/dialogs/envelope-item-delete-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeEditorRecipientForm } from './envelope-editor-recipient-form';
|
||||
import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
||||
@ -41,11 +45,13 @@ type LocalFile = {
|
||||
isError: boolean;
|
||||
};
|
||||
|
||||
export const EnvelopeEditorPageUpload = () => {
|
||||
const team = useCurrentTeam();
|
||||
const { t } = useLingui();
|
||||
export const EnvelopeEditorUploadPage = () => {
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { envelope, setLocalEnvelope } = useCurrentEnvelopeEditor();
|
||||
const { t } = useLingui();
|
||||
const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
|
||||
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [localFiles, setLocalFiles] = useState<LocalFile[]>(
|
||||
envelope.envelopeItems
|
||||
@ -220,12 +226,56 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
debouncedUpdateEnvelopeItems(newLocalFilesValue);
|
||||
};
|
||||
|
||||
const dropzoneDisabledMessage = useMemo(() => {
|
||||
if (!canItemsBeModified) {
|
||||
return msg`Cannot upload items after the document has been sent`;
|
||||
}
|
||||
|
||||
if (organisation.subscription && remaining.documents === 0) {
|
||||
return msg`Document upload disabled due to unpaid invoices`;
|
||||
}
|
||||
|
||||
if (maximumEnvelopeItemCount <= localFiles.length) {
|
||||
return msg`You cannot upload more than ${maximumEnvelopeItemCount} items per envelope.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localFiles.length, maximumEnvelopeItemCount, remaining.documents]);
|
||||
|
||||
const onFileDropRejected = (fileRejections: FileRejection[]) => {
|
||||
const maxItemsReached = fileRejections.some((fileRejection) =>
|
||||
fileRejection.errors.some((error) => error.code === DropzoneErrorCode.TooManyFiles),
|
||||
);
|
||||
|
||||
if (maxItemsReached) {
|
||||
toast({
|
||||
title: t`You cannot upload more than ${maximumEnvelopeItemCount} items per envelope.`,
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t`Upload failed`,
|
||||
description: t`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`,
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6 p-8">
|
||||
<Card backdropBlur={false} className="border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Documents</CardTitle>
|
||||
<CardDescription>Add and configure multiple documents</CardDescription>
|
||||
<CardTitle>
|
||||
<Trans>Documents</Trans>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans>Add and configure multiple documents</Trans>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
@ -233,9 +283,11 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
onDrop={onFileDrop}
|
||||
allowMultiple
|
||||
className="pb-4 pt-6"
|
||||
disabled={!canItemsBeModified}
|
||||
disabledMessage={msg`Cannot upload items after the document has been sent`}
|
||||
disabled={dropzoneDisabledMessage !== null}
|
||||
disabledMessage={dropzoneDisabledMessage || undefined}
|
||||
disabledHeading={msg`Upload disabled`}
|
||||
maxFiles={maximumEnvelopeItemCount - localFiles.length}
|
||||
onDropRejected={onFileDropRejected}
|
||||
/>
|
||||
|
||||
{/* Uploaded Files List */}
|
||||
@ -256,7 +308,7 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={provided.draggableProps.style}
|
||||
className={`flex items-center justify-between rounded-lg bg-gray-50 p-3 transition-shadow ${
|
||||
className={`bg-accent/50 flex items-center justify-between rounded-lg p-3 transition-shadow ${
|
||||
snapshot.isDragging ? 'shadow-md' : ''
|
||||
}`}
|
||||
>
|
||||
@ -282,7 +334,7 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
<p className="text-sm font-medium">{localFile.title}</p>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{localFile.isUploading ? (
|
||||
<Trans>Uploading</Trans>
|
||||
) : localFile.isError ? (
|
||||
@ -295,7 +347,7 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
<div className="flex items-center space-x-2">
|
||||
{localFile.isUploading && (
|
||||
<div className="flex h-6 w-10 items-center justify-center">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-500" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -338,7 +390,7 @@ export const EnvelopeEditorPageUpload = () => {
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button asChild>
|
||||
<Link to={`/t/${team.url}/documents/${envelope.id}/edit?step=addFields`}>
|
||||
<Link to={`${relativePath.editorPath}?step=addFields`}>
|
||||
<Trans>Add Fields</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
@ -24,7 +24,6 @@ import {
|
||||
mapSecondaryIdToDocumentId,
|
||||
mapSecondaryIdToTemplateId,
|
||||
} from '@documenso/lib/utils/envelope';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Separator } from '@documenso/ui/primitives/separator';
|
||||
@ -32,17 +31,17 @@ import { SpinnerBox } from '@documenso/ui/primitives/spinner';
|
||||
|
||||
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
||||
import { EnvelopeDistributeDialog } from '~/components/dialogs/envelope-distribute-dialog';
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
import { EnvelopeDuplicateDialog } from '~/components/dialogs/envelope-duplicate-dialog';
|
||||
import { EnvelopeRedistributeDialog } from '~/components/dialogs/envelope-redistribute-dialog';
|
||||
import { TemplateDeleteDialog } from '~/components/dialogs/template-delete-dialog';
|
||||
import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
|
||||
import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeEditorFieldsPage } from './envelope-editor-fields-page';
|
||||
import EnvelopeEditorHeader from './envelope-editor-header';
|
||||
import { EnvelopeEditorPageFields } from './envelope-editor-page-fields';
|
||||
import { EnvelopeEditorPagePreview } from './envelope-editor-page-preview';
|
||||
import { EnvelopeEditorPageUpload } from './envelope-editor-page-upload';
|
||||
import { EnvelopeEditorPreviewPage } from './envelope-editor-preview-page';
|
||||
import { EnvelopeEditorUploadPage } from './envelope-editor-upload-page';
|
||||
|
||||
type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
|
||||
|
||||
@ -74,10 +73,17 @@ export default function EnvelopeEditor() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { envelope, isDocument, isTemplate, isAutosaving, flushAutosave } =
|
||||
useCurrentEnvelopeEditor();
|
||||
const {
|
||||
envelope,
|
||||
isDocument,
|
||||
isTemplate,
|
||||
isAutosaving,
|
||||
flushAutosave,
|
||||
relativePath,
|
||||
syncEnvelope,
|
||||
editorFields,
|
||||
} = useCurrentEnvelopeEditor();
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -100,13 +106,10 @@ export default function EnvelopeEditor() {
|
||||
return 'upload';
|
||||
});
|
||||
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
const templatesPath = formatTemplatesPath(team.url);
|
||||
|
||||
const navigateToStep = (step: EnvelopeEditorStep) => {
|
||||
setCurrentStep(step);
|
||||
|
||||
flushAutosave();
|
||||
void flushAutosave();
|
||||
|
||||
if (!isStepLoading && isAutosaving) {
|
||||
setIsStepLoading(true);
|
||||
@ -128,6 +131,18 @@ export default function EnvelopeEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
// Watch the URL params and setStep if the step changes.
|
||||
useEffect(() => {
|
||||
const stepParam = searchParams.get('step') || envelopeEditorSteps[0].id;
|
||||
|
||||
const foundStep = envelopeEditorSteps.find((step) => step.id === stepParam);
|
||||
|
||||
if (foundStep && foundStep.id !== currentStep) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
navigateToStep(foundStep.id as EnvelopeEditorStep);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAutosaving) {
|
||||
setIsStepLoading(false);
|
||||
@ -138,20 +153,22 @@ export default function EnvelopeEditor() {
|
||||
envelopeEditorSteps.find((step) => step.id === currentStep) || envelopeEditorSteps[0];
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-gray-50">
|
||||
<div className="dark:bg-background h-screen w-screen bg-gray-50">
|
||||
<EnvelopeEditorHeader />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex h-[calc(100vh-73px)] w-screen">
|
||||
{/* Left Section - Step Navigation */}
|
||||
<div className="flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r border-gray-200 bg-white py-4">
|
||||
<div className="bg-background border-border flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4">
|
||||
{/* Left section step selector. */}
|
||||
<div className="px-4">
|
||||
<h3 className="flex items-end justify-between text-sm font-semibold text-gray-900">
|
||||
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
|
||||
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
|
||||
|
||||
<span className="text-muted-foreground ml-2 rounded border bg-gray-50 px-2 py-0.5 text-xs">
|
||||
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
||||
<span className="text-muted-foreground bg-muted/50 ml-2 rounded border px-2 py-0.5 text-xs">
|
||||
<Trans context="The step counter">
|
||||
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
||||
</Trans>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
@ -176,15 +193,17 @@ export default function EnvelopeEditor() {
|
||||
key={step.id}
|
||||
className={`cursor-pointer rounded-lg p-3 transition-colors ${
|
||||
isActive
|
||||
? 'border border-green-200 bg-green-50'
|
||||
: 'border border-gray-200 hover:bg-gray-50'
|
||||
? 'border border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
|
||||
: 'border border-gray-200 hover:bg-gray-50 dark:border-gray-400/20 dark:hover:bg-gray-400/10'
|
||||
}`}
|
||||
onClick={() => navigateToStep(step.id as EnvelopeEditorStep)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`rounded border p-2 ${
|
||||
isActive ? 'border-green-200 bg-green-50' : 'border-gray-100 bg-gray-100'
|
||||
isActive
|
||||
? 'border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
|
||||
: 'border-gray-100 bg-gray-100 dark:border-gray-400/20 dark:bg-gray-400/10'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
@ -194,12 +213,14 @@ export default function EnvelopeEditor() {
|
||||
<div>
|
||||
<div
|
||||
className={`text-sm font-medium ${
|
||||
isActive ? 'text-green-900' : 'text-gray-700'
|
||||
isActive
|
||||
? 'text-green-900 dark:text-green-400'
|
||||
: 'text-foreground dark:text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t(step.title)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{t(step.description)}</div>
|
||||
<div className="text-muted-foreground text-xs">{t(step.description)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -212,12 +233,25 @@ export default function EnvelopeEditor() {
|
||||
|
||||
{/* Quick Actions. */}
|
||||
<div className="space-y-3 px-4">
|
||||
<h4 className="text-sm font-semibold text-gray-900">
|
||||
<h4 className="text-foreground text-sm font-semibold">
|
||||
<Trans>Quick Actions</Trans>
|
||||
</h4>
|
||||
<EnvelopeEditorSettingsDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
{isDocument ? <Trans>Document Settings</Trans> : <Trans>Template Settings</Trans>}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{isDocument && (
|
||||
<EnvelopeDistributeDialog
|
||||
envelope={envelope}
|
||||
envelope={{
|
||||
...envelope,
|
||||
fields: editorFields.localFields,
|
||||
}}
|
||||
onDistribute={syncEnvelope}
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<SendIcon className="mr-2 h-4 w-4" />
|
||||
@ -239,16 +273,6 @@ export default function EnvelopeEditor() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<EnvelopeEditorSettingsDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
{isDocument ? <Trans>Document Settings</Trans> : <Trans>Template Settings</Trans>}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Todo: Envelopes */}
|
||||
{/* <Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Save as Template
|
||||
@ -283,11 +307,17 @@ export default function EnvelopeEditor() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Todo: Allow selecting which document to download and/or the original */}
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<DownloadCloudIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Download PDF</Trans>
|
||||
</Button>
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelope.id}
|
||||
envelopeStatus={envelope.status}
|
||||
envelopeItems={envelope.envelopeItems}
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
<DownloadCloudIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Download PDF</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -309,7 +339,7 @@ export default function EnvelopeEditor() {
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onDelete={async () => {
|
||||
await navigate(documentsPath);
|
||||
await navigate(relativePath.documentRootPath);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -318,7 +348,7 @@ export default function EnvelopeEditor() {
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onDelete={async () => {
|
||||
await navigate(templatesPath);
|
||||
await navigate(relativePath.templateRootPath);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -326,7 +356,7 @@ export default function EnvelopeEditor() {
|
||||
{/* Footer of left sidebar. */}
|
||||
<div className="mt-auto px-4">
|
||||
<Button variant="ghost" className="w-full justify-start" asChild>
|
||||
<Link to={isDocument ? documentsPath : templatesPath}>
|
||||
<Link to={relativePath.basePath}>
|
||||
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
||||
{isDocument ? (
|
||||
<Trans>Return to documents</Trans>
|
||||
@ -340,13 +370,12 @@ export default function EnvelopeEditor() {
|
||||
|
||||
{/* Main Content - Changes based on current step */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<p>{isAutosaving ? 'Autosaving...' : 'Not autosaving'}</p>
|
||||
<AnimateGenericFadeInOut key={currentStep}>
|
||||
{match({ currentStep, isStepLoading })
|
||||
.with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />)
|
||||
.with({ currentStep: 'upload' }, () => <EnvelopeEditorPageUpload />)
|
||||
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorPageFields />)
|
||||
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPagePreview />)
|
||||
.with({ currentStep: 'upload' }, () => <EnvelopeEditorUploadPage />)
|
||||
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorFieldsPage />)
|
||||
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPreviewPage />)
|
||||
.exhaustive()}
|
||||
</AnimateGenericFadeInOut>
|
||||
</div>
|
||||
|
||||
@ -20,16 +20,16 @@ export const EnvelopeItemSelector = ({
|
||||
}: EnvelopeItemSelectorProps) => {
|
||||
return (
|
||||
<button
|
||||
className={`flex min-w-0 cursor-pointer items-center space-x-3 rounded-lg border px-4 py-3 transition-colors ${
|
||||
className={`flex min-w-0 flex-shrink-0 cursor-pointer items-center space-x-3 rounded-lg border px-4 py-3 transition-colors ${
|
||||
isSelected
|
||||
? 'border-blue-200 bg-blue-50 text-blue-900'
|
||||
: 'border-gray-200 bg-gray-50 hover:bg-gray-100'
|
||||
? 'border-green-200 bg-green-50 text-green-900 dark:border-green-400/30 dark:bg-green-400/10 dark:text-green-400'
|
||||
: 'border-border bg-muted/50 hover:bg-muted/70'
|
||||
}`}
|
||||
{...buttonProps}
|
||||
>
|
||||
<div
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${
|
||||
isSelected ? 'bg-blue-100 text-blue-600' : 'bg-gray-200 text-gray-600'
|
||||
isSelected ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{number}
|
||||
@ -40,7 +40,7 @@ export const EnvelopeItemSelector = ({
|
||||
</div>
|
||||
<div
|
||||
className={cn('h-2 w-2 rounded-full', {
|
||||
'bg-blue-500': isSelected,
|
||||
'bg-green-500': isSelected,
|
||||
})}
|
||||
></div>
|
||||
</button>
|
||||
|
||||
@ -1,41 +1,32 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import Konva from 'konva';
|
||||
import type { Layer } from 'konva/lib/Layer';
|
||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
import { usePageContext } from 'react-pdf';
|
||||
import type Konva from 'konva';
|
||||
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
|
||||
export default function EnvelopeGenericPageRenderer() {
|
||||
const pageContext = usePageContext();
|
||||
const { i18n } = useLingui();
|
||||
|
||||
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 { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||
|
||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
stage,
|
||||
pageLayer,
|
||||
canvasElement,
|
||||
konvaContainer,
|
||||
pageContext,
|
||||
scaledViewport,
|
||||
unscaledViewport,
|
||||
} = usePageRenderer(({ stage, pageLayer }) => {
|
||||
createPageCanvas(stage, pageLayer);
|
||||
});
|
||||
|
||||
const stage = useRef<Konva.Stage | null>(null);
|
||||
const pageLayer = useRef<Layer | null>(null);
|
||||
|
||||
const viewport = useMemo(
|
||||
() => page.getViewport({ scale, rotation: rotate }),
|
||||
[page, rotate, scale],
|
||||
);
|
||||
const { _className, scale } = pageContext;
|
||||
|
||||
const localPageFields = useMemo(
|
||||
() =>
|
||||
@ -46,44 +37,6 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
[fields, 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 = (field: TEnvelope['fields'][number]) => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
@ -91,6 +44,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
}
|
||||
|
||||
renderField({
|
||||
scale,
|
||||
pageLayer: pageLayer.current,
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
@ -103,8 +57,9 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
pageWidth: viewport.width,
|
||||
pageHeight: viewport.height,
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
pageWidth: unscaledViewport.width,
|
||||
pageHeight: unscaledViewport.height,
|
||||
// color: getRecipientColorKey(field.recipientId),
|
||||
color: 'purple', // Todo
|
||||
editable: false,
|
||||
@ -113,25 +68,15 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
const createPageCanvas = (_currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
}
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
currentPageLayer.batchDraw();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -167,14 +112,19 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
||||
<div
|
||||
className="relative w-full"
|
||||
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||
>
|
||||
{/* 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