Compare commits

..

7 Commits

Author SHA1 Message Date
Catalin Pit fbbb0289c3 fix: fields font-size 2025-10-01 15:10:01 +03:00
Catalin Pit c7d21c6587 fix: update personal organisation email settings (#2048) 2025-09-29 10:11:00 +03:00
Mythie 2aa391f917 v1.12.7 2025-09-26 09:57:34 +10:00
Lucas Smith 681540b501 fix: add removed date formats (#2049)
Add date formats that were removed in a prior pull request causing
issues with certain API requests.
2025-09-26 09:56:46 +10:00
Catalin Pit f3305ac306 feat: show branding logo on signing page (#2031)
If the team has the branding enabled & a logo uploaded, it'll show on
the document signing page view.
2025-09-25 22:41:17 +10:00
Catalin Pit 68b4305b6a feat: add max file size for uploaded documents (#2044) 2025-09-25 22:40:00 +10:00
Catalin Pit 3de1ea0a02 feat: resend dialog improvements (#2034)
The checkboxes were difficult to see and the "Send reminder" button
wasn't disabled when no recipients were selected. This PR disables the
sending button when there's no selected recipient and improves the
checkboxes visibility.
2025-09-25 22:23:07 +10:00
23 changed files with 174 additions and 29 deletions
@@ -18,6 +18,11 @@ The guide assumes you have a Documenso account. If you don't, you can create a f
Navigate to the [Documenso dashboard](https://app.documenso.com/documents) and click on the "Add a document" button. Select the document you want to upload and wait for the upload to complete.
<Callout type="info">
The maximum file size for uploaded documents is 150MB in production. In staging, the limit is
50MB.
</Callout>
![Documenso dashboard](/document-signing/documenso-documents-dashboard.webp)
After the upload is complete, you will be redirected to the document's page. You can configure the document's settings and add recipients and fields here.
@@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Recipient, SigningStatus } from '@prisma/client';
import { History } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
@@ -85,6 +85,11 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
formState: { isSubmitting },
} = form;
const selectedRecipients = useWatch({
control: form.control,
name: 'recipients',
});
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
try {
await resendDocument({ documentId: document.id, recipients });
@@ -151,7 +156,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
<FormControl>
<Checkbox
className="h-5 w-5 rounded-full"
className="h-5 w-5 rounded-full border border-neutral-400"
value={recipient.id}
checked={value.includes(recipient.id)}
onCheckedChange={(checked: boolean) =>
@@ -182,7 +187,13 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
</Button>
</DialogClose>
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
<Button
className="flex-1"
loading={isSubmitting}
type="submit"
form={FORM_ID}
disabled={isSubmitting || selectedRecipients.length === 0}
>
<Trans>Send reminder</Trans>
</Button>
</div>
@@ -3,10 +3,11 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { TeamGlobalSettings } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { DocumentVisibility, OrganisationType } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { DATE_FORMATS } from '@documenso/lib/constants/date-formats';
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
@@ -86,8 +87,10 @@ export const DocumentPreferencesForm = ({
}: DocumentPreferencesFormProps) => {
const { t } = useLingui();
const { user, organisations } = useSession();
const currentOrganisation = useCurrentOrganisation();
const isPersonalLayoutMode = isPersonalLayout(organisations);
const isPersonalOrganisation = currentOrganisation.type === OrganisationType.PERSONAL;
const placeholderEmail = user.email ?? 'user@example.com';
@@ -331,7 +334,7 @@ export const DocumentPreferencesForm = ({
)}
/>
{!isPersonalLayoutMode && (
{!isPersonalLayoutMode && !isPersonalOrganisation && (
<FormField
control={form.control}
name="includeSenderDetails"
@@ -157,6 +157,7 @@ export const DocumentSigningDateField = ({
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
style={{ fontSize: `${parsedFieldMeta?.fontSize}px` }}
>
{localDateString}
</p>
@@ -132,7 +132,10 @@ export const DocumentSigningEmailField = ({
)}
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<DocumentSigningFieldsInserted
textAlign={parsedFieldMeta?.textAlign}
fontSize={parsedFieldMeta?.fontSize}
>
{field.customText}
</DocumentSigningFieldsInserted>
)}
@@ -27,14 +27,20 @@ type DocumentSigningFieldsInsertedProps = {
* Defaults to left.
*/
textAlign?: 'left' | 'center' | 'right';
/**
* The font size of the field in pixels.
*/
fontSize?: number;
};
export const DocumentSigningFieldsInserted = ({
children,
textAlign = 'left',
fontSize,
}: DocumentSigningFieldsInsertedProps) => {
return (
<div className="flex h-full w-full items-center overflow-hidden">
<div className="flex h-full w-full items-center overflow-visible">
<p
className={cn(
'text-foreground w-full whitespace-pre-wrap text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
@@ -43,6 +49,7 @@ export const DocumentSigningFieldsInserted = ({
'!text-right': textAlign === 'right',
},
)}
style={{ fontSize: `${fontSize}px` }}
>
{children}
</p>
@@ -139,7 +139,10 @@ export const DocumentSigningInitialsField = ({
)}
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<DocumentSigningFieldsInserted
textAlign={parsedFieldMeta?.textAlign}
fontSize={parsedFieldMeta?.fontSize}
>
{field.customText}
</DocumentSigningFieldsInserted>
)}
@@ -178,7 +178,10 @@ export const DocumentSigningNameField = ({
)}
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<DocumentSigningFieldsInserted
textAlign={parsedFieldMeta?.textAlign}
fontSize={parsedFieldMeta?.fontSize}
>
{field.customText}
</DocumentSigningFieldsInserted>
)}
@@ -253,7 +253,10 @@ export const DocumentSigningNumberField = ({
)}
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<DocumentSigningFieldsInserted
textAlign={parsedFieldMeta?.textAlign}
fontSize={parsedFieldMeta?.fontSize}
>
{field.customText}
</DocumentSigningFieldsInserted>
)}
@@ -160,6 +160,14 @@ export const DocumentSigningPageView = ({
return (
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
{document.team.teamGlobalSettings.brandingEnabled &&
document.team.teamGlobalSettings.brandingLogo && (
<img
src={`/api/branding/logo/team/${document.teamId}`}
alt={`${document.team.name}'s Logo`}
className="mb-4 h-12 w-12 md:mb-2"
/>
)}
<h1
className="block max-w-[20rem] truncate text-2xl font-semibold sm:mt-4 md:max-w-[30rem] md:text-3xl"
title={document.title}
@@ -250,7 +250,10 @@ export const DocumentSigningTextField = ({
)}
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<DocumentSigningFieldsInserted
textAlign={parsedFieldMeta?.textAlign}
fontSize={parsedFieldMeta?.fontSize}
>
{field.customText}
</DocumentSigningFieldsInserted>
)}
+1 -1
View File
@@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.6"
"version": "1.12.7"
}
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.6",
"version": "1.12.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.6",
"version": "1.12.7",
"workspaces": [
"apps/*",
"packages/*"
@@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.6",
"version": "1.12.7",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.6",
"version": "1.12.7",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",
@@ -504,7 +504,7 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
},
});
const insertedDate = DateTime.fromFormat(field?.customText ?? '', 'yyyy-MM-dd HH:mm');
const insertedDate = DateTime.fromFormat(field?.customText ?? '', 'yyyy-MM-dd hh:mm a');
expect(Math.abs(insertedDate.diff(now).minutes)).toBeLessThanOrEqual(1);
@@ -30,7 +30,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => {
await page.getByRole('option', { name: 'Australia/Perth' }).click();
// Set default date
await page.getByRole('combobox').filter({ hasText: 'yyyy-MM-dd HH:mm' }).click();
await page.getByRole('combobox').filter({ hasText: 'yyyy-MM-dd hh:mm AM/PM' }).click();
await page.getByRole('option', { name: 'DD/MM/YYYY', exact: true }).click();
await page.getByTestId('signature-types-trigger').click();
@@ -47,8 +47,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
// Set advanced options.
await page.getByRole('button', { name: 'Advanced Options' }).click();
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm' }).click();
await page.getByLabel('DD/MM/YYYY HH:mm').click();
await page.locator('button').filter({ hasText: 'YYYY-MM-DD hh:mm AM/PM' }).click();
await page.getByLabel('DD/MM/YYYY HH:mm', { exact: true }).click();
await page.locator('.time-zone-field').click();
await page.getByRole('option', { name: 'Etc/UTC' }).click();
@@ -150,8 +150,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
// Set advanced options.
await page.getByRole('button', { name: 'Advanced Options' }).click();
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm' }).click();
await page.getByLabel('DD/MM/YYYY HH:mm').click();
await page.locator('button').filter({ hasText: 'YYYY-MM-DD hh:mm AM/PM' }).click();
await page.getByLabel('DD/MM/YYYY HH:mm', { exact: true }).click();
await page.locator('.time-zone-field').click();
await page.getByRole('option', { name: 'Etc/UTC' }).click();
+45 -5
View File
@@ -2,22 +2,29 @@ import { DateTime } from 'luxon';
import { DEFAULT_DOCUMENT_TIME_ZONE } from './time-zones';
export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd HH:mm';
export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd hh:mm a';
export const VALID_DATE_FORMAT_VALUES = [
DEFAULT_DOCUMENT_DATE_FORMAT,
'yyyy-MM-dd',
'dd/MM/yyyy',
'MM/dd/yyyy',
'dd.MM.yyyy',
'yy-MM-dd',
'MMMM dd, yyyy',
'EEEE, MMMM dd, yyyy',
'dd/MM/yyyy hh:mm a',
'dd/MM/yyyy HH:mm',
'MM/dd/yyyy hh:mm a',
'MM/dd/yyyy HH:mm',
'dd.MM.yyyy',
'dd.MM.yyyy HH:mm',
'yyyy-MM-dd HH:mm',
'yy-MM-dd hh:mm a',
'yy-MM-dd HH:mm',
'yyyy-MM-dd HH:mm:ss',
'MMMM dd, yyyy hh:mm a',
'MMMM dd, yyyy HH:mm',
'EEEE, MMMM dd, yyyy hh:mm a',
'EEEE, MMMM dd, yyyy HH:mm',
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
] as const;
@@ -25,43 +32,76 @@ export const VALID_DATE_FORMAT_VALUES = [
export type ValidDateFormat = (typeof VALID_DATE_FORMAT_VALUES)[number];
export const DATE_FORMATS = [
{
key: 'yyyy-MM-dd_HH:mm_12H',
label: 'YYYY-MM-DD hh:mm AM/PM',
value: DEFAULT_DOCUMENT_DATE_FORMAT,
},
{
key: 'yyyy-MM-dd_HH:mm',
label: 'YYYY-MM-DD HH:mm',
value: DEFAULT_DOCUMENT_DATE_FORMAT,
value: 'yyyy-MM-dd HH:mm',
},
{
key: 'DDMMYYYY_TIME',
label: 'DD/MM/YYYY HH:mm',
value: 'dd/MM/yyyy HH:mm',
},
{
key: 'DDMMYYYY_TIME_12H',
label: 'DD/MM/YYYY HH:mm AM/PM',
value: 'dd/MM/yyyy hh:mm a',
},
{
key: 'MMDDYYYY_TIME',
label: 'MM/DD/YYYY HH:mm',
value: 'MM/dd/yyyy HH:mm',
},
{
key: 'MMDDYYYY_TIME_12H',
label: 'MM/DD/YYYY HH:mm AM/PM',
value: 'MM/dd/yyyy hh:mm a',
},
{
key: 'DDMMYYYYHHMM',
label: 'DD.MM.YYYY HH:mm',
value: 'dd.MM.yyyy HH:mm',
},
{
key: 'YYMMDD_TIME',
label: 'YY-MM-DD HH:mm',
value: 'yy-MM-dd HH:mm',
},
{
key: 'YYMMDD_TIME_12H',
label: 'YY-MM-DD HH:mm AM/PM',
value: 'yy-MM-dd hh:mm a',
},
{
key: 'YYYY_MM_DD_HH_MM_SS',
label: 'YYYY-MM-DD HH:mm:ss',
value: 'yyyy-MM-dd HH:mm:ss',
},
{
key: 'MonthDateYear_TIME',
label: 'Month Date, Year HH:mm',
value: 'MMMM dd, yyyy HH:mm',
},
{
key: 'MonthDateYear_TIME_12H',
label: 'Month Date, Year HH:mm AM/PM',
value: 'MMMM dd, yyyy hh:mm a',
},
{
key: 'DayMonthYear_TIME',
label: 'Day, Month Year HH:mm',
value: 'EEEE, MMMM dd, yyyy HH:mm',
},
{
key: 'DayMonthYear_TIME_12H',
label: 'Day, Month Year HH:mm AM/PM',
value: 'EEEE, MMMM dd, yyyy hh:mm a',
},
{
key: 'ISO8601',
label: 'ISO 8601',
@@ -91,6 +91,12 @@ export const getDocumentAndSenderByToken = async ({
select: {
name: true,
teamEmail: true,
teamGlobalSettings: {
select: {
brandingEnabled: true,
brandingLogo: true,
},
},
},
},
},
@@ -1,3 +1,5 @@
import { OrganisationType } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
@@ -104,6 +106,19 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
});
}
const isPersonalOrganisation = organisation.type === OrganisationType.PERSONAL;
const currentIncludeSenderDetails =
organisation.organisationGlobalSettings.includeSenderDetails;
const isChangingIncludeSenderDetails =
includeSenderDetails !== undefined && includeSenderDetails !== currentIncludeSenderDetails;
if (isPersonalOrganisation && isChangingIncludeSenderDetails) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Personal organisations cannot update the sender details',
});
}
await prisma.organisation.update({
where: {
id: organisationId,
@@ -1,7 +1,10 @@
import { Prisma } from '@prisma/client';
import { OrganisationType } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
@@ -97,6 +100,35 @@ export const updateTeamSettingsRoute = authenticatedProcedure
}
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId: team.organisationId,
userId: user.id,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
select: {
type: true,
organisationGlobalSettings: {
select: {
includeSenderDetails: true,
},
},
},
});
const isPersonalOrganisation = organisation?.type === OrganisationType.PERSONAL;
const currentIncludeSenderDetails =
organisation?.organisationGlobalSettings.includeSenderDetails;
const isChangingIncludeSenderDetails =
includeSenderDetails !== undefined && includeSenderDetails !== currentIncludeSenderDetails;
if (isPersonalOrganisation && isChangingIncludeSenderDetails) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Personal teams cannot update the sender details',
});
}
await prisma.team.update({
where: {
id: teamId,
@@ -755,7 +755,6 @@ export const AddSignersFormPartial = ({
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
console.log('onSearchQueryChange', query);
field.onChange(query);
setRecipientSearchQuery(query);
}}
@@ -188,9 +188,11 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
}
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
const fontSize = fieldMeta && 'fontSize' in fieldMeta ? fieldMeta.fontSize : undefined;
const fontSizeStyle = fontSize ? { fontSize: `${fontSize}px` } : {};
return (
<div className="flex h-full w-full items-center overflow-hidden">
<div className="overflow-visibile flex h-full w-full items-center">
<p
className={cn(
'text-foreground w-full whitespace-pre-wrap text-left text-[clamp(0.07rem,25cqw,0.825rem)] duration-200',
@@ -200,6 +202,7 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
},
)}
style={fontSizeStyle}
>
{textToDisplay || labelToDisplay}
</p>