mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +10:00
fix: wip
This commit is contained in:
@@ -9,6 +9,7 @@ export type EnvelopeItemTitleInputProps = {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
dataTestId?: string;
|
||||
};
|
||||
|
||||
export const EnvelopeItemTitleInput = ({
|
||||
@@ -17,6 +18,7 @@ export const EnvelopeItemTitleInput = ({
|
||||
className,
|
||||
placeholder,
|
||||
disabled,
|
||||
dataTestId,
|
||||
}: EnvelopeItemTitleInputProps) => {
|
||||
const [envelopeItemTitle, setEnvelopeItemTitle] = useState(value);
|
||||
const [isError, setIsError] = useState(false);
|
||||
@@ -63,6 +65,7 @@ export const EnvelopeItemTitleInput = ({
|
||||
{envelopeItemTitle || placeholder}
|
||||
</span>
|
||||
<input
|
||||
data-testid={dataTestId}
|
||||
data-1p-ignore
|
||||
autoComplete="off"
|
||||
ref={inputRef}
|
||||
@@ -72,7 +75,7 @@ export const EnvelopeItemTitleInput = ({
|
||||
disabled={disabled}
|
||||
style={{ width: `${inputWidth}px` }}
|
||||
className={cn(
|
||||
'text-foreground hover:outline-muted-foreground focus:outline-muted-foreground rounded-sm border-0 bg-transparent p-1 text-sm font-medium outline-none hover:outline hover:outline-1 focus:outline focus:outline-1',
|
||||
'rounded-sm border-0 bg-transparent p-1 text-sm font-medium text-foreground outline-none hover:outline hover:outline-1 hover:outline-muted-foreground focus:outline focus:outline-1 focus:outline-muted-foreground',
|
||||
className,
|
||||
{
|
||||
'outline-red-500': isError,
|
||||
|
||||
@@ -108,7 +108,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
);
|
||||
|
||||
const onFileDrop = async (files: File[]) => {
|
||||
const newUploadingFiles: (LocalFile & { file: File; data: ArrayBuffer | null })[] =
|
||||
const newUploadingFiles: (LocalFile & { file: File; data: Uint8Array<ArrayBuffer> | null })[] =
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
return {
|
||||
@@ -118,7 +118,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
file,
|
||||
isUploading: isEmbedded ? false : true,
|
||||
// Clone the buffer so it can be read multiple times (File.arrayBuffer() consumes the stream once)
|
||||
data: isEmbedded ? (await file.arrayBuffer()).slice(0) : null,
|
||||
data: isEmbedded ? new Uint8Array((await file.arrayBuffer()).slice(0)) : null,
|
||||
isError: false,
|
||||
};
|
||||
}),
|
||||
@@ -226,6 +226,30 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
};
|
||||
|
||||
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
|
||||
if (isEmbedded) {
|
||||
const nextEnvelopeItems = files
|
||||
.filter((item) => item.envelopeItemId)
|
||||
.map((item, index) => {
|
||||
const originalEnvelopeItem = envelope.envelopeItems.find(
|
||||
(envelopeItem) => envelopeItem.id === item.envelopeItemId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: item.envelopeItemId || '',
|
||||
title: item.title,
|
||||
order: index + 1,
|
||||
envelopeId: envelope.id,
|
||||
data: originalEnvelopeItem?.data,
|
||||
};
|
||||
});
|
||||
|
||||
setLocalEnvelope({
|
||||
envelopeItems: nextEnvelopeItems,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void updateEnvelopeItems({
|
||||
envelopeId: envelope.id,
|
||||
data: files
|
||||
@@ -310,6 +334,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
<CardContent>
|
||||
{uploadConfig?.allowUpload && (
|
||||
<DocumentDropzone
|
||||
data-testid="envelope-item-dropzone"
|
||||
onDrop={onFileDrop}
|
||||
allowMultiple
|
||||
className="pb-4 pt-6"
|
||||
@@ -326,7 +351,12 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="files">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} className="space-y-2">
|
||||
<div
|
||||
data-testid="envelope-items-list"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className="space-y-2"
|
||||
>
|
||||
{localFiles.map((localFile, index) => (
|
||||
<Draggable
|
||||
key={localFile.id}
|
||||
@@ -340,6 +370,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
data-testid={`envelope-item-row-${localFile.id}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={provided.draggableProps.style}
|
||||
@@ -351,6 +382,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
{uploadConfig?.allowConfigureOrder && (
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
data-testid={`envelope-item-drag-handle-${localFile.id}`}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<GripVerticalIcon className="h-5 w-5 flex-shrink-0 opacity-40" />
|
||||
@@ -365,6 +397,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
!uploadConfig?.allowConfigureTitle
|
||||
}
|
||||
value={localFile.title}
|
||||
dataTestId={`envelope-item-title-input-${localFile.id}`}
|
||||
placeholder={t`Document Title`}
|
||||
onChange={(title) => {
|
||||
onEnvelopeItemTitleChange(localFile.envelopeItemId!, title);
|
||||
@@ -399,7 +432,17 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
|
||||
{!localFile.isUploading &&
|
||||
localFile.envelopeItemId &&
|
||||
uploadConfig?.allowDelete && (
|
||||
uploadConfig?.allowDelete &&
|
||||
(isEmbedded ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-testid={`envelope-item-remove-button-${localFile.id}`}
|
||||
onClick={() => onFileDelete(localFile.envelopeItemId!)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<EnvelopeItemDeleteDialog
|
||||
canItemBeDeleted={canItemsBeModified}
|
||||
envelopeId={envelope.id}
|
||||
@@ -407,12 +450,16 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
envelopeItemTitle={localFile.title}
|
||||
onDelete={onFileDelete}
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-testid={`envelope-item-remove-button-${localFile.id}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
ReadStatus,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
import { CheckCircle2Icon } from 'lucide-react';
|
||||
|
||||
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
|
||||
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
|
||||
import {
|
||||
type TEmbedCreateEnvelopeAuthoring,
|
||||
ZEmbedCreateEnvelopeAuthoringSchema,
|
||||
} from '@documenso/lib/types/envelope-editor';
|
||||
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import { extractDerivedDocumentMeta } from '@documenso/lib/utils/document';
|
||||
import { buildEmbeddedFeatures } from '@documenso/lib/utils/embed-config';
|
||||
import { buildEmbeddedEditorOptions } from '@documenso/lib/utils/embed-config';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import { Spinner } from '@documenso/ui/primitives/spinner';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { EnvelopeEditor } from '~/components/general/envelope-editor/envelope-editor';
|
||||
import { EnvelopeEditorRenderProviderWrapper } from '~/components/general/envelope-editor/envelope-editor-renderer-provider-wrapper';
|
||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/envelope.create._index';
|
||||
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// We know that the token is present because we're checking it in the parent _layout route
|
||||
const token = url.searchParams.get('token') || '';
|
||||
|
||||
if (!token) {
|
||||
throw new Response('Invalid token', { status: 404 });
|
||||
}
|
||||
|
||||
// We also know that the token is valid, but we need the userId + teamId
|
||||
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
||||
|
||||
if (!result) {
|
||||
throw new Response('Invalid token', { status: 404 });
|
||||
}
|
||||
|
||||
const teamSettings = await getTeamSettings({
|
||||
userId: result.userId,
|
||||
teamId: result.teamId,
|
||||
});
|
||||
|
||||
return superLoaderJson({
|
||||
token,
|
||||
tokenUserId: result.userId,
|
||||
tokenTeamId: result.teamId,
|
||||
teamSettings,
|
||||
});
|
||||
};
|
||||
|
||||
export default function EmbeddingAuthoringEnvelopeCreatePage() {
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [embedAuthoringOptions, setEmbedAuthoringOptions] =
|
||||
useState<TEmbedCreateEnvelopeAuthoring | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
if (hash) {
|
||||
const result = ZEmbedCreateEnvelopeAuthoringSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setEmbedAuthoringOptions({
|
||||
...result.data,
|
||||
features: buildEmbeddedFeatures(result.data.features),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
|
||||
setHasInitialized(true);
|
||||
}, []);
|
||||
|
||||
if (!hasInitialized) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!embedAuthoringOptions) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Trans>Invalid Embedding Parameters</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <EnvelopeCreatePage embedAuthoringOptions={embedAuthoringOptions} />;
|
||||
}
|
||||
|
||||
type EnvelopeCreatePageProps = {
|
||||
embedAuthoringOptions: TEmbedCreateEnvelopeAuthoring;
|
||||
};
|
||||
|
||||
const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) => {
|
||||
const { token, tokenUserId, tokenTeamId, teamSettings } = useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isCreatingEnvelope, setIsCreatingEnvelope] = useState(false);
|
||||
const [createdEnvelope, setCreatedEnvelope] = useState<{ id: string } | null>(null);
|
||||
|
||||
const { mutateAsync: createEmbeddingEnvelope } =
|
||||
trpc.embeddingPresign.createEmbeddingEnvelope.useMutation();
|
||||
|
||||
const buildCreateEnvelopeRequest = (
|
||||
envelope: Omit<TEditorEnvelope, 'id'>,
|
||||
): { payload: TCreateEnvelopePayload; files: File[] } => {
|
||||
const sortedItems = [...envelope.envelopeItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
const itemIdToIndex = new Map<string, number>();
|
||||
|
||||
sortedItems.forEach((item, index) => {
|
||||
itemIdToIndex.set(String(item.id), index);
|
||||
});
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
for (const item of sortedItems) {
|
||||
if (!item.data) {
|
||||
throw new Error(`Envelope item "${item.title ?? item.id}" has no PDF data`);
|
||||
}
|
||||
|
||||
files.push(
|
||||
new File(
|
||||
[item.data],
|
||||
item.title?.endsWith('.pdf') ? item.title : `${item.title ?? 'document'}.pdf`,
|
||||
{
|
||||
type: 'application/pdf',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const recipients = envelope.recipients.map((recipient) => {
|
||||
const recipientFields = envelope.fields.filter((f) => f.recipientId === recipient.id);
|
||||
|
||||
const fields = recipientFields.map((field) => {
|
||||
return {
|
||||
identifier: itemIdToIndex.get(String(field.envelopeItemId)),
|
||||
page: field.page,
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
...({
|
||||
type: field.type,
|
||||
fieldMeta: field.fieldMeta ?? undefined,
|
||||
} as TEnvelopeFieldAndMeta),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder ?? undefined,
|
||||
accessAuth: recipient.authOptions?.accessAuth ?? [],
|
||||
actionAuth: recipient.authOptions?.actionAuth ?? [],
|
||||
fields,
|
||||
};
|
||||
});
|
||||
|
||||
const payload: TCreateEnvelopePayload = {
|
||||
title: envelope.title,
|
||||
type: envelope.type,
|
||||
externalId: envelope.externalId ?? undefined,
|
||||
visibility: envelope.visibility,
|
||||
globalAccessAuth: envelope.authOptions?.globalAccessAuth?.length
|
||||
? envelope.authOptions?.globalAccessAuth
|
||||
: undefined,
|
||||
globalActionAuth: envelope.authOptions?.globalActionAuth?.length
|
||||
? envelope.authOptions?.globalActionAuth
|
||||
: undefined,
|
||||
folderId: envelope.folderId ?? undefined,
|
||||
recipients,
|
||||
meta: {
|
||||
subject: envelope.documentMeta.subject ?? undefined,
|
||||
message: envelope.documentMeta.message ?? undefined,
|
||||
timezone: envelope.documentMeta.timezone ?? undefined,
|
||||
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
|
||||
distributionMethod: envelope.documentMeta.distributionMethod ?? undefined,
|
||||
signingOrder: envelope.documentMeta.signingOrder ?? undefined,
|
||||
allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner ?? undefined,
|
||||
redirectUrl: envelope.documentMeta.redirectUrl ?? undefined,
|
||||
language: envelope.documentMeta.language as SupportedLanguageCodes,
|
||||
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled ?? undefined,
|
||||
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled ?? undefined,
|
||||
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined,
|
||||
emailId: envelope.documentMeta.emailId ?? undefined,
|
||||
emailReplyTo: envelope.documentMeta.emailReplyTo ?? undefined,
|
||||
emailSettings: envelope.documentMeta.emailSettings ?? undefined,
|
||||
},
|
||||
};
|
||||
|
||||
return { payload, files };
|
||||
};
|
||||
|
||||
const createEmbeddedEnvelope = async (envelopeWithoutId: Omit<TEditorEnvelope, 'id'>) => {
|
||||
setIsCreatingEnvelope(true);
|
||||
|
||||
if (isCreatingEnvelope) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload, files } = buildCreateEnvelopeRequest(envelopeWithoutId);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
const { id } = await createEmbeddingEnvelope(formData);
|
||||
|
||||
// Send a message to the parent window with the document details
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'envelope-created',
|
||||
envelopeId: id,
|
||||
externalId: envelopeWithoutId.externalId,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setCreatedEnvelope({ id });
|
||||
} catch (err) {
|
||||
console.error('Failed to create envelope:', err);
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t`Error`,
|
||||
description: t`Failed to create document. Please try again.`,
|
||||
});
|
||||
}
|
||||
|
||||
setIsCreatingEnvelope(false);
|
||||
};
|
||||
|
||||
const embeded = useMemo(
|
||||
() => ({
|
||||
presignToken: token,
|
||||
mode: 'create' as const,
|
||||
onCreate: async (envelope: Omit<TEditorEnvelope, 'id'>) => createEmbeddedEnvelope(envelope),
|
||||
customBrandingLogo: Boolean(teamSettings.brandingEnabled && teamSettings.brandingLogo),
|
||||
}),
|
||||
[token],
|
||||
);
|
||||
|
||||
const editorConfig = useMemo(() => {
|
||||
return buildEmbeddedEditorOptions(embedAuthoringOptions.features, embeded);
|
||||
}, [embedAuthoringOptions.features, embeded]);
|
||||
|
||||
const initialEnvelope = useMemo((): TEditorEnvelope => {
|
||||
const defaultDocumentMeta = extractDerivedDocumentMeta(teamSettings, undefined);
|
||||
|
||||
const defaultRecipients = teamSettings.defaultRecipients
|
||||
? ZDefaultRecipientsSchema.parse(teamSettings.defaultRecipients)
|
||||
: [];
|
||||
|
||||
const recipients: TEditorEnvelope['recipients'] = defaultRecipients.map((recipient, index) => ({
|
||||
id: -(index + 1),
|
||||
envelopeId: '',
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
token: '',
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: {
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
signingOrder: index + 1,
|
||||
rejectionReason: null,
|
||||
}));
|
||||
|
||||
const type = embedAuthoringOptions.type;
|
||||
|
||||
return {
|
||||
id: '',
|
||||
secondaryId: '',
|
||||
internalVersion: 2,
|
||||
type,
|
||||
status: DocumentStatus.DRAFT,
|
||||
source: 'DOCUMENT',
|
||||
visibility: teamSettings.documentVisibility,
|
||||
templateType: 'PRIVATE',
|
||||
completedAt: null,
|
||||
deletedAt: null,
|
||||
title: type === EnvelopeType.DOCUMENT ? 'Document Title' : 'Template Title',
|
||||
authOptions: {
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: [],
|
||||
},
|
||||
publicTitle: '',
|
||||
publicDescription: '',
|
||||
userId: tokenUserId,
|
||||
teamId: tokenTeamId,
|
||||
folderId: null,
|
||||
documentMeta: {
|
||||
id: '',
|
||||
...defaultDocumentMeta,
|
||||
},
|
||||
recipients,
|
||||
fields: [],
|
||||
envelopeItems: [],
|
||||
directLink: null,
|
||||
team: {
|
||||
id: tokenTeamId,
|
||||
url: '',
|
||||
},
|
||||
user: {
|
||||
id: tokenUserId,
|
||||
name: '',
|
||||
email: '',
|
||||
},
|
||||
externalId: embedAuthoringOptions?.externalId ?? null,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-w-screen relative min-h-screen">
|
||||
{isCreatingEnvelope && (
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
|
||||
<Spinner />
|
||||
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{initialEnvelope.type === EnvelopeType.DOCUMENT ? (
|
||||
<Trans>Creating Document</Trans>
|
||||
) : (
|
||||
<Trans>Creating Template</Trans>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdEnvelope && (
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
|
||||
<div className="mx-auto w-full max-w-md text-center">
|
||||
<CheckCircle2Icon className="mx-auto h-16 w-16 text-primary" />
|
||||
|
||||
<h1 className="mt-6 text-2xl font-bold">
|
||||
{initialEnvelope.type === EnvelopeType.TEMPLATE ? (
|
||||
<Trans>Template Created</Trans>
|
||||
) : (
|
||||
<Trans>Document Created</Trans>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{initialEnvelope.type === EnvelopeType.TEMPLATE ? (
|
||||
<Trans>Your template has been created successfully</Trans>
|
||||
) : (
|
||||
<Trans>Your document has been created successfully</Trans>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EnvelopeEditorProvider initialEnvelope={initialEnvelope} editorConfig={editorConfig}>
|
||||
<EnvelopeEditorRenderProviderWrapper presignedToken={token}>
|
||||
<EnvelopeEditor />
|
||||
</EnvelopeEditorRenderProviderWrapper>
|
||||
</EnvelopeEditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,353 @@
|
||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { CheckCircle2Icon } from 'lucide-react';
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
|
||||
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
|
||||
import {
|
||||
type TEmbedEditEnvelopeAuthoring,
|
||||
ZEmbedEditEnvelopeAuthoringSchema,
|
||||
} from '@documenso/lib/types/envelope-editor';
|
||||
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import { buildEmbeddedEditorOptions } from '@documenso/lib/utils/embed-config';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TUpdateEmbeddingEnvelopePayload } from '@documenso/trpc/server/embedding-router/update-embedding-envelope.types';
|
||||
import { Spinner } from '@documenso/ui/primitives/spinner';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { EnvelopeEditor } from '~/components/general/envelope-editor/envelope-editor';
|
||||
import { EnvelopeEditorRenderProviderWrapper } from '~/components/general/envelope-editor/envelope-editor-renderer-provider-wrapper';
|
||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/envelope.edit.$id';
|
||||
|
||||
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const { id } = params;
|
||||
|
||||
if (!id || !id.startsWith('envelope_')) {
|
||||
throw redirect(`/embed/v2/authoring/error/not-found`);
|
||||
}
|
||||
|
||||
// We know that the token is present because we're checking it in the parent _layout route
|
||||
const token = url.searchParams.get('token') || '';
|
||||
|
||||
if (!token) {
|
||||
throw new Response('Invalid token', { status: 404 });
|
||||
}
|
||||
|
||||
// We also know that the token is valid, but we need the userId + teamId
|
||||
const result = await verifyEmbeddingPresignToken({ token, scope: `envelope:${id}` }).catch(
|
||||
() => null,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: result.userId,
|
||||
teamId: result.teamId,
|
||||
});
|
||||
|
||||
const envelope = await getEnvelopeById({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id,
|
||||
},
|
||||
type: null,
|
||||
userId: result.userId,
|
||||
teamId: result.teamId,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!envelope) {
|
||||
throw redirect(`/embed/v2/authoring/error/not-found`);
|
||||
}
|
||||
|
||||
let brandingLogo: string | undefined = undefined;
|
||||
|
||||
if (settings.brandingEnabled && settings.brandingLogo) {
|
||||
brandingLogo = settings.brandingLogo;
|
||||
}
|
||||
|
||||
return superLoaderJson({
|
||||
token,
|
||||
envelope,
|
||||
brandingLogo,
|
||||
});
|
||||
};
|
||||
|
||||
export default function EmbeddingAuthoringEnvelopeEditPage() {
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [embedAuthoringOptions, setEmbedAuthoringOptions] =
|
||||
useState<TEmbedEditEnvelopeAuthoring | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
if (hash) {
|
||||
const result = ZEmbedEditEnvelopeAuthoringSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setEmbedAuthoringOptions(result.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
|
||||
setHasInitialized(true);
|
||||
}, []);
|
||||
|
||||
if (!hasInitialized) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!embedAuthoringOptions) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Trans>Invalid Embedding Parameters</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <EnvelopeEditPage embedAuthoringOptions={embedAuthoringOptions} />;
|
||||
}
|
||||
|
||||
type EnvelopeEditPageProps = {
|
||||
embedAuthoringOptions: TEmbedEditEnvelopeAuthoring;
|
||||
};
|
||||
|
||||
const EnvelopeEditPage = ({ embedAuthoringOptions }: EnvelopeEditPageProps) => {
|
||||
const { envelope, token, brandingLogo } = useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isUpdatingEnvelope, setIsUpdatingEnvelope] = useState(false);
|
||||
const [updatedEnvelope, setUpdatedEnvelope] = useState<{ id: string } | null>(null);
|
||||
|
||||
const { mutateAsync: updateEmbeddingEnvelope } =
|
||||
trpc.embeddingPresign.updateEmbeddingEnvelope.useMutation();
|
||||
|
||||
const buildUpdateEnvelopeRequest = (
|
||||
envelope: TEditorEnvelope,
|
||||
): { payload: TUpdateEmbeddingEnvelopePayload; files: File[] } => {
|
||||
const files: File[] = [];
|
||||
|
||||
const envelopeItems = envelope.envelopeItems.map((item) => {
|
||||
// Attach any new envelope item files to the request.
|
||||
if (item.data) {
|
||||
files.push(
|
||||
new File(
|
||||
[item.data],
|
||||
item.title?.endsWith('.pdf') ? item.title : `${item.title ?? 'document'}.pdf`,
|
||||
{
|
||||
type: 'application/pdf',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
index: item.data ? files.length - 1 : undefined, // Todo: Embed why not use the ID instead?
|
||||
};
|
||||
});
|
||||
|
||||
const recipients = envelope.recipients.map((recipient) => {
|
||||
const recipientFields = envelope.fields.filter((f) => f.recipientId === recipient.id);
|
||||
|
||||
const fields = recipientFields.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),
|
||||
...({
|
||||
type: field.type,
|
||||
fieldMeta: field.fieldMeta ?? undefined,
|
||||
} as TEnvelopeFieldAndMeta),
|
||||
}));
|
||||
|
||||
return {
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder ?? undefined,
|
||||
accessAuth: recipient.authOptions?.accessAuth ?? [],
|
||||
actionAuth: recipient.authOptions?.actionAuth ?? [],
|
||||
fields,
|
||||
};
|
||||
});
|
||||
|
||||
const payload: TUpdateEmbeddingEnvelopePayload = {
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
title: envelope.title,
|
||||
externalId: envelope.externalId,
|
||||
visibility: envelope.visibility,
|
||||
globalAccessAuth: envelope.authOptions?.globalAccessAuth,
|
||||
globalActionAuth: envelope.authOptions?.globalActionAuth,
|
||||
folderId: envelope.folderId,
|
||||
recipients,
|
||||
envelopeItems,
|
||||
},
|
||||
meta: {
|
||||
subject: envelope.documentMeta.subject ?? undefined,
|
||||
message: envelope.documentMeta.message ?? undefined,
|
||||
timezone: envelope.documentMeta.timezone ?? undefined,
|
||||
distributionMethod: envelope.documentMeta.distributionMethod ?? undefined,
|
||||
signingOrder: envelope.documentMeta.signingOrder ?? undefined,
|
||||
allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner ?? undefined,
|
||||
redirectUrl: envelope.documentMeta.redirectUrl ?? undefined,
|
||||
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled ?? undefined,
|
||||
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled ?? undefined,
|
||||
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined,
|
||||
emailId: envelope.documentMeta.emailId ?? undefined,
|
||||
emailReplyTo: envelope.documentMeta.emailReplyTo ?? undefined,
|
||||
emailSettings: envelope.documentMeta.emailSettings ?? undefined,
|
||||
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
|
||||
language: envelope.documentMeta.language as SupportedLanguageCodes,
|
||||
},
|
||||
};
|
||||
|
||||
return { payload, files };
|
||||
};
|
||||
|
||||
const updateEmbeddedEnvelope = async (envelope: TEditorEnvelope) => {
|
||||
setIsUpdatingEnvelope(true);
|
||||
|
||||
if (isUpdatingEnvelope) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload, files } = buildUpdateEnvelopeRequest(envelope);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
await updateEmbeddingEnvelope(formData);
|
||||
|
||||
// Send a message to the parent window with the document details
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'envelope-updated',
|
||||
envelopeId: envelope.id,
|
||||
externalId: envelope.externalId || null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to the completion page.
|
||||
setUpdatedEnvelope({ id: envelope.id });
|
||||
} catch (err) {
|
||||
console.error('Failed to update envelope:', err);
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t`Error`,
|
||||
description: t`Failed to update envelope. Please try again.`,
|
||||
});
|
||||
}
|
||||
|
||||
setIsUpdatingEnvelope(false);
|
||||
};
|
||||
|
||||
const embeded = useMemo(
|
||||
() => ({
|
||||
presignToken: token,
|
||||
mode: 'edit' as const,
|
||||
onUpdate: async (envelope: TEditorEnvelope) => updateEmbeddedEnvelope(envelope),
|
||||
brandingLogo,
|
||||
}),
|
||||
[token],
|
||||
);
|
||||
|
||||
const editorConfig = useMemo(() => {
|
||||
return buildEmbeddedEditorOptions(embedAuthoringOptions.features, embeded);
|
||||
}, [embedAuthoringOptions.features, embeded]);
|
||||
|
||||
const initialEnvelope = useMemo(
|
||||
() => ({
|
||||
...envelope,
|
||||
externalId: embedAuthoringOptions?.externalId || envelope.externalId || null,
|
||||
}),
|
||||
[envelope, embedAuthoringOptions?.externalId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-w-screen relative min-h-screen">
|
||||
{isUpdatingEnvelope && (
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
|
||||
<Spinner />
|
||||
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{envelope.type === EnvelopeType.DOCUMENT ? (
|
||||
<Trans>Updating Document</Trans>
|
||||
) : (
|
||||
<Trans>Updating Template</Trans>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedEnvelope && (
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
|
||||
<div className="mx-auto w-full max-w-md text-center">
|
||||
<CheckCircle2Icon className="mx-auto h-16 w-16 text-primary" />
|
||||
|
||||
<h1 className="mt-6 text-2xl font-bold">
|
||||
{envelope.type === EnvelopeType.TEMPLATE ? (
|
||||
<Trans>Template Updated</Trans>
|
||||
) : (
|
||||
<Trans>Document Updated</Trans>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{envelope.type === EnvelopeType.TEMPLATE ? (
|
||||
<Trans>Your template has been updated successfully</Trans>
|
||||
) : (
|
||||
<Trans>Your document has been updated successfully</Trans>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EnvelopeEditorProvider initialEnvelope={initialEnvelope} editorConfig={editorConfig}>
|
||||
<EnvelopeEditorRenderProviderWrapper presignedToken={token}>
|
||||
<EnvelopeEditor />
|
||||
</EnvelopeEditorRenderProviderWrapper>
|
||||
</EnvelopeEditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user