mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 17:21:41 +10:00
fix: add preview page
This commit is contained in:
@ -1,10 +1,20 @@
|
||||
import { lazy, useEffect, useState } from 'react';
|
||||
import { lazy, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { faker } from '@faker-js/faker/locale/en';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ConstructionIcon, FileTextIcon } from 'lucide-react';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import {
|
||||
EnvelopeRenderProvider,
|
||||
useCurrentEnvelopeRender,
|
||||
} from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
|
||||
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
@ -15,15 +25,169 @@ import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||
|
||||
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
|
||||
|
||||
// Todo: Envelopes - Dynamically import faker
|
||||
export const EnvelopeEditorPreviewPage = () => {
|
||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||
|
||||
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
|
||||
'recipient',
|
||||
);
|
||||
|
||||
const fieldsWithPlaceholders = useMemo(() => {
|
||||
return fields.map((field) => {
|
||||
const fieldMeta = ZFieldAndMetaSchema.parse(field);
|
||||
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
faker.seed(recipient.id);
|
||||
|
||||
const recipientName = recipient.name || faker.person.fullName();
|
||||
const recipientEmail = recipient.email || faker.internet.email();
|
||||
|
||||
faker.seed(recipient.id + field.id);
|
||||
|
||||
return {
|
||||
...field,
|
||||
inserted: true,
|
||||
...match(fieldMeta)
|
||||
.with({ type: FieldType.TEXT }, ({ fieldMeta }) => {
|
||||
let text = fieldMeta?.text || faker.lorem.words(5);
|
||||
|
||||
if (fieldMeta?.characterLimit) {
|
||||
text = text.slice(0, fieldMeta?.characterLimit);
|
||||
}
|
||||
|
||||
return {
|
||||
customText: text,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.NUMBER }, ({ fieldMeta }) => {
|
||||
let number = fieldMeta?.value ?? '';
|
||||
|
||||
if (number === '') {
|
||||
number = faker.number
|
||||
.int({
|
||||
min: fieldMeta?.minValue ?? 0,
|
||||
max: fieldMeta?.maxValue ?? 1000,
|
||||
})
|
||||
.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
customText: number,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.DATE }, () => {
|
||||
const date = extractFieldInsertionValues({
|
||||
fieldValue: {
|
||||
type: FieldType.DATE,
|
||||
value: true,
|
||||
},
|
||||
field,
|
||||
documentMeta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
return {
|
||||
customText: date.customText,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.EMAIL }, () => {
|
||||
return {
|
||||
customText: recipientEmail,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.NAME }, () => {
|
||||
return {
|
||||
customText: recipientName,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.INITIALS }, () => {
|
||||
return {
|
||||
customText: extractInitials(recipientName),
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.RADIO }, ({ fieldMeta }) => {
|
||||
const values = fieldMeta?.values ?? [];
|
||||
|
||||
if (values.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let customText = '';
|
||||
|
||||
const preselectedValue = values.findIndex((value) => value.checked);
|
||||
|
||||
if (preselectedValue !== -1) {
|
||||
customText = preselectedValue.toString();
|
||||
} else {
|
||||
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
|
||||
customText = randomIndex.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
customText,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.CHECKBOX }, ({ fieldMeta }) => {
|
||||
let checkedValues: number[] = [];
|
||||
|
||||
const values = fieldMeta?.values ?? [];
|
||||
|
||||
values.forEach((value, index) => {
|
||||
if (value.checked) {
|
||||
checkedValues.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (checkedValues.length === 0 && values.length > 0) {
|
||||
const numberOfValues = fieldMeta?.validationLength || 1;
|
||||
|
||||
checkedValues = Array.from({ length: numberOfValues }, (_, index) => index);
|
||||
}
|
||||
|
||||
return {
|
||||
customText: toCheckboxCustomText(checkedValues),
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.DROPDOWN }, ({ fieldMeta }) => {
|
||||
const values = fieldMeta?.values ?? [];
|
||||
|
||||
let customText = fieldMeta?.defaultValue || '';
|
||||
|
||||
if (!customText && values.length > 0) {
|
||||
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
|
||||
customText = values[randomIndex].value;
|
||||
}
|
||||
|
||||
return {
|
||||
customText,
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.SIGNATURE }, () => {
|
||||
return {
|
||||
customText: '',
|
||||
signature: {
|
||||
signatureImageAsBase64: '',
|
||||
typedSignature: recipientName,
|
||||
},
|
||||
};
|
||||
})
|
||||
.with({ type: FieldType.FREE_SIGNATURE }, () => {
|
||||
return {
|
||||
customText: '',
|
||||
};
|
||||
})
|
||||
.exhaustive(),
|
||||
};
|
||||
});
|
||||
}, [fields, envelope, envelope.recipients, envelope.documentMeta]);
|
||||
|
||||
/**
|
||||
* Set the selected recipient to the first recipient in the envelope.
|
||||
*/
|
||||
@ -31,40 +195,38 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null);
|
||||
}, []);
|
||||
|
||||
// Override the parent renderer provider so we can inject custom fields.
|
||||
return (
|
||||
<div className="relative flex h-full">
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
<EnvelopeRenderProvider
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={fieldsWithPlaceholders}
|
||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
||||
overrideSettings={{
|
||||
mode: 'export',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex h-full">
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex flex-col items-center justify-center">
|
||||
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex flex-col items-center justify-center">
|
||||
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Coming soon section */}
|
||||
<div className="border-border bg-card hover:bg-accent/10 flex w-full max-w-[800px] items-center gap-4 rounded-lg border p-4 transition-colors">
|
||||
<div className="flex w-full flex-col items-center justify-center gap-2 py-32">
|
||||
<ConstructionIcon className="text-muted-foreground h-10 w-10" />
|
||||
<h3 className="text-foreground text-sm font-semibold">
|
||||
<Trans>Coming soon</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>This feature is coming soon</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Todo: Envelopes - Remove div after preview mode is implemented */}
|
||||
<div className="hidden">
|
||||
{currentEnvelopeItem !== null ? (
|
||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
|
||||
<PDFViewerKonvaLazy
|
||||
renderer="editor"
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-32">
|
||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||
@ -78,27 +240,28 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Form Fields Panel */}
|
||||
{currentEnvelopeItem && false && (
|
||||
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||
{/* Add fields section. */}
|
||||
<section className="px-4">
|
||||
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
{/* Right Section - Form Fields Panel */}
|
||||
{currentEnvelopeItem && false && (
|
||||
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||
{/* Add fields section. */}
|
||||
<section className="px-4">
|
||||
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Preivew Mode</Trans>
|
||||
</h3> */}
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert variant="neutral">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Preview what the signed document will look like with placeholder data
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* <Alert variant="neutral">
|
||||
{/* <Alert variant="neutral">
|
||||
<RadioGroup
|
||||
className="gap-y-1"
|
||||
value={selectedPreviewMode}
|
||||
@ -137,36 +300,37 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
<div>Preview what a recipient will see</div>
|
||||
|
||||
<div>Preview the signed document</div> */}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{false && (
|
||||
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
||||
{selectedPreviewMode === 'recipient' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
{false && (
|
||||
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
||||
{selectedPreviewMode === 'recipient' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Recipient selector section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
{/* Recipient selector section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
|
||||
<RecipientSelector
|
||||
selectedRecipient={editorFields.selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) =>
|
||||
editorFields.setSelectedRecipient(recipient.id)
|
||||
}
|
||||
recipients={envelope.recipients}
|
||||
className="w-full"
|
||||
align="end"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</AnimateGenericFadeInOut>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RecipientSelector
|
||||
selectedRecipient={editorFields.selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) =>
|
||||
editorFields.setSelectedRecipient(recipient.id)
|
||||
}
|
||||
recipients={envelope.recipients}
|
||||
className="w-full"
|
||||
align="end"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</AnimateGenericFadeInOut>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EnvelopeRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -174,7 +174,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
const { t, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { envelope } = useCurrentEnvelopeEditor();
|
||||
const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
const organisation = useCurrentOrganisation();
|
||||
@ -186,14 +186,12 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
documentAuth: envelope.authOptions,
|
||||
});
|
||||
|
||||
const form = useForm<TAddSettingsFormSchema>({
|
||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||
defaultValues: {
|
||||
externalId: envelope.externalId || '', // Todo: String or undefined?
|
||||
const createDefaultValues = () => {
|
||||
return {
|
||||
externalId: envelope.externalId || '',
|
||||
visibility: envelope.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
|
||||
meta: {
|
||||
subject: envelope.documentMeta.subject ?? '',
|
||||
message: envelope.documentMeta.message ?? '',
|
||||
@ -210,10 +208,13 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
|
||||
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation();
|
||||
const form = useForm<TAddSettingsFormSchema>({
|
||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||
defaultValues: createDefaultValues(),
|
||||
});
|
||||
|
||||
const envelopeHasBeenSent =
|
||||
envelope.type === EnvelopeType.DOCUMENT &&
|
||||
@ -239,8 +240,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
.safeParse(data.globalAccessAuth);
|
||||
|
||||
try {
|
||||
await updateEnvelope({
|
||||
envelopeId: envelope.id,
|
||||
await updateEnvelopeAsync({
|
||||
data: {
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
@ -295,7 +295,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
form.reset(createDefaultValues());
|
||||
setActiveTab('general');
|
||||
}, [open, form]);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
export default function EnvelopeGenericPageRenderer() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } =
|
||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError, overrideSettings } =
|
||||
useCurrentEnvelopeRender();
|
||||
|
||||
const {
|
||||
@ -50,12 +50,11 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
...field,
|
||||
customText: '',
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
inserted: false,
|
||||
customText: field.inserted ? field.customText : '',
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
@ -63,7 +62,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: false,
|
||||
mode: 'sign',
|
||||
mode: overrideSettings?.mode ?? 'sign',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@epic-web/remember": "^1.1.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hono/node-server": "^1.13.7",
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
@ -104,4 +105,4 @@
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user