mirror of
https://github.com/documenso/documenso.git
synced 2025-11-25 14:11:43 +10:00
Merge branch 'main' into feat/bin-tab
This commit is contained in:
@ -23,12 +23,12 @@ import {
|
||||
getUsersWithSubscriptionsCount,
|
||||
} from '@documenso/lib/server-only/admin/get-users-stats';
|
||||
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
|
||||
import { AdminStatsUsersWithDocumentsChart } from '~/components/general/admin-stats-users-with-documents';
|
||||
import { CardMetric } from '~/components/general/metric-card';
|
||||
|
||||
import { version } from '../../../../package.json';
|
||||
import type { Route } from './+types/stats';
|
||||
|
||||
export async function loader() {
|
||||
@ -89,7 +89,7 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
|
||||
value={usersWithSubscriptionsCount}
|
||||
/>
|
||||
|
||||
<CardMetric icon={FileCog} title={_(msg`App Version`)} value={`v${env('APP_VERSION')}`} />
|
||||
<CardMetric icon={FileCog} title={_(msg`App Version`)} value={`v${version}`} />
|
||||
</div>
|
||||
|
||||
<div className="mt-16 gap-8">
|
||||
|
||||
@ -6,6 +6,7 @@ import { redirect } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
@ -59,6 +60,8 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
throw redirect('/');
|
||||
}
|
||||
|
||||
const isPlatformDocument = await isDocumentPlatform(document);
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
||||
|
||||
const auditLogs = await getDocumentCertificateAuditLogs({
|
||||
@ -70,6 +73,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
return {
|
||||
document,
|
||||
documentLanguage,
|
||||
isPlatformDocument,
|
||||
auditLogs,
|
||||
messages,
|
||||
};
|
||||
@ -85,7 +89,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||
*/
|
||||
export default function SigningCertificate({ loaderData }: Route.ComponentProps) {
|
||||
const { document, documentLanguage, auditLogs, messages } = loaderData;
|
||||
const { document, documentLanguage, isPlatformDocument, auditLogs, messages } = loaderData;
|
||||
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
@ -245,24 +249,24 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
{signature.secondaryId}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted-foreground">N/A</p>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||
@ -337,15 +341,17 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
{isPlatformDocument && (
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,7 +79,14 @@ export default function DirectTemplatePage() {
|
||||
const { template, directTemplateRecipient } = data;
|
||||
|
||||
return (
|
||||
<DocumentSigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
|
||||
<DocumentSigningProvider
|
||||
email={user?.email}
|
||||
fullName={user?.name}
|
||||
signature={user?.signature}
|
||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
|
||||
>
|
||||
<DocumentSigningAuthProvider
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={directTemplateRecipient}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { DocumentSigningOrder, DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { Clock8 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
@ -13,6 +13,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getNextPendingRecipient } from '@documenso/lib/server-only/recipient/get-next-pending-recipient';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
@ -72,7 +73,24 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
? await getRecipientsForAssistant({
|
||||
token,
|
||||
})
|
||||
: [];
|
||||
: [recipient];
|
||||
|
||||
if (
|
||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||
recipient.role !== RecipientRole.ASSISTANT
|
||||
) {
|
||||
const nextPendingRecipient = await getNextPendingRecipient({
|
||||
documentId: document.id,
|
||||
currentRecipientId: recipient.id,
|
||||
});
|
||||
|
||||
if (nextPendingRecipient) {
|
||||
allRecipients.push({
|
||||
...nextPendingRecipient,
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
@ -215,6 +233,9 @@ export default function SigningPage() {
|
||||
email={recipient.email}
|
||||
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
||||
signature={user?.email === recipient.email ? user?.signature : undefined}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
||||
>
|
||||
<DocumentSigningAuthProvider
|
||||
documentAuthOptions={document.authOptions}
|
||||
|
||||
@ -6,7 +6,7 @@ import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import type { Route } from './+types/team.verify.transfer.token';
|
||||
import type { Route } from './+types/team.verify.transfer.$token';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { token } = params;
|
||||
@ -131,7 +131,14 @@ export default function EmbedDirectTemplatePage() {
|
||||
} = useSuperLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<DocumentSigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
|
||||
<DocumentSigningProvider
|
||||
email={user?.email}
|
||||
fullName={user?.name}
|
||||
signature={user?.signature}
|
||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
|
||||
>
|
||||
<DocumentSigningAuthProvider
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={recipient}
|
||||
@ -8,6 +8,7 @@ import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-ent
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
@ -33,7 +34,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
|
||||
const { user } = await getOptionalSession(request);
|
||||
|
||||
const [document, fields, recipient] = await Promise.all([
|
||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
userId: user?.id,
|
||||
@ -41,6 +42,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
}).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
getCompletedFieldsForToken({ token }).catch(() => []),
|
||||
]);
|
||||
|
||||
// `document.directLink` is always available but we're doing this to
|
||||
@ -130,6 +132,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
allRecipients,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
isPlatformDocument,
|
||||
isEnterpriseDocument,
|
||||
@ -145,6 +148,7 @@ export default function EmbedSignDocumentPage() {
|
||||
allRecipients,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
isPlatformDocument,
|
||||
isEnterpriseDocument,
|
||||
@ -156,6 +160,9 @@ export default function EmbedSignDocumentPage() {
|
||||
email={recipient.email}
|
||||
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
||||
signature={user?.email === recipient.email ? user?.signature : undefined}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
||||
>
|
||||
<DocumentSigningAuthProvider
|
||||
documentAuthOptions={document.authOptions}
|
||||
@ -168,6 +175,7 @@ export default function EmbedSignDocumentPage() {
|
||||
documentData={document.documentData}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
completedFields={completedFields}
|
||||
metadata={document.documentMeta}
|
||||
isCompleted={isDocumentCompleted(document.status)}
|
||||
hidePoweredBy={
|
||||
66
apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx
Normal file
66
apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
import { TrpcProvider, trpc } from '@documenso/trpc/react';
|
||||
|
||||
import { EmbedClientLoading } from '~/components/embed/embed-client-loading';
|
||||
import { ZBaseEmbedAuthoringSchema } from '~/types/embed-authoring-base-schema';
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
|
||||
export default function AuthoringLayout() {
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
const {
|
||||
mutateAsync: verifyEmbeddingPresignToken,
|
||||
isPending: isVerifyingEmbeddingPresignToken,
|
||||
data: isVerified,
|
||||
} = trpc.embeddingPresign.verifyEmbeddingPresignToken.useMutation();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { token, css, cssVars, darkModeDisabled } = result.data;
|
||||
|
||||
if (darkModeDisabled) {
|
||||
document.documentElement.classList.add('dark-mode-disabled');
|
||||
}
|
||||
|
||||
injectCss({
|
||||
css,
|
||||
cssVars,
|
||||
});
|
||||
|
||||
void verifyEmbeddingPresignToken({ token }).then((result) => {
|
||||
if (result.success) {
|
||||
setToken(token);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error verifying embedding presign token:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isVerifyingEmbeddingPresignToken) {
|
||||
return <EmbedClientLoading />;
|
||||
}
|
||||
|
||||
if (typeof isVerified !== 'undefined' && !isVerified.success) {
|
||||
return <div>Invalid embedding presign token</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TrpcProvider headers={{ authorization: `Bearer ${token}` }}>
|
||||
<Outlet />
|
||||
</TrpcProvider>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { CheckCircle2 } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
export default function EmbeddingAuthoringCompletedPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Get templateId and externalId from URL search params
|
||||
const templateId = searchParams.get('templateId');
|
||||
const documentId = searchParams.get('documentId');
|
||||
const externalId = searchParams.get('externalId');
|
||||
|
||||
const id = Number(templateId || documentId);
|
||||
const type = templateId ? 'template' : 'document';
|
||||
|
||||
// Send postMessage to parent window with the details
|
||||
useEffect(() => {
|
||||
if (!id || !window.parent || window.parent === window) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: `${type}-created`,
|
||||
[`${type}Id`]: id,
|
||||
externalId,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}, [id, type, externalId]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[100dvh] flex-col items-center justify-center p-6 text-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<CheckCircle2 className="text-primary mx-auto h-16 w-16" />
|
||||
|
||||
<h1 className="mt-6 text-2xl font-bold">
|
||||
{type === 'template' ? <Trans>Template Created</Trans> : <Trans>Document Created</Trans>}
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{type === 'template' ? (
|
||||
<Trans>Your template has been created successfully</Trans>
|
||||
) : (
|
||||
<Trans>Your document has been created successfully</Trans>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
apps/remix/app/routes/embed+/v1.authoring+/document.create.tsx
Normal file
184
apps/remix/app/routes/embed+/v1.authoring+/document.create.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||
import {
|
||||
ConfigureFieldsView,
|
||||
type TConfigureFieldsFormSchema,
|
||||
} from '~/components/embed/authoring/configure-fields-view';
|
||||
import {
|
||||
type TBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringSchema,
|
||||
} from '~/types/embed-authoring-base-schema';
|
||||
|
||||
export default function EmbeddingAuthoringDocumentCreatePage() {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [configuration, setConfiguration] = useState<TConfigureEmbedFormSchema | null>(null);
|
||||
const [fields, setFields] = useState<TConfigureFieldsFormSchema | null>(null);
|
||||
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||
const [externalId, setExternalId] = useState<string | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const { mutateAsync: createEmbeddingDocument } =
|
||||
trpc.embeddingPresign.createEmbeddingDocument.useMutation();
|
||||
|
||||
const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => {
|
||||
// Store the configuration data and move to the field placement stage
|
||||
setConfiguration(data);
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
const handleBackToConfig = (data: TConfigureFieldsFormSchema) => {
|
||||
// Return to the configuration view but keep the data
|
||||
setFields(data);
|
||||
setCurrentStep(1);
|
||||
};
|
||||
|
||||
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
||||
try {
|
||||
if (!configuration || !configuration.documentData) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: _('Error'),
|
||||
description: _('Please configure the document first'),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = data.fields;
|
||||
|
||||
const documentData = await putPdfFile({
|
||||
arrayBuffer: async () => Promise.resolve(configuration.documentData!.data.buffer),
|
||||
name: configuration.documentData.name,
|
||||
type: configuration.documentData.type,
|
||||
});
|
||||
|
||||
// Use the externalId from the URL fragment if available
|
||||
const documentExternalId = externalId || configuration.meta.externalId;
|
||||
|
||||
const createResult = await createEmbeddingDocument({
|
||||
title: configuration.title,
|
||||
documentDataId: documentData.id,
|
||||
externalId: documentExternalId,
|
||||
meta: {
|
||||
...configuration.meta,
|
||||
drawSignatureEnabled:
|
||||
configuration.meta.signatureTypes.length === 0 ||
|
||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
typedSignatureEnabled:
|
||||
configuration.meta.signatureTypes.length === 0 ||
|
||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled:
|
||||
configuration.meta.signatureTypes.length === 0 ||
|
||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
},
|
||||
recipients: configuration.signers.map((signer) => ({
|
||||
name: signer.name,
|
||||
email: signer.email,
|
||||
role: signer.role,
|
||||
fields: fields
|
||||
.filter((field) => field.signerEmail === signer.email)
|
||||
// There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map<any>((f) => ({
|
||||
...f,
|
||||
pageX: f.pageX,
|
||||
pageY: f.pageY,
|
||||
width: f.pageWidth,
|
||||
height: f.pageHeight,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _('Success'),
|
||||
description: _('Document created successfully'),
|
||||
});
|
||||
|
||||
// Send a message to the parent window with the document details
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'document-created',
|
||||
documentId: createResult.documentId,
|
||||
externalId: documentExternalId,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
// Navigate to the completion page instead of the document details page
|
||||
await navigate(
|
||||
`/embed/v1/authoring/create-completed?documentId=${createResult.documentId}&externalId=${documentExternalId}#${hash}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error creating document:', err);
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: _('Error'),
|
||||
description: _('Failed to create document'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFeatures(result.data.features);
|
||||
|
||||
// Extract externalId from the parsed data if available
|
||||
if (result.data.externalId) {
|
||||
setExternalId(result.data.externalId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
|
||||
<ConfigureDocumentProvider isTemplate={false} features={features ?? {}}>
|
||||
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
|
||||
<ConfigureDocumentView
|
||||
defaultValues={configuration ?? undefined}
|
||||
onSubmit={handleConfigurePageViewSubmit}
|
||||
/>
|
||||
|
||||
<ConfigureFieldsView
|
||||
configData={configuration!}
|
||||
defaultValues={fields ?? undefined}
|
||||
onBack={handleBackToConfig}
|
||||
onSubmit={handleConfigureFieldsSubmit}
|
||||
/>
|
||||
</Stepper>
|
||||
</ConfigureDocumentProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
175
apps/remix/app/routes/embed+/v1.authoring+/template.create.tsx
Normal file
175
apps/remix/app/routes/embed+/v1.authoring+/template.create.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||
import {
|
||||
ConfigureFieldsView,
|
||||
type TConfigureFieldsFormSchema,
|
||||
} from '~/components/embed/authoring/configure-fields-view';
|
||||
import {
|
||||
type TBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringSchema,
|
||||
} from '~/types/embed-authoring-base-schema';
|
||||
|
||||
export default function EmbeddingAuthoringTemplateCreatePage() {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [configuration, setConfiguration] = useState<TConfigureEmbedFormSchema | null>(null);
|
||||
const [fields, setFields] = useState<TConfigureFieldsFormSchema | null>(null);
|
||||
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||
const [externalId, setExternalId] = useState<string | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const { mutateAsync: createEmbeddingTemplate } =
|
||||
trpc.embeddingPresign.createEmbeddingTemplate.useMutation();
|
||||
|
||||
const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => {
|
||||
// Store the configuration data and move to the field placement stage
|
||||
setConfiguration(data);
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
const handleBackToConfig = (data: TConfigureFieldsFormSchema) => {
|
||||
// Return to the configuration view but keep the data
|
||||
setFields(data);
|
||||
setCurrentStep(1);
|
||||
};
|
||||
|
||||
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
||||
try {
|
||||
console.log('configuration', configuration);
|
||||
console.log('data', data);
|
||||
if (!configuration || !configuration.documentData) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: _('Error'),
|
||||
description: _('Please configure the template first'),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = data.fields;
|
||||
|
||||
const documentData = await putPdfFile({
|
||||
arrayBuffer: async () => Promise.resolve(configuration.documentData!.data.buffer),
|
||||
name: configuration.documentData.name,
|
||||
type: configuration.documentData.type,
|
||||
});
|
||||
|
||||
// Use the externalId from the URL fragment if available
|
||||
const metaWithExternalId = {
|
||||
...configuration.meta,
|
||||
externalId: externalId || configuration.meta.externalId,
|
||||
};
|
||||
|
||||
const createResult = await createEmbeddingTemplate({
|
||||
title: configuration.title,
|
||||
documentDataId: documentData.id,
|
||||
meta: metaWithExternalId,
|
||||
recipients: configuration.signers.map((signer) => ({
|
||||
name: signer.name,
|
||||
email: signer.email,
|
||||
role: signer.role,
|
||||
fields: fields
|
||||
.filter((field) => field.signerEmail === signer.email)
|
||||
// There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map<any>((field) => ({
|
||||
...field,
|
||||
pageX: field.pageX,
|
||||
pageY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _('Success'),
|
||||
description: _('Template created successfully'),
|
||||
});
|
||||
|
||||
// Send a message to the parent window with the template details
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'template-created',
|
||||
templateId: createResult.templateId,
|
||||
externalId: metaWithExternalId.externalId,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
// Navigate to the completion page instead of the template details page
|
||||
await navigate(
|
||||
`/embed/v1/authoring/create-completed?templateId=${createResult.templateId}&externalId=${metaWithExternalId.externalId}#${hash}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error creating template:', err);
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: _('Error'),
|
||||
description: _('Failed to create template'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFeatures(result.data.features);
|
||||
|
||||
// Extract externalId from the parsed data if available
|
||||
if (result.data.externalId) {
|
||||
setExternalId(result.data.externalId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
|
||||
<ConfigureDocumentProvider isTemplate={true} features={features ?? {}}>
|
||||
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
|
||||
<ConfigureDocumentView
|
||||
defaultValues={configuration ?? undefined}
|
||||
onSubmit={handleConfigurePageViewSubmit}
|
||||
/>
|
||||
|
||||
<ConfigureFieldsView
|
||||
configData={configuration!}
|
||||
defaultValues={fields ?? undefined}
|
||||
onBack={handleBackToConfig}
|
||||
onSubmit={handleConfigureFieldsSubmit}
|
||||
/>
|
||||
</Stepper>
|
||||
</ConfigureDocumentProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user