Compare commits

..

4 Commits

Author SHA1 Message Date
d4e259a9a7 refactor: improve layout of completed signing page 2025-11-18 10:16:13 +02:00
798b6bd750 feat: add japanese chinese and korean support (#2202)
## Description

Adds the following languages since we updated our PDF sealing to support
special characters
- Japanese
- Korean
- Chinese (Simplified)

## Tests

Ran through the signing process in these new languages.
2025-11-18 16:57:38 +11:00
8fbace0f61 fix: viewed webhook had stale data (#2208) 2025-11-18 16:57:14 +11:00
1bbd04be9b feat: add field dev mode (#2203) 2025-11-18 16:57:06 +11:00
7 changed files with 90 additions and 43 deletions

View File

@ -5,7 +5,7 @@ import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { FieldType, RecipientRole } from '@prisma/client'; import { FieldType, RecipientRole } from '@prisma/client';
import { FileTextIcon } from 'lucide-react'; import { FileTextIcon } from 'lucide-react';
import { Link } from 'react-router'; import { Link, useSearchParams } from 'react-router';
import { isDeepEqual } from 'remeda'; import { isDeepEqual } from 'remeda';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -65,6 +65,8 @@ const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
}; };
export const EnvelopeEditorFieldsPage = () => { export const EnvelopeEditorFieldsPage = () => {
const [searchParams] = useSearchParams();
const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor(); const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { currentEnvelopeItem } = useCurrentEnvelopeRender();
@ -208,6 +210,37 @@ export const EnvelopeEditorFieldsPage = () => {
<section> <section>
<Separator className="my-4" /> <Separator className="my-4" />
{searchParams.get('devmode') && (
<>
<div className="px-4">
<h3 className="text-foreground mb-3 text-sm font-semibold">
<Trans>Developer Mode</Trans>
</h3>
<div className="bg-muted/50 border-border text-foreground space-y-2 rounded-md border p-3 text-sm">
<p>
<span className="text-muted-foreground min-w-12">Pos X:&nbsp;</span>
{selectedField.positionX.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Pos Y:&nbsp;</span>
{selectedField.positionY.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Width:&nbsp;</span>
{selectedField.width.toFixed(2)}
</p>
<p>
<span className="text-muted-foreground min-w-12">Height:&nbsp;</span>
{selectedField.height.toFixed(2)}
</p>
</div>
</div>
<Separator className="my-4" />
</>
)}
<div className="[&_label]:text-foreground/70 px-4 [&_label]:text-xs"> <div className="[&_label]:text-foreground/70 px-4 [&_label]:text-xs">
<h3 className="text-sm font-semibold"> <h3 className="text-sm font-semibold">
{t(FieldSettingsTypeTranslations[selectedField.type])} {t(FieldSettingsTypeTranslations[selectedField.type])}

View File

@ -44,7 +44,7 @@ export default function EnvelopeEditorHeader() {
<nav className="bg-background border-border w-full border-b px-4 py-3 md:px-6"> <nav className="bg-background border-border w-full border-b px-4 py-3 md:px-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Link to={relativePath.basePath}> <Link to="/">
<BrandingLogo className="h-6 w-auto" /> <BrandingLogo className="h-6 w-auto" />
</Link> </Link>
<Separator orientation="vertical" className="h-6" /> <Separator orientation="vertical" className="h-6" />

View File

@ -22,14 +22,12 @@ import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounce
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { import {
ZRecipientActionAuthTypesSchema, ZRecipientActionAuthTypesSchema,
ZRecipientAuthOptionsSchema, ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients'; import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select'; import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
@ -84,8 +82,7 @@ const ZEnvelopeRecipientsForm = z.object({
type TEnvelopeRecipientsForm = z.infer<typeof ZEnvelopeRecipientsForm>; type TEnvelopeRecipientsForm = z.infer<typeof ZEnvelopeRecipientsForm>;
export const EnvelopeEditorRecipientForm = () => { export const EnvelopeEditorRecipientForm = () => {
const { envelope, setRecipientsDebounced, updateEnvelope, isTemplate } = const { envelope, setRecipientsDebounced, updateEnvelope } = useCurrentEnvelopeEditor();
useCurrentEnvelopeEditor();
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
@ -122,7 +119,6 @@ export const EnvelopeEditorRecipientForm = () => {
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
signingOrder: 1, signingOrder: 1,
actionAuth: [], actionAuth: [],
...(isTemplate ? generateRecipientPlaceholder(1) : {}),
}, },
]; ];
@ -238,8 +234,6 @@ export const EnvelopeEditorRecipientForm = () => {
}; };
const onAddSigner = () => { const onAddSigner = () => {
const placeholderRecipientCount = signers.length > 1 ? signers.length + 1 : 2;
appendSigner({ appendSigner({
formId: nanoid(12), formId: nanoid(12),
name: '', name: '',
@ -247,10 +241,7 @@ export const EnvelopeEditorRecipientForm = () => {
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
actionAuth: [], actionAuth: [],
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1, signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
...(isTemplate ? generateRecipientPlaceholder(placeholderRecipientCount) : {}),
}); });
void form.trigger('signers');
}; };
const onRemoveSigner = (index: number) => { const onRemoveSigner = (index: number) => {
@ -815,7 +806,7 @@ export const EnvelopeEditorRecipientForm = () => {
})} })}
> >
{!showAdvancedSettings && index === 0 && ( {!showAdvancedSettings && index === 0 && (
<FormLabel required={!isTemplate}> <FormLabel required>
<Trans>Email</Trans> <Trans>Email</Trans>
</FormLabel> </FormLabel>
)} )}
@ -824,12 +815,7 @@ export const EnvelopeEditorRecipientForm = () => {
<RecipientAutoCompleteInput <RecipientAutoCompleteInput
type="email" type="email"
placeholder={t`Email`} placeholder={t`Email`}
value={ value={field.value}
isTemplate &&
isTemplateRecipientEmailPlaceholder(field.value)
? ''
: field.value
}
disabled={ disabled={
snapshot.isDragging || snapshot.isDragging ||
isSubmitting || isSubmitting ||

View File

@ -202,8 +202,12 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</p> </p>
))} ))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4"> <div className="mt-8 flex w-full max-w-xs flex-col items-stretch gap-4 md:w-auto md:max-w-none md:flex-row md:items-center">
<DocumentShareButton documentId={document.id} token={recipient.token} /> <DocumentShareButton
documentId={document.id}
token={recipient.token}
className="w-full max-w-none md:flex-1"
/>
{isDocumentCompleted(document.status) && ( {isDocumentCompleted(document.status) && (
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
@ -212,13 +216,21 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
envelopeItems={document.envelopeItems} envelopeItems={document.envelopeItems}
token={recipient?.token} token={recipient?.token}
trigger={ trigger={
<Button type="button" variant="outline" className="flex-1"> <Button type="button" variant="outline" className="flex-1 md:flex-initial">
<DownloadIcon className="mr-2 h-5 w-5" /> <DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans> <Trans>Download</Trans>
</Button> </Button>
} }
/> />
)} )}
{user && (
<Button asChild>
<Link to="/">
<Trans>Go Back Home</Trans>
</Link>
</Button>
)}
</div> </div>
</div> </div>
@ -238,12 +250,6 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} /> <ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div> </div>
)} )}
{user && (
<Link to="/" className="text-documenso-700 hover:text-documenso-600 mt-2">
<Trans>Go Back Home</Trans>
</Link>
)}
</div> </div>
</div> </div>

View File

@ -1,6 +1,16 @@
import { z } from 'zod'; import { z } from 'zod';
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es', 'it', 'pl'] as const; export const SUPPORTED_LANGUAGE_CODES = [
'de',
'en',
'fr',
'es',
'it',
'pl',
'ja',
'ko',
'zh',
] as const;
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en'); export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
@ -54,6 +64,18 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
short: 'pl', short: 'pl',
full: 'Polish', full: 'Polish',
}, },
ja: {
short: 'ja',
full: 'Japanese',
},
ko: {
short: 'ko',
full: 'Korean',
},
zh: {
short: 'zh',
full: 'Chinese',
},
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>; } satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
export const isValidLanguageCode = (code: unknown): code is SupportedLanguageCodes => export const isValidLanguageCode = (code: unknown): code is SupportedLanguageCodes =>

View File

@ -31,26 +31,16 @@ export const viewedDocument = async ({
type: EnvelopeType.DOCUMENT, type: EnvelopeType.DOCUMENT,
}, },
}, },
include: {
envelope: {
include: {
documentMeta: true,
recipients: true,
},
},
},
}); });
if (!recipient) { if (!recipient) {
return; return;
} }
const { envelope } = recipient;
await prisma.documentAuditLog.create({ await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
envelopeId: envelope.id, envelopeId: recipient.envelopeId,
user: { user: {
name: recipient.name, name: recipient.name,
email: recipient.email, email: recipient.email,
@ -86,7 +76,7 @@ export const viewedDocument = async ({
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
envelopeId: envelope.id, envelopeId: recipient.envelopeId,
user: { user: {
name: recipient.name, name: recipient.name,
email: recipient.email, email: recipient.email,
@ -103,6 +93,16 @@ export const viewedDocument = async ({
}); });
}); });
const envelope = await prisma.envelope.findUniqueOrThrow({
where: {
id: recipient.envelopeId,
},
include: {
documentMeta: true,
recipients: true,
},
});
await triggerWebhook({ await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_OPENED, event: WebhookTriggerEvents.DOCUMENT_OPENED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),

View File

@ -127,11 +127,11 @@ export const DocumentShareButton = ({
<Button <Button
variant="outline" variant="outline"
disabled={!token || !documentId} disabled={!token || !documentId}
className={cn('flex-1 text-[11px]', className)} className={cn('w-full max-w-lg flex-1 text-[11px]', className)}
loading={isLoading} loading={isLoading}
> >
{!isLoading && <Sparkles className="mr-2 h-5 w-5" />} {!isLoading && <Sparkles className="mr-2 h-5 w-5" />}
<Trans>Share Signature Card</Trans> <Trans>Share</Trans>
</Button> </Button>
)} )}
</DialogTrigger> </DialogTrigger>