mirror of
https://github.com/documenso/documenso.git
synced 2026-09-30 00:34:35 +10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d4bbd8bc | ||
|
|
c942ad4a2c | ||
|
|
c8ff704467 |
@@ -1,5 +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 { 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';
|
||||
@@ -26,7 +25,6 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Separator } from '@documenso/ui/primitives/separator';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@@ -34,7 +32,6 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||
import { AlertTriangleIcon, FileTextIcon, PencilIcon, SparklesIcon } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useRevalidator, useSearchParams } from 'react-router';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -87,7 +84,6 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
const { currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
||||
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||
@@ -100,34 +96,6 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
|
||||
const selectedField = useMemo(() => structuredClone(editorFields.selectedField), [editorFields.selectedField]);
|
||||
|
||||
const [copiedField, setCopiedField] = useState<TLocalField | null>(null);
|
||||
|
||||
useHotkeys(['ctrl+c', 'meta+c'], (event) => {
|
||||
// Keep the native copy when the user has selected text.
|
||||
if (!selectedField || window.getSelection()?.toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
setCopiedField(selectedField);
|
||||
|
||||
toast({
|
||||
title: _(msg`Copied field`),
|
||||
description: _(msg`Copied field to clipboard`),
|
||||
});
|
||||
});
|
||||
|
||||
useHotkeys(['ctrl+v', 'meta+v'], (event) => {
|
||||
if (!copiedField) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Paste the next copy offset from the last one, so repeated pastes do not stack.
|
||||
setCopiedField(editorFields.duplicateField(copiedField));
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -633,65 +633,6 @@ const assertDuplicateDeleteFieldPersistedInDatabase = async ({
|
||||
expect(envelope.fields[0].type).toBe(FieldType.SIGNATURE);
|
||||
};
|
||||
|
||||
// --- Copy and paste fields flow ---
|
||||
|
||||
type TCopyPasteFlowResult = {
|
||||
externalId: string;
|
||||
};
|
||||
|
||||
const runCopyPasteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TCopyPasteFlowResult> => {
|
||||
const externalId = `e2e-copy-paste-${nanoid()}`;
|
||||
const root = surface.root;
|
||||
|
||||
await updateExternalId(surface, externalId);
|
||||
await setupRecipientsForFieldPlacement(surface);
|
||||
|
||||
await clickEnvelopeEditorStep(root, 'addFields');
|
||||
await expect(root.locator('.konva-container canvas').first()).toBeVisible();
|
||||
|
||||
await placeFieldOnPdf(root, 'Signature', { x: 150, y: 150 });
|
||||
await selectFieldOnCanvas(root, { x: 150, y: 150 });
|
||||
|
||||
await root.keyboard.press('ControlOrMeta+c');
|
||||
await root.keyboard.press('ControlOrMeta+v');
|
||||
await root.keyboard.press('ControlOrMeta+v');
|
||||
|
||||
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);
|
||||
|
||||
// Navigate away and back to persist changes.
|
||||
await clickEnvelopeEditorStep(root, 'upload');
|
||||
await clickEnvelopeEditorStep(root, 'addFields');
|
||||
await expect.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);
|
||||
|
||||
return { externalId };
|
||||
};
|
||||
|
||||
const assertCopyPasteFieldPersistedInDatabase = async ({
|
||||
surface,
|
||||
externalId,
|
||||
}: {
|
||||
surface: TEnvelopeEditorSurface;
|
||||
externalId: string;
|
||||
}) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
externalId,
|
||||
userId: surface.userId,
|
||||
teamId: surface.teamId,
|
||||
type: surface.envelopeType,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { fields: true },
|
||||
});
|
||||
|
||||
expect(envelope.fields).toHaveLength(3);
|
||||
expect(envelope.fields.every((field) => field.type === FieldType.SIGNATURE)).toBe(true);
|
||||
|
||||
// Each paste is offset from the previous one, so no two fields share a position.
|
||||
const positions = new Set(envelope.fields.map((field) => `${field.positionX}:${field.positionY}`));
|
||||
expect(positions.size).toBe(3);
|
||||
};
|
||||
|
||||
// --- Change field type flow ---
|
||||
|
||||
type TChangeFieldTypeFlowResult = {
|
||||
@@ -961,16 +902,6 @@ test.describe('document editor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('copy and paste fields with keyboard shortcuts', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const result = await runCopyPasteFieldFlow(surface);
|
||||
|
||||
await assertCopyPasteFieldPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('place and configure all 10 field types', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const result = await runAllFieldTypesFlow(surface);
|
||||
@@ -1033,16 +964,6 @@ test.describe('template editor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('copy and paste fields with keyboard shortcuts', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const result = await runCopyPasteFieldFlow(surface);
|
||||
|
||||
await assertCopyPasteFieldPersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
test('place and configure all 10 field types', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const result = await runAllFieldTypesFlow(surface);
|
||||
|
||||
@@ -29,7 +29,7 @@ export type TrpcRouteMeta = {
|
||||
successDescription?: string;
|
||||
errorResponses?: number[] | Record<number, string>;
|
||||
};
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
const t = initTRPC
|
||||
.meta<TrpcRouteMeta>()
|
||||
@@ -37,32 +37,30 @@ const t = initTRPC
|
||||
.create({
|
||||
transformer: dataTransformer,
|
||||
errorFormatter(opts) {
|
||||
const { shape, error, ctx } = opts;
|
||||
const { shape: errorTemplate, error, ctx } = opts;
|
||||
|
||||
const originalError = error.cause;
|
||||
|
||||
let data: Record<string, unknown> = shape.data;
|
||||
if (!(originalError instanceof AppError)) {
|
||||
return errorTemplate;
|
||||
}
|
||||
|
||||
if (originalError.headers && ctx) {
|
||||
for (const [headerKey, headerValue] of Object.entries(originalError.headers)) {
|
||||
ctx.res.headers.append(headerKey, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Default unknown errors to 400, since if you're throwing an AppError it is expected
|
||||
// that you already know what you're doing.
|
||||
if (originalError instanceof AppError) {
|
||||
if (originalError.headers && ctx) {
|
||||
for (const [headerKey, headerValue] of Object.entries(originalError.headers)) {
|
||||
ctx.res.headers.append(headerKey, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
...data,
|
||||
return {
|
||||
...errorTemplate,
|
||||
data: {
|
||||
...errorTemplate.data,
|
||||
appError: AppError.toJSON(originalError),
|
||||
code: originalError.code,
|
||||
httpStatus: originalError.statusCode ?? genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ?? 400,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...shape,
|
||||
data,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user