Merge branch 'main' into feat/reset-branding-to-default

This commit is contained in:
Catalin Pit
2026-06-25 12:18:59 +03:00
committed by GitHub
39 changed files with 2501 additions and 1168 deletions
@@ -3,12 +3,13 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError } from '@documenso/lib/errors/app-error';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { hasOverlappingFields } from '@documenso/lib/utils/fields-overlap';
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
import { zEmail } from '@documenso/lib/utils/zod';
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { cn } from '@documenso/ui/lib/utils';
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 {
Dialog,
@@ -32,7 +33,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { InfoIcon } from 'lucide-react';
import { AlertTriangleIcon, InfoIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
@@ -138,6 +139,27 @@ export const EnvelopeDistributeDialog = ({
});
}, [recipientsWithIndex, envelope.authOptions]);
/**
* Whether any fields significantly overlap each other. This is surfaced as a
* non-blocking warning since overlapping fields still allow sending, but can
* complicate the signing process or cause fields to behave unexpectedly.
*/
const hasOverlappingEnvelopeFields = useMemo(
() =>
hasOverlappingFields(
envelope.fields.map((field) => ({
id: field.id,
envelopeItemId: field.envelopeItemId,
page: field.page,
positionX: Number(field.positionX),
positionY: Number(field.positionY),
width: Number(field.width),
height: Number(field.height),
})),
),
[envelope.fields],
);
const invalidEnvelopeCode = useMemo(() => {
if (recipientsMissingSignatureFields.length > 0) {
return 'MISSING_SIGNATURES';
@@ -240,6 +262,24 @@ export const EnvelopeDistributeDialog = ({
<Form {...form}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<fieldset disabled={isSubmitting}>
{hasOverlappingEnvelopeFields && (
<Alert variant="warning" className="mb-4 flex flex-row items-start gap-3">
<AlertTriangleIcon className="mt-0.5 h-5 w-5 flex-shrink-0" />
<div className="flex flex-col gap-1">
<AlertTitle>
<Trans>Overlapping fields detected</Trans>
</AlertTitle>
<AlertDescription>
<Trans>
Some fields are placed on top of each other. This may complicate the signing process or cause
fields to not work as expected.
</Trans>
</AlertDescription>
</div>
</Alert>
)}
<Tabs
onValueChange={(value) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -1,3 +1,4 @@
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
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';
@@ -13,6 +14,7 @@ import {
} 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 { getOverlappingFieldPairs } from '@documenso/lib/utils/fields-overlap';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import {
Command,
@@ -62,6 +64,36 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
[editorFields.localFields, pageNumber, currentEnvelopeItem?.id],
);
/**
* Debounce the fields used for overlap highlighting so we don't recompute on every
* small drag/resize tick. Overlaps only occur within the same page and envelope
* item, so computing from this page's fields alone is sufficient.
*/
const debouncedPageFields = useDebouncedValue(localPageFields, 300);
const overlappingFieldFormIds = useMemo(() => {
const formIds = new Set<string>();
const pairs = getOverlappingFieldPairs(
debouncedPageFields.map((field) => ({
id: field.formId,
envelopeItemId: field.envelopeItemId,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
})),
);
for (const pair of pairs) {
formIds.add(pair.fieldA.id);
formIds.add(pair.fieldB.id);
}
return formIds;
}, [debouncedPageFields]);
const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
const isDragEvent = event.type === 'dragend';
@@ -113,6 +145,62 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
pageLayer.current?.batchDraw();
};
/**
* Draws (or removes) a dashed warning outline over a field that significantly
* overlaps another field. The highlight is a child of the field group so it moves
* and resizes with the field, and sits on top of the field's own rect (which is
* re-styled on every render and would otherwise clobber a direct stroke change).
*/
const syncOverlapHighlight = (fieldGroup: Konva.Group, isOverlapping: boolean) => {
const existingHighlight = fieldGroup.findOne('.field-overlap-highlight');
// Skip while a field is actively being dragged/resized. The highlight is driven
// by debounced field data, so it would lag behind and distort during the gesture.
// It is repainted once the gesture settles (the effect re-runs on isFieldChanging).
if (isFieldChanging) {
existingHighlight?.destroy();
return;
}
if (!isOverlapping) {
existingHighlight?.destroy();
return;
}
const fieldRect = fieldGroup.findOne('.field-rect');
if (!fieldRect) {
return;
}
const highlightAttrs = {
x: 0,
y: 0,
width: fieldRect.width(),
height: fieldRect.height(),
stroke: '#f59e0b',
strokeWidth: 2,
dash: [6, 4],
cornerRadius: 2,
strokeScaleEnabled: false,
listening: false,
} satisfies Partial<Konva.RectConfig>;
if (existingHighlight instanceof Konva.Rect) {
existingHighlight.setAttrs(highlightAttrs);
existingHighlight.moveToTop();
return;
}
const highlight = new Konva.Rect({
name: 'field-overlap-highlight',
...highlightAttrs,
});
fieldGroup.add(highlight);
highlight.moveToTop();
};
const unsafeRenderFieldOnLayer = (field: TLocalField) => {
if (!pageLayer.current) {
return;
@@ -139,6 +227,8 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
mode: 'edit',
});
syncOverlapHighlight(fieldGroup, overlappingFieldFormIds.has(field.formId));
if (!isFieldEditable) {
return;
}
@@ -435,7 +525,7 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
interactiveTransformer.current?.forceUpdate();
pageLayer.current.batchDraw();
}, [localPageFields, selectedKonvaFieldGroups]);
}, [localPageFields, selectedKonvaFieldGroups, overlappingFieldFormIds, isFieldChanging]);
const setSelectedFields = (nodes: Konva.Node[]) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -1,3 +1,4 @@
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
@@ -17,6 +18,7 @@ import {
type TTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { getOverlappingFieldPairs } from '@documenso/lib/utils/fields-overlap';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { cn } from '@documenso/ui/lib/utils';
@@ -28,7 +30,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { FileTextIcon, PencilIcon, SparklesIcon } from 'lucide-react';
import { AlertTriangleIcon, FileTextIcon, PencilIcon, SparklesIcon } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useRevalidator, useSearchParams } from 'react-router';
import { isDeepEqual } from 'remeda';
@@ -78,7 +80,7 @@ export const EnvelopeEditorFieldsPage = () => {
const { envelope, editorFields, navigateToStep, editorConfig } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const { currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
const { _ } = useLingui();
@@ -93,6 +95,53 @@ export const EnvelopeEditorFieldsPage = () => {
const selectedField = useMemo(() => structuredClone(editorFields.selectedField), [editorFields.selectedField]);
/**
* Debounce the fields used for overlap detection so we don't recompute on every
* small drag/resize movement, which is expensive on large field counts and can
* bog down lower-end devices.
*/
const debouncedLocalFields = useDebouncedValue(editorFields.localFields, 300);
/**
* Fields that significantly overlap each other. Overlapping fields render poorly in
* the editor and can behave unexpectedly during signing, so we warn the author here.
*/
const overlappingFieldPairs = useMemo(
() =>
getOverlappingFieldPairs(
debouncedLocalFields.map((field) => ({
id: field.formId,
envelopeItemId: field.envelopeItemId,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
})),
),
[debouncedLocalFields],
);
const handleReviewOverlappingField = () => {
const firstPair = overlappingFieldPairs[0];
if (!firstPair) {
return;
}
const targetField = editorFields.localFields.find((field) => field.formId === firstPair.fieldA.id);
if (!targetField) {
return;
}
if (targetField.envelopeItemId !== currentEnvelopeItem?.id) {
setCurrentEnvelopeItem(targetField.envelopeItemId);
}
editorFields.setSelectedField(targetField.formId);
};
const updateSelectedFieldMeta = (fieldMeta: TFieldMetaSchema) => {
if (!selectedField) {
return;
@@ -211,6 +260,29 @@ export const EnvelopeEditorFieldsPage = () => {
</Alert>
)}
{overlappingFieldPairs.length > 0 && (
<Alert
variant="warning"
className="mt-20 mb-4 flex w-full max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm"
>
<div className="flex flex-row items-start gap-3">
<AlertTriangleIcon className="mt-0.5 h-5 w-5 flex-shrink-0" />
<div className="flex flex-col gap-1">
<AlertTitle>
<Trans>Overlapping fields detected</Trans>
</AlertTitle>
<AlertDescription>
<Trans>
Some fields are placed on top of each other. This may complicate the signing process or cause
fields to not work as expected.
</Trans>
</AlertDescription>
</div>
</div>
</Alert>
)}
{currentEnvelopeItem !== null ? (
<EnvelopePdfViewer
customPageRenderer={EnvelopeEditorFieldsPageRenderer}