fix: handle loading files in embedded authoring update flows

This commit is contained in:
Lucas Smith
2025-11-19 23:22:33 +11:00
parent fa1680aaf1
commit 044b267016
7 changed files with 95 additions and 63 deletions

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentData, FieldType } from '@prisma/client'; import type { EnvelopeItem, FieldType } from '@prisma/client';
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client'; import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
import { base64 } from '@scure/base'; import { base64 } from '@scure/base';
import { ChevronsUpDown } from 'lucide-react'; import { ChevronsUpDown } from 'lucide-react';
@ -40,7 +40,8 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type ConfigureFieldsViewProps = { export type ConfigureFieldsViewProps = {
configData: TConfigureEmbedFormSchema; configData: TConfigureEmbedFormSchema;
documentData?: DocumentData; presignToken?: string | undefined;
envelopeItem?: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
defaultValues?: Partial<TConfigureFieldsFormSchema>; defaultValues?: Partial<TConfigureFieldsFormSchema>;
onBack?: (data: TConfigureFieldsFormSchema) => void; onBack?: (data: TConfigureFieldsFormSchema) => void;
onSubmit: (data: TConfigureFieldsFormSchema) => void; onSubmit: (data: TConfigureFieldsFormSchema) => void;
@ -48,7 +49,8 @@ export type ConfigureFieldsViewProps = {
export const ConfigureFieldsView = ({ export const ConfigureFieldsView = ({
configData, configData,
documentData, presignToken,
envelopeItem,
defaultValues, defaultValues,
onBack, onBack,
onSubmit, onSubmit,
@ -82,12 +84,12 @@ export const ConfigureFieldsView = ({
}, []); }, []);
const normalizedDocumentData = useMemo(() => { const normalizedDocumentData = useMemo(() => {
if (documentData) { if (envelopeItem) {
return documentData.data; return undefined;
} }
if (!configData.documentData) { if (!configData.documentData) {
return null; return undefined;
} }
return base64.encode(configData.documentData.data); return base64.encode(configData.documentData.data);
@ -534,56 +536,55 @@ export const ConfigureFieldsView = ({
)} )}
<Form {...form}> <Form {...form}>
{normalizedDocumentData && ( {normalizedDocumentData ||
<div> (envelopeItem && (
<PDFViewer <div>
overrideData={normalizedDocumentData} <PDFViewer
envelopeItem={{ presignToken={presignToken}
id: '', overrideData={normalizedDocumentData}
envelopeId: '', envelopeItem={envelopeItem}
}} token={undefined}
token={undefined} version="signed"
version="signed" />
/>
<ElementVisible <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`} target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
> >
{localFields.map((field, index) => { {localFields.map((field, index) => {
const recipientIndex = recipients.findIndex( const recipientIndex = recipients.findIndex(
(r) => r.id === field.recipientId, (r) => r.id === field.recipientId,
); );
return ( return (
<FieldItem <FieldItem
key={field.formId} key={field.formId}
field={field} field={field}
minHeight={MIN_HEIGHT_PX} minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX} minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX} defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX} defaultWidth={DEFAULT_WIDTH_PX}
onResize={(node) => onFieldResize(node, index)} onResize={(node) => onFieldResize(node, index)}
onMove={(node) => onFieldMove(node, index)} onMove={(node) => onFieldMove(node, index)}
onRemove={() => remove(index)} onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })} onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })} onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onFocus={() => setLastActiveField(field)} onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)} onBlur={() => setLastActiveField(null)}
onAdvancedSettings={() => { onAdvancedSettings={() => {
setCurrentField(field); setCurrentField(field);
setShowAdvancedSettings(true); setShowAdvancedSettings(true);
}} }}
recipientIndex={recipientIndex} recipientIndex={recipientIndex}
active={activeFieldId === field.formId} active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)} onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)} onFieldDeactivate={() => setActiveFieldId(null)}
disabled={selectedRecipient?.id !== field.recipientId} disabled={selectedRecipient?.id !== field.recipientId}
/> />
); );
})} })}
</ElementVisible> </ElementVisible>
</div> </div>
)} ))}
</Form> </Form>
</div> </div>
</div> </div>

View File

@ -75,6 +75,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
})); }));
return { return {
token,
document: { document: {
...document, ...document,
fields, fields,
@ -86,7 +87,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { document } = useLoaderData<typeof loader>(); const { document, token } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasFinishedInit, setHasFinishedInit] = useState(false);
@ -321,7 +322,8 @@ export default function EmbeddingAuthoringDocumentEditPage() {
<ConfigureFieldsView <ConfigureFieldsView
configData={configuration!} configData={configuration!}
documentData={document.documentData} presignToken={token}
envelopeItem={document.envelopeItems[0]}
defaultValues={fields ?? undefined} defaultValues={fields ?? undefined}
onBack={canGoBack ? handleBackToConfig : undefined} onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit} onSubmit={handleConfigureFieldsSubmit}

View File

@ -75,6 +75,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
})); }));
return { return {
token,
template: { template: {
...template, ...template,
fields, fields,
@ -86,7 +87,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { template } = useLoaderData<typeof loader>(); const { template, token } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasFinishedInit, setHasFinishedInit] = useState(false);
@ -321,7 +322,8 @@ export default function EmbeddingAuthoringTemplateEditPage() {
<ConfigureFieldsView <ConfigureFieldsView
configData={configuration!} configData={configuration!}
documentData={template.templateDocumentData} presignToken={token}
envelopeItem={template.envelopeItems[0]}
defaultValues={fields ?? undefined} defaultValues={fields ?? undefined}
onBack={canGoBack ? handleBackToConfig : undefined} onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit} onSubmit={handleConfigureFieldsSubmit}

View File

@ -5,6 +5,7 @@ import { Hono } from 'hono';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team'; import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
@ -16,6 +17,7 @@ import {
type TGetPresignedPostUrlResponse, type TGetPresignedPostUrlResponse,
ZGetEnvelopeItemFileDownloadRequestParamsSchema, ZGetEnvelopeItemFileDownloadRequestParamsSchema,
ZGetEnvelopeItemFileRequestParamsSchema, ZGetEnvelopeItemFileRequestParamsSchema,
ZGetEnvelopeItemFileRequestQuerySchema,
ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema, ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema,
ZGetEnvelopeItemFileTokenRequestParamsSchema, ZGetEnvelopeItemFileTokenRequestParamsSchema,
ZGetPresignedPostUrlRequestSchema, ZGetPresignedPostUrlRequestSchema,
@ -68,12 +70,24 @@ export const filesRoute = new Hono<HonoEnv>()
.get( .get(
'/envelope/:envelopeId/envelopeItem/:envelopeItemId', '/envelope/:envelopeId/envelopeItem/:envelopeItemId',
sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema), sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema),
sValidator('query', ZGetEnvelopeItemFileRequestQuerySchema),
async (c) => { async (c) => {
const { envelopeId, envelopeItemId } = c.req.valid('param'); const { envelopeId, envelopeItemId } = c.req.valid('param');
const { token } = c.req.query();
const session = await getOptionalSession(c); const session = await getOptionalSession(c);
if (!session.user) { let userId = session.user?.id;
if (token) {
const presignToken = await verifyEmbeddingPresignToken({
token,
}).catch(() => undefined);
userId = presignToken?.userId;
}
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401); return c.json({ error: 'Unauthorized' }, 401);
} }
@ -104,7 +118,7 @@ export const filesRoute = new Hono<HonoEnv>()
} }
const team = await getTeamById({ const team = await getTeamById({
userId: session.user.id, userId: userId,
teamId: envelope.teamId, teamId: envelope.teamId,
}).catch((error) => { }).catch((error) => {
console.error(error); console.error(error);

View File

@ -36,6 +36,14 @@ export type TGetEnvelopeItemFileRequestParams = z.infer<
typeof ZGetEnvelopeItemFileRequestParamsSchema typeof ZGetEnvelopeItemFileRequestParamsSchema
>; >;
export const ZGetEnvelopeItemFileRequestQuerySchema = z.object({
token: z.string().optional(),
});
export type TGetEnvelopeItemFileRequestQuery = z.infer<
typeof ZGetEnvelopeItemFileRequestQuerySchema
>;
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({ export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
token: z.string().min(1), token: z.string().min(1),
envelopeItemId: z.string().min(1), envelopeItemId: z.string().min(1),

View File

@ -8,15 +8,17 @@ export type EnvelopeItemPdfUrlOptions =
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>; envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined; token: string | undefined;
version: 'original' | 'signed'; version: 'original' | 'signed';
presignToken?: undefined;
} }
| { | {
type: 'view'; type: 'view';
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>; envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined; token: string | undefined;
presignToken?: string | undefined;
}; };
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => { export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
const { envelopeItem, token, type } = options; const { envelopeItem, token, type, presignToken } = options;
const { id, envelopeId } = envelopeItem; const { id, envelopeId } = envelopeItem;
@ -24,11 +26,11 @@ export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
const version = options.version; const version = options.version;
return token return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}` ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}${presignToken ? `?presignToken=${presignToken}` : ''}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`; : `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
} }
return token return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}` ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}${presignToken ? `?presignToken=${presignToken}` : ''}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`; : `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
}; };

View File

@ -56,6 +56,7 @@ export type PDFViewerProps = {
className?: string; className?: string;
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>; envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined; token: string | undefined;
presignToken?: string | undefined;
version: 'original' | 'signed'; version: 'original' | 'signed';
onDocumentLoad?: (_doc: LoadedPDFDocument) => void; onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick; onPageClick?: OnPDFViewerPageClick;
@ -67,6 +68,7 @@ export const PDFViewer = ({
className, className,
envelopeItem, envelopeItem,
token, token,
presignToken,
version, version,
onDocumentLoad, onDocumentLoad,
onPageClick, onPageClick,
@ -166,6 +168,7 @@ export const PDFViewer = ({
type: 'view', type: 'view',
envelopeItem: envelopeItem, envelopeItem: envelopeItem,
token, token,
presignToken,
}); });
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer()); const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());