mirror of
https://github.com/documenso/documenso.git
synced 2025-11-27 14:59:10 +10:00
Compare commits
5 Commits
936d8d90b3
...
v2.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
378dd605b9 | ||
|
|
211ae6c9e9 | ||
|
|
f931885a95 | ||
|
|
4ade408001 | ||
|
|
3d0e3c6e8e |
@@ -76,8 +76,10 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
||||
|
||||
const payload = {
|
||||
title: file.name,
|
||||
timezone: userTimezone,
|
||||
folderId: folderId ?? undefined,
|
||||
meta: {
|
||||
timezone: userTimezone,
|
||||
},
|
||||
} satisfies TCreateDocumentPayloadSchema;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { ErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
|
||||
import {
|
||||
ErrorCode as DropzoneErrorCode,
|
||||
ErrorCode,
|
||||
type FileRejection,
|
||||
useDropzone,
|
||||
} from 'react-dropzone';
|
||||
import { Link, useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -16,21 +21,26 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export interface DocumentDropZoneWrapperProps {
|
||||
export interface EnvelopeDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
type: EnvelopeType;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZoneWrapperProps) => {
|
||||
const { _ } = useLingui();
|
||||
export const EnvelopeDropZoneWrapper = ({
|
||||
children,
|
||||
type,
|
||||
className,
|
||||
}: EnvelopeDropZoneWrapperProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
const { folderId } = useParams();
|
||||
@@ -47,13 +57,13 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
|
||||
DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
const { quota, remaining, refreshLimits } = useLimits();
|
||||
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||
const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation();
|
||||
|
||||
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
const onFileDrop = async (files: File[]) => {
|
||||
if (isUploadDisabled && IS_BILLING_ENABLED()) {
|
||||
await navigate(`/o/${organisation.url}/settings/billing`);
|
||||
return;
|
||||
@@ -63,51 +73,67 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
setIsLoading(true);
|
||||
|
||||
const payload = {
|
||||
title: file.name,
|
||||
timezone: userTimezone,
|
||||
folderId: folderId ?? undefined,
|
||||
} satisfies TCreateDocumentPayloadSchema;
|
||||
folderId,
|
||||
type,
|
||||
title: files[0].name,
|
||||
meta: {
|
||||
timezone: userTimezone,
|
||||
},
|
||||
} satisfies TCreateEnvelopePayload;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
formData.append('file', file);
|
||||
|
||||
const { envelopeId: id } = await createDocument(formData);
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
const { id } = await createEnvelope(formData);
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: _(msg`Document uploaded`),
|
||||
description: _(msg`Your document has been uploaded successfully.`),
|
||||
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`,
|
||||
description:
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? t`Your document has been uploaded successfully.`
|
||||
: t`Your template has been uploaded successfully.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
|
||||
const pathPrefix =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? formatDocumentsPath(team.url)
|
||||
: formatTemplatesPath(team.url);
|
||||
|
||||
await navigate(`${pathPrefix}/${id}/edit`);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||
() => msg`You have reached the limit of the number of files per envelope`,
|
||||
() => t`You have reached the limit of the number of files per envelope`,
|
||||
)
|
||||
.otherwise(() => msg`An error occurred while uploading your document.`);
|
||||
.otherwise(() => t`An error occurred during upload.`);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
title: t`Error`,
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
@@ -121,6 +147,20 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Since users can only upload only one file (no multi-upload), we only handle the first file rejection
|
||||
const { file, errors } = fileRejections[0];
|
||||
|
||||
@@ -155,7 +195,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
);
|
||||
|
||||
toast({
|
||||
title: _(msg`Upload failed`),
|
||||
title: t`Upload failed`,
|
||||
description,
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
@@ -165,17 +205,11 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
//disabled: isUploadDisabled,
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
onDrop: ([acceptedFile]) => {
|
||||
if (acceptedFile) {
|
||||
void onFileDrop(acceptedFile);
|
||||
}
|
||||
},
|
||||
onDropRejected: (fileRejections) => {
|
||||
onFileDropRejected(fileRejections);
|
||||
},
|
||||
maxFiles: maximumEnvelopeItemCount,
|
||||
onDrop: (files) => void onFileDrop(files),
|
||||
onDropRejected: onFileDropRejected,
|
||||
noClick: true,
|
||||
noDragEventsBubbling: true,
|
||||
});
|
||||
@@ -189,7 +223,11 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
|
||||
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
|
||||
<h2 className="text-foreground text-2xl font-semibold">
|
||||
<Trans>Upload Document</Trans>
|
||||
{type === EnvelopeType.DOCUMENT ? (
|
||||
<Trans>Upload Document</Trans>
|
||||
) : (
|
||||
<Trans>Upload Template</Trans>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground text-md mt-4">
|
||||
@@ -224,7 +262,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
|
||||
<Loader className="text-primary h-12 w-12 animate-spin" />
|
||||
<p className="text-foreground mt-8 font-medium">
|
||||
<Trans>Uploading document...</Trans>
|
||||
<Trans>Uploading</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@ import { DocumentUploadButtonLegacy } from '~/components/general/document/docume
|
||||
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeUploadButton } from '../document/envelope-upload-button';
|
||||
import { EnvelopeUploadButton } from '../envelope/envelope-upload-button';
|
||||
|
||||
export type FolderGridProps = {
|
||||
type: FolderType;
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { ErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export interface TemplateDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZoneWrapperProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { folderId } = useParams();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const payload = {
|
||||
title: file.name,
|
||||
folderId: folderId ?? undefined,
|
||||
} satisfies TCreateTemplatePayloadSchema;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
formData.append('file', file);
|
||||
|
||||
const { envelopeId: id } = await createTemplate(formData);
|
||||
|
||||
toast({
|
||||
title: _(msg`Template uploaded`),
|
||||
description: _(
|
||||
msg`Your template has been uploaded successfully. You will be redirected to the template page.`,
|
||||
),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`Please try again later.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileDropRejected = (fileRejections: FileRejection[]) => {
|
||||
if (!fileRejections.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since users can only upload only one file (no multi-upload), we only handle the first file rejection
|
||||
const { file, errors } = fileRejections[0];
|
||||
|
||||
if (!errors.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorNodes = errors.map((error, index) => (
|
||||
<span key={index} className="block">
|
||||
{match(error.code)
|
||||
.with(ErrorCode.FileTooLarge, () => (
|
||||
<Trans>File is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB</Trans>
|
||||
))
|
||||
.with(ErrorCode.FileInvalidType, () => <Trans>Only PDF files are allowed</Trans>)
|
||||
.with(ErrorCode.FileTooSmall, () => <Trans>File is too small</Trans>)
|
||||
.with(ErrorCode.TooManyFiles, () => (
|
||||
<Trans>Only one file can be uploaded at a time</Trans>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<Trans>Unknown error</Trans>
|
||||
))}
|
||||
</span>
|
||||
));
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<span className="font-medium">
|
||||
<Trans>{file.name} couldn't be uploaded:</Trans>
|
||||
</span>
|
||||
{errorNodes}
|
||||
</>
|
||||
);
|
||||
|
||||
toast({
|
||||
title: _(msg`Upload failed`),
|
||||
description,
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
//disabled: isUploadDisabled,
|
||||
multiple: false,
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
onDrop: ([acceptedFile]) => {
|
||||
if (acceptedFile) {
|
||||
void onFileDrop(acceptedFile);
|
||||
}
|
||||
},
|
||||
onDropRejected: (fileRejections) => {
|
||||
onFileDropRejected(fileRejections);
|
||||
},
|
||||
noClick: true,
|
||||
noDragEventsBubbling: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} className={cn('relative min-h-screen', className)}>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
|
||||
{isDragActive && (
|
||||
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
|
||||
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
|
||||
<h2 className="text-foreground text-2xl font-semibold">
|
||||
<Trans>Upload Template</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground text-md mt-4">
|
||||
<Trans>Drag and drop your PDF file here</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="bg-muted/30 absolute inset-0 z-50 backdrop-blur-[2px]">
|
||||
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
|
||||
<Loader className="text-primary h-12 w-12 animate-spin" />
|
||||
<p className="text-foreground mt-8 font-medium">
|
||||
<Trans>Uploading template...</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { FolderType, OrganisationType } from '@prisma/client';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
@@ -18,9 +19,9 @@ import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/av
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
|
||||
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
|
||||
import { DocumentSearch } from '~/components/general/document/document-search';
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
|
||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||
import { PeriodSelector } from '~/components/general/period-selector';
|
||||
import { DocumentsTable } from '~/components/tables/documents-table';
|
||||
@@ -108,9 +109,8 @@ export default function DocumentsPage() {
|
||||
}
|
||||
}, [data?.stats]);
|
||||
|
||||
// Todo: Envelopes - Change the dropzone wrapper to create to V2 documents after we're ready.
|
||||
return (
|
||||
<DocumentDropZoneWrapper>
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.DOCUMENT}>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<FolderGrid type={FolderType.DOCUMENT} parentId={folderId ?? null} />
|
||||
|
||||
@@ -210,6 +210,6 @@ export default function DocumentsPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DocumentDropZoneWrapper>
|
||||
</EnvelopeDropZoneWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Bird } from 'lucide-react';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
|
||||
@@ -8,8 +9,8 @@ import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/t
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
|
||||
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
|
||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||
import { TemplatesTable } from '~/components/tables/templates-table';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
@@ -37,7 +38,7 @@ export default function TemplatesPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<TemplateDropZoneWrapper>
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.TEMPLATE}>
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
|
||||
|
||||
@@ -85,6 +86,6 @@ export default function TemplatesPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TemplateDropZoneWrapper>
|
||||
</EnvelopeDropZoneWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,5 +106,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.0.6"
|
||||
"version": "2.0.7"
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ import { lingui } from '@lingui/vite-plugin';
|
||||
import { reactRouter } from '@react-router/dev/vite';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import serverAdapter from 'hono-react-router-adapter/vite';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, normalizePath } from 'vite';
|
||||
import macrosPlugin from 'vite-plugin-babel-macros';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
|
||||
const cMapsDir = normalizePath(path.join(pdfjsDistPath, 'cmaps'));
|
||||
|
||||
/**
|
||||
* Note: We load the env variables externally so we can have runtime enviroment variables
|
||||
* for docker.
|
||||
@@ -25,6 +32,14 @@ export default defineConfig({
|
||||
strictPort: true,
|
||||
},
|
||||
plugins: [
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: cMapsDir,
|
||||
dest: 'static',
|
||||
},
|
||||
],
|
||||
}),
|
||||
reactRouter(),
|
||||
macrosPlugin(),
|
||||
lingui(),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
FROM node:22-alpine3.20 AS base
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
RUN apk add --no-cache font-freefont
|
||||
|
||||
|
||||
###########################
|
||||
@@ -114,4 +115,4 @@ COPY --chown=nodejs:nodejs ./docker/start.sh /app/apps/remix/start.sh
|
||||
|
||||
WORKDIR /app/apps/remix
|
||||
|
||||
CMD ["sh", "start.sh"]
|
||||
CMD ["sh", "start.sh"]
|
||||
|
||||
102
package-lock.json
generated
102
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -52,6 +52,7 @@
|
||||
"trpc-to-openapi": "2.4.0",
|
||||
"turbo": "^1.9.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-static-copy": "^3.1.4",
|
||||
"zod-openapi": "^4.2.4",
|
||||
"zod-prisma-types": "3.3.5"
|
||||
},
|
||||
@@ -100,7 +101,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.2",
|
||||
"@documenso/api": "*",
|
||||
@@ -19112,10 +19113,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -33479,13 +33483,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -33495,9 +33499,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -35944,6 +35948,76 @@
|
||||
"vite": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz",
|
||||
"integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"p-map": "^7.0.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy/node_modules/p-map": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
|
||||
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-tsconfig-paths": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
@@ -45,6 +45,12 @@
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@lingui/cli": "^5.2.0",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@trpc/client": "11.7.0",
|
||||
"@trpc/react-query": "11.7.0",
|
||||
"@trpc/server": "11.7.0",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@ts-rest/open-api": "^3.52.1",
|
||||
"@ts-rest/serverless": "^3.52.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^8.40.0",
|
||||
@@ -59,18 +65,13 @@
|
||||
"prisma-json-types-generator": "^3.6.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"turbo": "^1.9.3",
|
||||
"@trpc/client": "11.7.0",
|
||||
"@trpc/react-query": "11.7.0",
|
||||
"@trpc/server": "11.7.0",
|
||||
"superjson": "^2.2.5",
|
||||
"trpc-to-openapi": "2.4.0",
|
||||
"turbo": "^1.9.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-static-copy": "^3.1.4",
|
||||
"zod-openapi": "^4.2.4",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@ts-rest/open-api": "^3.52.1",
|
||||
"@ts-rest/serverless": "^3.52.1",
|
||||
"zod-prisma-types": "3.3.5",
|
||||
"vite": "^6.3.5"
|
||||
"zod-prisma-types": "3.3.5"
|
||||
},
|
||||
"name": "@documenso/root",
|
||||
"workspaces": [
|
||||
|
||||
@@ -9,6 +9,7 @@ import { seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
@@ -81,20 +82,23 @@ test('[TEAMS]: can create a document inside a document folder', async ({ page })
|
||||
redirectPath: `/t/${team.url}/documents/f/${teamFolder.id}`,
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').nth(2);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
// Upload document.
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.getByRole('button', { name: 'Document (Legacy)' }).click(),
|
||||
]);
|
||||
|
||||
await fileInput.setInputFiles(
|
||||
await fileChooser.setFiles(
|
||||
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
|
||||
);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
|
||||
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf');
|
||||
|
||||
await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`);
|
||||
|
||||
await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
|
||||
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf');
|
||||
});
|
||||
|
||||
test('[TEAMS]: can pin a document folder', async ({ page }) => {
|
||||
@@ -382,11 +386,11 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Expect redirect.
|
||||
await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
|
||||
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf');
|
||||
|
||||
// Return to folder and verify file is visible.
|
||||
await page.goto(`/t/${team.url}/templates/f/${folder.id}`);
|
||||
await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
|
||||
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf');
|
||||
});
|
||||
|
||||
test('[TEAMS]: can pin a template folder', async ({ page }) => {
|
||||
@@ -851,7 +855,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => {
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
|
||||
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf');
|
||||
|
||||
await expect(page.getByRole('combobox').filter({ hasText: 'Admins only' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Konva from 'konva';
|
||||
// sort-imports-ignore
|
||||
import 'konva/skia-backend';
|
||||
|
||||
import Konva from 'konva';
|
||||
import path from 'node:path';
|
||||
import type { Canvas } from 'skia-canvas';
|
||||
import { FontLibrary } from 'skia-canvas';
|
||||
@@ -21,13 +23,13 @@ export const insertFieldInPDFV2 = async ({
|
||||
}: InsertFieldInPDFV2Options) => {
|
||||
const fontPath = path.join(process.cwd(), 'public/fonts');
|
||||
|
||||
FontLibrary.use([
|
||||
path.join(fontPath, 'caveat.ttf'),
|
||||
path.join(fontPath, 'noto-sans.ttf'),
|
||||
path.join(fontPath, 'noto-sans-japanese.ttf'),
|
||||
path.join(fontPath, 'noto-sans-chinese.ttf'),
|
||||
path.join(fontPath, 'noto-sans-korean.ttf'),
|
||||
]);
|
||||
FontLibrary.use({
|
||||
['Caveat']: [path.join(fontPath, 'caveat.ttf')],
|
||||
['Noto Sans']: [path.join(fontPath, 'noto-sans.ttf')],
|
||||
['Noto Sans Japanese']: [path.join(fontPath, 'noto-sans-japanese.ttf')],
|
||||
['Noto Sans Chinese']: [path.join(fontPath, 'noto-sans-chinese.ttf')],
|
||||
['Noto Sans Korean']: [path.join(fontPath, 'noto-sans-korean.ttf')],
|
||||
});
|
||||
|
||||
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||
const layer = new Konva.Layer();
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
export const konvaTextFontFamily =
|
||||
'Noto Sans, Noto Sans Japanese, Noto Sans Chinese, Noto Sans Korean, sans-serif';
|
||||
'"Noto Sans", "Noto Sans Japanese", "Noto Sans Chinese", "Noto Sans Korean", sans-serif';
|
||||
export const konvaTextFill = 'black';
|
||||
|
||||
export const upsertFieldGroup = (
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateDocumentFormDataRequestSchema,
|
||||
ZCreateDocumentFormDataResponseSchema,
|
||||
createDocumentFormDataMeta,
|
||||
} from './create-document-formdata.types';
|
||||
|
||||
/**
|
||||
* Temporary endpoint for V2 Beta until we allow passthrough documents on create.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createDocumentFormDataRoute = authenticatedProcedure
|
||||
.meta(createDocumentFormDataMeta)
|
||||
.input(ZCreateDocumentFormDataRequestSchema)
|
||||
.output(ZCreateDocumentFormDataResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
|
||||
const { payload, file } = input;
|
||||
|
||||
const {
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients,
|
||||
meta,
|
||||
folderId,
|
||||
attachments,
|
||||
} = payload;
|
||||
|
||||
const { remaining } = await getServerLimits({ userId: user.id, teamId });
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = await putPdfFileServerSide(file);
|
||||
|
||||
const createdEnvelope = await createEnvelope({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
normalizePdf: false, // Not normalizing because of presigned URL.
|
||||
internalVersion: 1,
|
||||
data: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients: (recipients || []).map((recipient) => ({
|
||||
...recipient,
|
||||
fields: (recipient.fields || []).map((field) => ({
|
||||
...field,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
documentDataId: documentData.id,
|
||||
})),
|
||||
})),
|
||||
folderId,
|
||||
envelopeItems: [
|
||||
{
|
||||
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
|
||||
documentDataId: documentData.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
attachments,
|
||||
meta: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings ?? undefined,
|
||||
},
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
const envelopeItems = await prisma.envelopeItem.findMany({
|
||||
where: {
|
||||
envelopeId: createdEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(createdEnvelope.secondaryId);
|
||||
|
||||
const firstDocumentData = envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Document data not found');
|
||||
}
|
||||
|
||||
return {
|
||||
document: {
|
||||
...createdEnvelope,
|
||||
envelopeId: createdEnvelope.id,
|
||||
documentDataId: firstDocumentData.id,
|
||||
documentData: {
|
||||
...firstDocumentData,
|
||||
envelopeItemId: envelopeItems[0].id,
|
||||
},
|
||||
documentMeta: {
|
||||
...createdEnvelope.documentMeta,
|
||||
documentId: legacyDocumentId,
|
||||
},
|
||||
id: legacyDocumentId,
|
||||
fields: createdEnvelope.fields.map((field) => ({
|
||||
...field,
|
||||
documentId: legacyDocumentId,
|
||||
templateId: null,
|
||||
})),
|
||||
recipients: createdEnvelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
documentId: legacyDocumentId,
|
||||
templateId: null,
|
||||
})),
|
||||
},
|
||||
folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release.
|
||||
};
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentTitleSchema,
|
||||
ZDocumentVisibilitySchema,
|
||||
} from './schema';
|
||||
|
||||
export const createDocumentFormDataMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/create/formdata',
|
||||
contentTypes: ['multipart/form-data'],
|
||||
summary: 'Create document',
|
||||
description: 'Create a document using form data.',
|
||||
tags: ['Document'],
|
||||
},
|
||||
};
|
||||
|
||||
const ZCreateDocumentFormDataPayloadRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
formValues: ZDocumentFormValuesSchema.optional(),
|
||||
folderId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||
)
|
||||
.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
ZCreateRecipientSchema.extend({
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
}),
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
.optional(),
|
||||
attachments: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().min(1, 'Label is required'),
|
||||
data: z.string().url('Must be a valid URL'),
|
||||
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
meta: ZDocumentMetaCreateSchema.optional(),
|
||||
});
|
||||
|
||||
// !: Can't use zfd.formData() here because it receives `undefined`
|
||||
// !: somewhere in the pipeline of our openapi schema generation and throws
|
||||
// !: an error.
|
||||
export const ZCreateDocumentFormDataRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateDocumentFormDataPayloadRequestSchema),
|
||||
file: zfd.file(),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentFormDataResponseSchema = z.object({
|
||||
document: ZDocumentSchema,
|
||||
});
|
||||
|
||||
export type TCreateDocumentFormDataRequest = z.infer<typeof ZCreateDocumentFormDataRequestSchema>;
|
||||
export type TCreateDocumentFormDataResponse = z.infer<typeof ZCreateDocumentFormDataResponseSchema>;
|
||||
@@ -3,6 +3,7 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
|
||||
@@ -22,9 +23,34 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
|
||||
const { payload, file } = input;
|
||||
|
||||
const { title, timezone, folderId, attachments } = payload;
|
||||
const {
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients,
|
||||
meta,
|
||||
folderId,
|
||||
formValues,
|
||||
attachments,
|
||||
} = payload;
|
||||
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdf),
|
||||
});
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
@@ -48,7 +74,20 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
data: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title,
|
||||
userTimezone: timezone,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients: (recipients || []).map((recipient) => ({
|
||||
...recipient,
|
||||
fields: (recipient.fields || []).map((field) => ({
|
||||
...field,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
documentDataId,
|
||||
})),
|
||||
})),
|
||||
folderId,
|
||||
envelopeItems: [
|
||||
{
|
||||
@@ -58,7 +97,10 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
],
|
||||
},
|
||||
attachments,
|
||||
normalizePdf: true,
|
||||
meta: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings ?? undefined,
|
||||
},
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
|
||||
import { ZDocumentVisibilitySchema } from '@documenso/lib/types/document-visibility';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import { ZDocumentTitleSchema } from './schema';
|
||||
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from './schema';
|
||||
|
||||
export const createDocumentMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
@@ -21,8 +36,35 @@ export const createDocumentMeta: TrpcRouteMeta = {
|
||||
|
||||
export const ZCreateDocumentPayloadSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
formValues: ZDocumentFormValuesSchema.optional(),
|
||||
folderId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||
)
|
||||
.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
ZCreateRecipientSchema.extend({
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
}),
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
.optional(),
|
||||
attachments: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -32,6 +74,7 @@ export const ZCreateDocumentPayloadSchema = z.object({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
meta: ZDocumentMetaCreateSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentRequestSchema = zodFormData({
|
||||
|
||||
@@ -5,7 +5,6 @@ import { deleteAttachmentRoute } from './attachment/delete-attachment';
|
||||
import { findAttachmentsRoute } from './attachment/find-attachments';
|
||||
import { updateAttachmentRoute } from './attachment/update-attachment';
|
||||
import { createDocumentRoute } from './create-document';
|
||||
import { createDocumentFormDataRoute } from './create-document-formdata';
|
||||
import { createDocumentTemporaryRoute } from './create-document-temporary';
|
||||
import { deleteDocumentRoute } from './delete-document';
|
||||
import { distributeDocumentRoute } from './distribute-document';
|
||||
@@ -39,11 +38,11 @@ export const documentRouter = router({
|
||||
search: searchDocumentRoute,
|
||||
share: shareDocumentRoute,
|
||||
|
||||
// Temporary v2 beta routes to be removed once V2 is fully released.
|
||||
downloadBeta: downloadDocumentBetaRoute,
|
||||
download: downloadDocumentRoute,
|
||||
|
||||
// Deprecated endpoints which need to be removed in the future.
|
||||
downloadBeta: downloadDocumentBetaRoute,
|
||||
createDocumentTemporary: createDocumentTemporaryRoute,
|
||||
createDocumentFormData: createDocumentFormDataRoute,
|
||||
|
||||
// Internal document routes for custom frontend requests.
|
||||
getDocumentByToken: getDocumentByTokenRoute,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
@@ -58,10 +59,31 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'You cannot upload non-PDF files',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// For each file, stream to s3 and create the document data.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
|
||||
@@ -22,6 +23,10 @@ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps`,
|
||||
};
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
@@ -167,6 +172,7 @@ export const PdfViewerKonva = ({
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
// options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
|
||||
@@ -8,9 +8,10 @@ import { base64 } from '@scure/base';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
// import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
// import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
|
||||
@@ -27,6 +28,10 @@ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps`,
|
||||
};
|
||||
|
||||
export type OnPDFViewerPageClick = (_event: {
|
||||
pageNumber: number;
|
||||
numPages: number;
|
||||
@@ -234,6 +239,7 @@ export const PDFViewer = ({
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
// options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
|
||||
Reference in New Issue
Block a user