Compare commits

..

9 Commits

Author SHA1 Message Date
David Nguyen 5f17288af6 fix: rebase 2025-06-12 17:46:03 +10:00
David Nguyen e3ce7f94e6 chore: update build 2025-06-11 14:52:23 +10:00
Shubham Palriwala cad04f26e7 feat: sitemap auto-generation for docs (#1822) 2025-06-11 14:09:45 +10:00
Ephraim Duncan d27f0ee0ef fix: duplicate field bugs (#1685) 2025-06-11 13:26:19 +10:00
Ephraim Duncan fd2b413ed9 chore: increase wait times for tests (#1778) 2025-06-11 13:25:21 +10:00
Catalin Pit d11ec8fa2a feat: show field coordinates in devmode (#1802)
Show the fields coordinates when the `devmode` search param is present.
It's meant to help API users understand where to position the fields.
2025-06-11 12:28:39 +10:00
Damien B. b1127b4f0d chore: update readme 2025-06-11 10:42:32 +10:00
Lucas Smith be4244fb62 chore: add translations (#1832)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-06-11 10:39:33 +10:00
David Nguyen 504a0893ab chore: add organisation docs (#1831) 2025-06-10 20:54:36 +10:00
81 changed files with 873 additions and 284 deletions
+3 -3
View File
@@ -247,14 +247,14 @@ Now you can install the dependencies and build it:
```
npm i
npm run build:web
npm run build
npm run prisma:migrate-deploy
```
Finally, you can start it with:
```
cd apps/web
cd apps/remix
npm run start
```
@@ -275,7 +275,7 @@ After=network.target
Environment=PATH=/path/to/your/node/binaries
Type=simple
User=www-data
WorkingDirectory=/var/www/documenso/apps/web
WorkingDirectory=/var/www/documenso/apps/remix
ExecStart=/usr/bin/next start -p 3500
TimeoutSec=15
Restart=always
+5
View File
@@ -34,3 +34,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# next-sitemap output
/public/sitemap.xml
/public/robots.txt
/public/sitemap-*.xml
@@ -0,0 +1,5 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: 'https://docs.documenso.com', // Replace with your actual site URL
generateRobotsTxt: true, // Generates robots.txt
};
+3 -2
View File
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev -p 3002",
"build": "next build",
"build": "next build && next-sitemap",
"start": "next start -p 3002",
"lint:fix": "next lint --fix",
"clean": "rimraf .next && rimraf node_modules"
@@ -26,6 +26,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"next-sitemap": "^4.2.3",
"typescript": "5.6.2"
}
}
}
@@ -5,6 +5,7 @@
"title": "Development & Deployment"
},
"local-development": "Local Development",
"developer-mode": "Developer Mode",
"self-hosting": "Self Hosting",
"contributing": "Contributing",
"-- API & Integration Guides": {
@@ -0,0 +1,18 @@
---
title: Field Coordinates
description: Learn how to get the coordinates of a field in a document.
---
## Field Coordinates
Field coordinates represent the position of a field in a document. They are returned in the `pageX` and `pageY` properties of the field.
To enable field coordinates, you can use the `devmode` query parameter.
```bash
https://app.documenso.com/documents/<document-id>/edit?devmode=true
```
You should then see the coordinates on top of each field.
![Field Coordinates](/developer-mode/field-coordinates.webp)
+5 -4
View File
@@ -6,11 +6,12 @@
"title": "How To Use"
},
"get-started": "Get Started",
"profile": "User Profile",
"signing-documents": "Signing Documents",
"profile": "Public Profile",
"organisations": "Organisations",
"documents": "Documents",
"templates": "Templates",
"branding": "Branding",
"direct-links": "Direct Signing Links",
"teams": "Teams",
"-- Legal Overview": {
"type": "separator",
"title": "Legal Overview"
@@ -18,4 +19,4 @@
"fair-use": "Fair Use Policy",
"licenses": "Licenses",
"compliance": "Compliance"
}
}
@@ -0,0 +1,28 @@
---
title: Branding Preferences
description: Learn how to set the branding preferences for your team account.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Branding Preferences
Branding preferences allow you to set the default settings when emailing documents to your recipients.
## Preferences
Branding preferences can be set on either the organisation or team level.
By default, teams inherit the preferences from the organisation. You can override these preferences on the team level at any time.
To access the preferences, navigate to either the organisation or teams settings page and click the **Preferences** tab. This page contains both the preferences for documents and branding, the branding section is located at the bottom of the page.
![A screenshot of the organisation's document preferences page](/organisations/organisation-branding.webp)
On this page, you can:
- **Upload a Logo** - Upload your team's logo to be displayed instead of the default Documenso logo.
- **Set the Brand Website** - Enter the URL of your team's website to be displayed in the email communications sent by the team.
- **Add Additional Brand Details** - You can add additional information to display at the bottom of the emails sent by the team. This can include contact information, social media links, and other relevant details.
@@ -0,0 +1,5 @@
{
"sending-documents": "Sending Documents",
"document-preferences": "Document Preferences",
"document-visibility": "Document Visibility"
}
@@ -0,0 +1,42 @@
---
title: Preferences
description: Learn how to manage your team's global preferences.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Document Preferences
Document preferences allow you to set the default settings when creating new documents and templates.
For example, you can set the default language for documents sent by the team, or set the allowed signatures types.
## Preferences
Document preferences can be set on either the organisation or team level.
By default, teams inherit the preferences from the organisation. You can override these preferences on the team level at any time.
To access the preferences, navigate to either the organisation or teams settings page and click the **Preferences** tab.
![A screenshot of the organisation's document preferences page](/organisations/organisation-document-preferences.webp)
- **Document Visibility** - Set the default visibility of the documents created by team members. Learn more about [document visibility](/users/documents/document-visibility).
- **Default Document Language** - This setting allows you to set the default language for the documents uploaded in the organisation. The default language is used as the default language in the email communications with the document recipients.
- **Signature Settings** - Controls what signatures are allowed to be used when signing the documents.
- **Sender Details** - Set whether the sender's name should be included in the emails sent by the team. See more below [sender details](/users/documents/document-preferences#sender-details).
- **Include the Signing Certificate** - This setting controls whether the signing certificate should be included in the signed documents. If enabled, the signing certificate is included in the signed documents. If disabled, the signing certificate is not included in the signed documents. Regardless of this setting, the signing certificate is always available in the document's audit log page.
Document visibility, language and signature settings can be overriden on a per document basis.
### Sender Details
If the **Sender Details** setting is enabled, the emails sent by the team will include the sender's name. The email will say:
> "Example User" on behalf of "Example Team" has invited you to sign "document.pdf"
If the **Sender Details** setting is disabled, the emails sent by the team will not include the sender's name. The email will say:
> "Example Team" has invited you to sign "document.pdf"
@@ -5,19 +5,25 @@ description: Learn how to control the visibility of your team documents.
import { Callout } from 'nextra/components';
# Team's Document Visibility
# Document Visibility
The default document visibility option allows you to control who can view and access the documents uploaded to your team account. The document visibility can be set to one of the following options:
The default document visibility option allows you to control who can view and access the documents uploaded within a team.
This value can either be set in the [document preferences](/users/documents/document-preferences), or when you [create the document](/users/documents/send-document)
## Document Visibility Options
The document visibility can be set to one of the following options:
- **Everyone** - The document is visible to all team members.
- **Managers and above** - The document is visible to team members with the role of _Manager or above_ and _Admin_.
- **Admin only** - The document is only visible to the team's admins.
![A screenshot of the document visibility selector from the team's global preferences page](/teams/team-preferences-document-visibility.webp)
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [document preferences page](/users/documents/document-preferences) and selecting a different visibility option.
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [team's general preferences page](/users/teams/preferences) and selecting a different visibility option.
![Document visibility preference](/organisations/organisation-document-visibility.webp)
Here's how it works:
## How it works
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Everyone_", the document's visibility is set to "_EVERYONE_".
- The user can't change the visibility of the document in the document editor.
@@ -115,7 +115,7 @@ All fields can be placed anywhere on the document and resized as needed.
<Callout type="info">
Learn more about the available field types and how to use them on the [Fields
page](signing-documents/fields).
page](/users/documents/fields).
</Callout>
#### Signature Required
@@ -10,7 +10,7 @@ import { Callout, Steps } from 'nextra/components';
<Steps>
### Pick a Plan
The first step to start using Documenso is to pick a plan and create an account. At the moment of writing this guide, we have 3 plans available: Free, Individual, and Teams.
The first step to start using Documenso is to pick a plan and create an account. At the moment of writing this guide, we have 3 plans available: Free, Individual, Teams and Platform.
Explore each plan's features and choose the one that best suits your needs. The [pricing page](https://documen.so/pricing) has more information about the plans.
@@ -28,6 +28,6 @@ You can claim a premium username by upgrading to a paid plan. After upgrading to
### Optional: Create a Team
If you are working with others, you can create a team and invite your team members to collaborate on your documents. More information about teams is available in the [Teams section](/users/get-started/teams).
If you are working with others, you can create a team and invite your team members to collaborate on your documents. More information about teams is available in the [Teams section](/users/organisations/teams).
</Steps>
@@ -1,99 +0,0 @@
---
title: Teams
description: Learn how to create and manage teams in Documenso.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Teams
Documenso allows you to create teams to collaborate with others on creating and signing documents.
<Steps>
### Create a New Team
Anyone can create a team from their account by clicking on the "+" (plus) button in the "Teams" section from the account dropdown.
![Documenso account dropdown menu](/get-started-images/add-team.webp)
Each team is a separate entity with its members, documents, and templates. You can create as many teams as you like but remember that each team is billed separately.
<Callout type="info">You can transfer the ownership of the team at any time.</Callout>
### Name and URL
Clicking the "+" button will open a modal where you must pick your team's name and URL. The URL is the team's identifier and will link to the team's page and settings. An example URL would be:
```bash
https://app.documenso.com/t/<your-team-name>
```
![Documenso create team modal](/get-started-images/add-team-2.webp)
You can select a different name and URL for your team, but we recommend using the same or similar name.
### Invite Team Members
After creating the team, you can invite team members by navigating to the "Members" tab in the team settings and clicking the "Invite member" button.
To access the team settings, click on the team's name in the account dropdown and select the appropriate team. Lastly, click again on the avatar and then "Team Settings".
Or you can copy this URL:
```bash
https://app.documenso.com/t/<your-team-name>/settings/members
```
Once you click on the "Invite member" button, you will be prompted to enter the email address of the person you want to invite. You can also select the role of the person you are inviting.
![Invite team members in Documenso dashboard](/get-started-images/add-team-members-documenso.webp)
You can also bulk-invite members by uploading a CSV file with the email addresses and roles of the people you want to invite.
The table below shows how the CSV file should be structured:
| Email address | Role |
| -------------------------- | ------- |
| team-admin@documenso.com | Admin |
| team-manager@documenso.com | Manager |
| team-member@documenso.com | Member |
<Callout type="info">
The basic team plan includes 5 members. You can invite as many members as you like by upgrading
your team's seats on the team's billing page.
</Callout>
#### Roles
You can assign different permissions to team members based on their roles. The roles available are:
| Role | Create, Edit, Send Documents | Manage Users | Manage Admins | Settings | Billing | Delete/ Transfer |
| :-----: | :--------------------------: | :----------: | :-----------: | :------: | :-----: | :--------------: |
| Member | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Manager | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Admin | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Owner | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Set a Team Email
You can add a team email to make signing and sending documents easier. Adding a team email allows you to:
- See a signing request sent to this email (Team Inbox)
- See all documents sent on behalf of the team
### (Optional) Transfer Team Ownership
You can transfer the team's ownership at any time. To do this, navigate to the "General" tab in the team settings and click the "Transfer team" button.
Use this URL to get to the team settings:
```bash
https://app.documenso.com/t/<your-team-name>/settings
```
### [Send your First Document](https://app.documenso.com/)
</Steps>
@@ -0,0 +1,7 @@
{
"index": "Introduction",
"members": "Members",
"groups": "Groups",
"teams": "Teams",
"billing": "Billing"
}
@@ -0,0 +1,19 @@
---
title: Billing
description: Learn how to manage your organisation's billing and subscription.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
### Billing and Subscription Management
Organisations handle billing centrally, making it easier to manage:
- **Unified Billing**: One subscription covers all teams in the organisation
- **Seat Management**: Add or remove seats across all teams automatically (Teams plan)
You can change plans, view invoices and manage your subscription from the billing page which is accessible from the organisation settings.
![A screenshot of the organisation's billing page](/organisations/organisations-billing.webp)
@@ -0,0 +1,75 @@
---
title: Preferences
description: Learn how to manage your team's global preferences.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Organisation Groups
Organisation groups are a powerful administrative tool that streamlines user management across your entire organisation. Instead of manually assigning individual users to multiple teams, groups allow you to manage access at scale.
This automated approach ensures consistent permissions while reducing administrative overhead for tasks like onboarding employees or managing contractor access.
## Understanding groups
### Key Benefits
- **Instant Access Management**: New hires get immediate, appropriate access across all relevant teams
- **Bulk Operations**: Remove an entire group (like a departing contractor team) and all members lose access simultaneously
- **Role Consistency**: Ensure the same role is applied consistently across teams—no more accidentally giving admin access when member access was intended
- **Audit Trail**: Easily track which groups have access to which teams
### Example use case: Legal Compliance Team
Imagine you have a legal compliance team that needs access to review documents across all departments. Instead of manually adding each legal team member to every departmental team (Sales, Marketing, HR, Operations), you can:
1. Create a "Legal Compliance" group with the "Member" Organisation Role
2. Add legal team members to this group
3. Assign the "Legal Compliance" group to the required teams
Now, when Sarah from Legal joins the company, you can simply add her to the "Legal Compliance" group. Once added, she automatically gains access to all teams the "Legal Compliance" group is assigned to.
When John from Legal leaves the company, you remove him from the group and his access is instantly revoked across all teams.
## Getting started with groups
Navigate to the "Groups" section in your organisation settings to create and manage groups.
There are two types of roles when using groups:
- **Organisation Role**: A global organisation role given to all members of the group
- **Team Role**: A team role you select when assigning the group to a team
You should generally have the "Organisation Role" set to "Organisation Member", otherwise these members would by default have access to all teams anyway due to the high organisation role.
### Creating Custom Groups
When creating a custom group, you can:
1. **Name the Group**: Give it a descriptive name that reflects its purpose
2. **Set Organisation Role**: Define the default **organisation role** for group members
3. **Add Members**: Include organisation members in the group
![Organisation group creation](/organisations/organisation-group-create.webp)
### Manage Custom Groups
By clicking the "Manage" button on a custom group, you can view all teams it is assigned to and modify the group's settings.
![Organisation group management](/organisations/organisation-group-manage.webp)
### Assigning a group to a team
To assign a group to a team, you need to navigate to the team settings and click the "Groups" tab.
![Organisation group assignment](/organisations/organisation-group-assignment.webp)
From here, click the "Add groups" button to begin the process of assigning a group to a team. Once you have added the group you can see that the members have been automatically added to the team in the members tab.
## What's next?
- [Create Your First Team](/users/organisations/teams)
- [Manage Default Settings](/users/documents/document-preferences)
@@ -0,0 +1,65 @@
---
title: Organisations
description: Learn how to create and manage organisations in Documenso.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Organisations
Organisations allow you to manage multiple teams and users under a single managed entity. This powerful feature enables enterprise-level collaboration and streamlined management across your entire organisation.
## What are Organisations?
Organisations are the top-level entity in Documenso's hierarchy structure:
![Organisations diagram](/organisations/organisations-basic-diagram.webp)
Each organisation can contain multiple teams, and each team can have multiple members. This structure provides:
- **Centralized Management**: Control multiple teams from a single organisational dashboard
- **Unified Billing**: Manage billing and subscriptions at the organisation level
- **Access Control**: Define roles and groups across the entire organisation
- **Group Management**: Create custom groups to organise members and control team access
- **Global Settings**: Apply consistent settings across all teams in your organisation
## Create a new organisation
You can create multiple organisations, but each organisation will be billed separately.
<Steps>
### Creating Organisations
To create a new organisation, navigate to the organisation section in your account settings and click the "Create Organisation" button.
![Create organisation in Documenso dashboard](/organisations/organisations-create.webp)
### Select your plan
Choose from our range of plans for your new organisation. If you want to instead upgrade your current organisation, you can do so by going into your settings billing page and upgrade it there.
### Name setup
When creating an organisation, you'll need to provide:
- **Organisation Name**: The display name for your organisation
</Steps>
Once your organisation is established, you can create teams to organise your work and collaborate effectively. Each team operates independently but inherits organisation-level settings and branding.
## Best Practices for Organisation Management
1. **Use groups effectively**: Leverage groups to simplify permission management
2. **Set default settings**: Configure organisation-wide settings for consistency
## What's next?
- [Create Your First Team](/users/organisations/teams)
- [Invite Organisation Members](/users/organisations/members)
- [Create Organisation Groups](/users/organisations/groups)
- [Manage Default Settings](/users/documents/document-preferences)
- [Manage Default Branding](/users/branding)
@@ -0,0 +1,65 @@
---
title: Members
description: Learn how to invite and manage your organisation's members.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Organisation Members
Organisation members are the core users of your organisation. They are the ones who can access the team resources and collaborate with other members.
## Organisation Roles
You can assign different permissions to organisation members by using roles. The roles available are:
| Role | Manage Settings/Teams/Members | Billing | Delete Organisation |
| :------------------: | :---------------------------: | :-----: | ------------------- |
| Organisation Owner | ✅ | ✅ | ✅ |
| Organisation Admin | ✅ | ✅ | ✅ |
| Organisation Manager | ✅ | ❌ | ❌ |
| Organisation Member | ❌ | ❌ | ❌ |
<Callout type="info">
Organisation admins and managers will automatically have access to all teams as the "Team Admin"
role. When creating a team you can also decide whether to automatically allow normal members to
access it by default as well.
</Callout>
## Invite Organisation Members
To invite organisation members, you need to be an organisation owner, admin or manager.
1. Open the menu switcher top right
2. Hover over your new organisation and click the settings icon
3. Navigate to the "Members" tab
4. Click "Invite Member"
Once you click on the "Invite member" button, you will be prompted to enter the email address of the person you want to invite. You can also select the role of the person you are inviting.
![Invite organisation members](/organisations/organisations-member-invite.webp)
You can also bulk-invite members by uploading a CSV file with the email addresses and roles of the people you want to invite.
The table below shows how the CSV file should be structured:
| Email address | Role |
| ------------------------- | ------- |
| org-admin@documenso.com | Admin |
| org-manager@documenso.com | Manager |
| org-member@documenso.com | Member |
<Callout type="info">
The basic team plan includes 5 organisation members. Going over the 5 members will charge your
organisation according to the seat plan pricing.
</Callout>
## Manage Organisation Members
On the same page, you can change the organisation member's roles or remove them from the organisation.
## What's next?
- [Use groups to organise your members](/users/organisations/groups)
@@ -0,0 +1,121 @@
---
title: Teams
description: Learn how to create and manage teams in Documenso.
---
import Image from 'next/image';
import { Callout, Steps } from 'nextra/components';
# Teams
Documenso teams allow you to collaborate with others on creating, sending and receiving documents within your organisation. Teams operate within the organisational structure and inherit settings and branding from their parent organisation.
## Team Structure
Teams provide focused collaboration spaces while benefiting from organisation-level management and settings.
Each team within an organisation has its own:
- Team members and roles
- Documents and templates
- Team-specific settings (that can override organisation defaults)
- Team email and branding (if enabled)
## Creating a Team
Only members with the "Organisation Admin" or "Organisation Manager" role can create teams.
<Steps>
### Create Team
To create a team, navigate to the organisation settings page and click the "Teams" tab. Then you can click the "Create Team" button.
![Create Team Dialog](/teams/team-create.webp)
### Name and URL
When creating a team, you'll need to provide:
- **Team Name**: The display name for your team
- **Team URL**: A unique identifier for your team
The team URL will follow this format:
```bash
https://app.documenso.com/t/<team-url>
```
You can select different names and URLs for your team, but we recommend using the same or similar names for consistency.
![Documenso create team modal](/teams/team-create-dialog.webp)
You can also decide whether to automatically inherit members from the organisation into the team. This means that all members of the organisation will have access to this team.
Members with the "Organisation Admin" or "Organisation Manager" role will be assigned as "Team Admin" regardless of this setting. This will only affect members with the "Organisation Member" role, who will be added to the team as a "Team Member".
Disabling this setting will remove all these members automatically. This can always be turned on or off later in the teams member settings page.
</Steps>
## Team Members
After creating the team, you can add organisation members into your team using two methods:
- Directly adding members to the team
- Add members using groups
### Directly adding members
1. Navigate to the team settings member page
2. Click the "Add Members" button
3. Choose which members you want to add
4. Assign the team roles for each team member
If you want to add people outside of the organisation, you will need to invite them to the organisation first.
See the [organisation members](/users/organisations/members#invite-organisation-members) page for more information.
### Adding members using groups
1. Navigate to the teams settings groups page
2. Click the "Add groups" button
3. Choose which groups you want to add
4. Assign the team roles for each group
See the [organisation groups](/users/organisations/groups) page for more information.
### Team Member Roles
You can assign different permissions to team members based on their roles. The roles available are:
| Role | Manage Documents | Manage Team | Delete Team |
| :----------: | :--------------: | :---------: | :---------: |
| Team Admin | ✅ | ✅ | ✅ |
| Team Manager | ✅ | ✅ | ❌ |
| Team Member | ✅ | ❌ | ❌ |
These roles can be used for document visibility and management as well.
## Set a Team Email
You can add a team email which allows you to:
- See signing requests sent to this email (Team Inbox)
- See documents sent from this email
- Send documents on behalf of the team
- Maintain consistent team branding in communications
## Team Settings and Branding
You can override preferences and settings from the Organisation on the Team level. See the following pages for more information:
- [Document preferences](/users/documents/document-preferences)
- [Branding preferences](/users/branding/branding-preferences)
## What's next?
- [Send your first document](/users/documents/sending-documents)
- [Setup your default document preferences](/users/documents/document-preferences)
- [Setup your default branding preferences](/users/branding)
+4 -2
View File
@@ -1,5 +1,5 @@
---
title: User Profile
title: Public Profile
description: Learn how to set up your public profile on Documenso.
---
@@ -15,7 +15,7 @@ Documenso allows you to create a public profile to share your templates for anyo
### Navigate to Your Profile Settings
Click on your profile picture in the top right corner and select "User settings". Then, navigate to the "Public Profile" tab to configure your profile.
Click on your profile picture in the top right corner and select "Settings" or "Team Settings". Then, navigate to the "Public Profile" tab to configure your profile.
![The profile settings page](/public-profile/documenso-public-profile-settings.webp)
@@ -45,6 +45,8 @@ You can choose to make your profile public or private. Only you can access it if
To make your profile public, toggle the switch to the right ("Show") at the top right-hand side of the page.
![An example of a enabling a public profile on Documenso](/public-profile/documenso-enable-public-profile-settings.webp)
### (Optional) Link Templates
Linking templates to your profile is optional, but it's what makes your profile helpful. Linking templates allow people to sign documents directly from your profile. As a result, we recommend linking at least one template you want to share with others.
@@ -1,4 +0,0 @@
{
"index": "Send Documents",
"fields": "Document Fields"
}
@@ -1,6 +0,0 @@
{
"preferences": "Preferences",
"document-visibility": "Document Visibility",
"sender-details": "Email Sender Details",
"branding-preferences": "Branding Preferences"
}
@@ -1,16 +0,0 @@
---
title: Branding Preferences
description: Learn how to set the branding preferences for your team account.
---
# Branding Preferences
You can set the branding preferences for your team account by going to the **Branding Preferences** tab in the team's settings dashboard.
![A screenshot of the team's branding preferences page](/teams/team-branding-preferences.webp)
On this page, you can:
- **Upload a Logo** - Upload your team's logo to be displayed instead of the default Documenso logo.
- **Set the Brand Website** - Enter the URL of your team's website to be displayed in the email communications sent by the team.
- **Add Additional Brand Details** - You can add additional information to display at the bottom of the emails sent by the team. This can include contact information, social media links, and other relevant details.
@@ -1,19 +0,0 @@
---
title: Preferences
description: Learn how to manage your team's global preferences.
---
# Preferences
You can manage your team's global preferences by clicking on the **Preferences** tab in the team's settings dashboard.
![A screenshot of the team's global preferences page](/teams/team-preferences.webp)
The preferences page allows you to update the following settings:
- **Document Visibility** - Set the default visibility of the documents created by team members. Learn more about [document visibility](/users/teams/document-visibility).
- **Default Document Language** - This setting allows you to set the default language for the documents uploaded in the team account. The default language is used as the default language in the email communications with the document recipients. You can change the language for individual documents when uploading them.
- **Sender Details** - Set whether the sender's name should be included in the emails sent by the team. Learn more about [sender details](/users/teams/sender-details).
- **Typed Signature** - It controls whether the document recipients can sign the documents with a typed signature or not. If enabled, the recipients can sign the document using either a drawn or a typed signature. If disabled, the recipients can only sign the documents usign a drawn signature. This setting can also be changed for individual documents when uploading them.
- **Include the Signing Certificate** - This setting controls whether the signing certificate should be included in the signed documents. If enabled, the signing certificate is included in the signed documents. If disabled, the signing certificate is not included in the signed documents. Regardless of this setting, the signing certificate is always available in the document's audit log page.
- **Branding Preferences** - Set the branding preferences and defaults for the team account. Learn more about [branding preferences](/users/teams/branding-preferences).
@@ -1,14 +0,0 @@
---
title: Email Sender Details
description: Learn how to update the sender details for your team's email notifications.
---
## Sender Details
If the **Sender Details** setting is enabled, the emails sent by the team will include the sender's name. The email will say:
> "Example User" on behalf of "Example Team" has invited you to sign "document.pdf"
If the **Sender Details** setting is disabled, the emails sent by the team will not include the sender's name. The email will say:
> "Example Team" has invited you to sign "document.pdf"
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

@@ -142,11 +142,11 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
.with('SELECT', () => (
<DialogHeader>
<DialogTitle>
<Trans>Add members</Trans>
<Trans>Add groups</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Select members or groups of members to add to the team.</Trans>
<Trans>Select groups of members to add to the team.</Trans>
</DialogDescription>
</DialogHeader>
))
@@ -0,0 +1,54 @@
import { useEffect } from 'react';
import type { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { useRevalidator } from 'react-router';
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
type DocumentType = {
id: number;
status: DocumentStatus;
deletedAt: Date | null;
recipients: Array<{
role: RecipientRole;
signingStatus: SigningStatus;
}>;
};
export type DocumentProcessingPollProps = {
documents?: DocumentType[] | DocumentType;
};
export const DocumentProcessingPoll = ({ documents }: DocumentProcessingPollProps) => {
const { revalidate } = useRevalidator();
useEffect(() => {
if (!documents) {
return;
}
const documentArray = Array.isArray(documents) ? documents : [documents];
if (documentArray.length === 0) {
return;
}
const hasProcessingDocuments = documentArray.some((document) =>
isDocumentBeingProcessed(document),
);
if (!hasProcessingDocuments) {
return;
}
const interval = setInterval(() => {
if (window.document.hasFocus()) {
void revalidate();
}
}, 3000);
return () => clearInterval(interval);
}, [documents, revalidate]);
return null;
};
@@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { CheckCircle2, Clock, File, XCircle } from 'lucide-react';
import { CheckCircle2, Clock, File, Loader2, XCircle } from 'lucide-react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
@@ -15,6 +15,7 @@ type FriendlyStatus = {
labelExtended: MessageDescriptor;
icon?: LucideIcon;
color: string;
animate?: boolean;
};
export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
@@ -55,20 +56,31 @@ export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus>
},
};
const PROCESSING_STATUS: FriendlyStatus = {
label: msg`Processing`,
labelExtended: msg`Document processing`,
icon: Loader2,
color: 'text-blue-600 dark:text-blue-300',
animate: true,
};
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
status: ExtendedDocumentStatus;
inheritColor?: boolean;
isProcessing?: boolean;
};
export const DocumentStatus = ({
className,
status,
inheritColor,
isProcessing,
...props
}: DocumentStatusProps) => {
const { _ } = useLingui();
const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
const statusConfig = isProcessing ? PROCESSING_STATUS : FRIENDLY_STATUS_MAP[status];
const { label, icon: Icon, color, animate } = statusConfig;
return (
<span className={cn('flex items-center', className)} {...props}>
@@ -76,6 +88,7 @@ export const DocumentStatus = ({
<Icon
className={cn('mr-2 inline-block h-4 w-4', {
[color]: !inheritColor,
'animate-spin': animate,
})}
/>
)}
@@ -9,7 +9,7 @@ import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { isDocumentBeingProcessed, isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@@ -77,7 +77,12 @@ export const DocumentsTable = ({
{
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
cell: ({ row }) => (
<DocumentStatus
status={row.original.status}
isProcessing={isDocumentBeingProcessed(row.original)}
/>
),
size: 140,
},
{
@@ -6,6 +6,7 @@ import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
import { trpc } from '@documenso/trpc/react';
import {
Accordion,
@@ -24,6 +25,7 @@ import {
import { useToast } from '@documenso/ui/primitives/use-toast';
import { AdminDocumentDeleteDialog } from '~/components/dialogs/admin-document-delete-dialog';
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
import { DocumentStatus } from '~/components/general/document/document-status';
import { AdminDocumentRecipientItemTable } from '~/components/tables/admin-document-recipient-item-table';
@@ -69,7 +71,10 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<div className="flex items-start justify-between">
<div className="flex items-center gap-x-4">
<h1 className="text-2xl font-semibold">{document.title}</h1>
<DocumentStatus status={document.status} />
<DocumentStatus
status={document.status}
isProcessing={isDocumentBeingProcessed(document)}
/>
</div>
{document.deletedAt && (
@@ -162,6 +167,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<hr className="my-4" />
{document && <AdminDocumentDeleteDialog document={document} />}
<DocumentProcessingPoll documents={document} />
</div>
);
}
@@ -8,6 +8,7 @@ import { Link, useSearchParams } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
@@ -75,7 +76,12 @@ export default function AdminDocumentsPage() {
{
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
cell: ({ row }) => (
<DocumentStatus
status={row.original.status}
isProcessing={isDocumentBeingProcessed(row.original)}
/>
),
},
{
header: _(msg`Owner`),
@@ -10,6 +10,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { isDocumentBeingProcessed } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
@@ -23,6 +24,7 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import {
DocumentStatus as DocumentStatusComponent,
@@ -128,6 +130,7 @@ export default function DocumentPage() {
<DocumentStatusComponent
inheritColor
status={document.status}
isProcessing={isDocumentBeingProcessed(document)}
className="text-muted-foreground"
/>
@@ -241,6 +244,8 @@ export default function DocumentPage() {
</div>
</div>
</div>
<DocumentProcessingPoll documents={document} />
</div>
);
}
@@ -3,8 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { OrganisationType } from '@prisma/client';
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { useNavigate, useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
@@ -29,6 +28,7 @@ import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
import { DocumentSearch } from '~/components/general/document/document-search';
import { DocumentStatus } from '~/components/general/document/document-status';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
@@ -346,6 +346,8 @@ export default function DocumentsPage() {
}}
/>
)}
<DocumentProcessingPoll documents={data?.data} />
</div>
</div>
@@ -26,6 +26,7 @@ import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentProcessingPoll } from '~/components/general/document/document-processing-poll';
import { DocumentSearch } from '~/components/general/document/document-search';
import { DocumentStatus } from '~/components/general/document/document-status';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
@@ -314,6 +315,8 @@ export default function DocumentsPage() {
}}
/>
)}
<DocumentProcessingPoll documents={data?.data} />
</div>
</div>
@@ -3,8 +3,14 @@ import { useEffect } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
import {
type Document,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch, Loader2 } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
@@ -16,10 +22,11 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getAllRecipientsByDocumentId } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDialog } from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
@@ -50,9 +57,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 });
}
const [fields, recipient] = await Promise.all([
const [fields, recipient, allRecipients] = await Promise.all([
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
getAllRecipientsByDocumentId({ documentId: document.id }),
]);
if (!recipient) {
@@ -66,17 +74,26 @@ export async function loader({ params, request }: Route.LoaderArgs) {
userId: user?.id,
});
const isDocumentWaitingForSignatureFromOthers =
allRecipients.length > 1 &&
allRecipients.some(
(r) => r.role !== RecipientRole.CC && r.signingStatus !== SigningStatus.SIGNED,
);
if (!isDocumentAccessValid) {
return {
isDocumentAccessValid: false,
recipientEmail: recipient.email,
isDocumentWaitingForSignatureFromOthers,
} as const;
}
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
const isExistingUser = await getUserByEmail({ email: recipient.email })
.then((u) => !!u)
.catch(() => false);
const isExistingUser = recipient.email
? await getUserByEmail({ email: recipient.email })
.then((u) => !!u)
.catch(() => false)
: false;
const recipientName =
recipient.name ||
@@ -93,6 +110,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
signatures,
document,
recipient,
isDocumentWaitingForSignatureFromOthers,
};
}
@@ -110,10 +128,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
document,
recipient,
recipientEmail,
isDocumentWaitingForSignatureFromOthers,
} = loaderData;
if (!isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={recipientEmail} />;
return <DocumentSigningAuthPageView email={recipientEmail ?? ''} />;
}
return (
@@ -142,7 +161,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
{/* Card with recipient */}
<SigningCard3D
name={recipientName}
name={recipientName ?? ''}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
@@ -153,7 +172,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
{recipient.role === RecipientRole.APPROVER && <Trans>Document Approved</Trans>}
</h2>
{match({ status: document.status, deletedAt: document.deletedAt })
{match({
status: document.status,
deletedAt: document.deletedAt,
waitingForOthers: isDocumentWaitingForSignatureFromOthers,
})
.with({ status: DocumentStatus.COMPLETED }, () => (
<div className="text-documenso-700 mt-4 flex items-center text-center">
<CheckCircle2 className="mr-2 h-5 w-5" />
@@ -162,7 +185,15 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</span>
</div>
))
.with({ deletedAt: null }, () => (
.with({ deletedAt: null, waitingForOthers: false }, () => (
<div className="mt-4 flex items-center text-center text-blue-600">
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
<span className="text-sm">
<Trans>Processing document...</Trans>
</span>
</div>
))
.with({ deletedAt: null, waitingForOthers: true }, () => (
<div className="mt-4 flex items-center text-center text-blue-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">
@@ -179,7 +210,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</div>
))}
{match({ status: document.status, deletedAt: document.deletedAt })
{match({
status: document.status,
deletedAt: document.deletedAt,
waitingForOthers: isDocumentWaitingForSignatureFromOthers,
})
.with({ status: DocumentStatus.COMPLETED }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>
@@ -187,7 +222,15 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</Trans>
</p>
))
.with({ deletedAt: null }, () => (
.with({ deletedAt: null, waitingForOthers: false }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>
All parties have completed their actions. The document is now being finalized. You
will receive an Email copy once it is ready.
</Trans>
</p>
))
.with({ deletedAt: null, waitingForOthers: true }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>
You will receive an Email copy of the signed document once everyone has signed.
@@ -244,7 +287,10 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</Trans>
</p>
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
<ClaimAccount
defaultName={recipientName ?? ''}
defaultEmail={recipient.email ?? ''}
/>
</div>
)}
@@ -1,28 +0,0 @@
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
import { Button } from '@documenso/ui/primitives/button';
import type { Route } from './+types/team.verify.transfer.$token';
export default function VerifyTeamTransferPage({ loaderData }: Route.ComponentProps) {
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">
<Trans>Invalid link</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>This link is invalid or has expired.</Trans>
</p>
<Button asChild>
<Link to="/">
<Trans>Return</Trans>
</Link>
</Button>
</div>
</div>
);
}
+43
View File
@@ -69,6 +69,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"next-sitemap": "^4.2.3",
"typescript": "5.6.2"
}
},
@@ -2122,6 +2123,13 @@
"node": ">=v14"
}
},
"node_modules/@corex/deepmerge": {
"version": "4.0.43",
"resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz",
"integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -25341,6 +25349,41 @@
"react-dom": ">=16.0.0"
}
},
"node_modules/next-sitemap": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz",
"integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==",
"dev": true,
"funding": [
{
"url": "https://github.com/iamvishnusankar/next-sitemap.git"
}
],
"license": "MIT",
"dependencies": {
"@corex/deepmerge": "^4.0.43",
"@next/env": "^13.4.3",
"fast-glob": "^3.2.12",
"minimist": "^1.2.8"
},
"bin": {
"next-sitemap": "bin/next-sitemap.mjs",
"next-sitemap-cjs": "bin/next-sitemap.cjs"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"next": "*"
}
},
"node_modules/next-sitemap/node_modules/@next/env": {
"version": "13.5.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz",
"integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==",
"dev": true,
"license": "MIT"
},
"node_modules/next-themes": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
@@ -197,7 +197,7 @@ test.describe('Template Field Prefill API v1', () => {
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
value: '98765',
},
{
id: radioField.id,
@@ -256,7 +256,7 @@ test.describe('Template Field Prefill API v1', () => {
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
value: '98765',
});
const documentRadioField = document?.fields.find(
@@ -329,7 +329,7 @@ test.describe('Template Field Prefill API v1', () => {
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
await expect(page.getByText('98765', { exact: true })).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
@@ -383,7 +383,7 @@ test.describe('Template Field Prefill API v1', () => {
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
@@ -403,7 +403,7 @@ test.describe('Template Field Prefill API v1', () => {
});
// Add NUMBER field
const numberField = await prisma.field.create({
await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
@@ -194,7 +194,7 @@ test.describe('Template Field Prefill API v2', () => {
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
value: '98765',
},
{
id: radioField.id,
@@ -253,7 +253,7 @@ test.describe('Template Field Prefill API v2', () => {
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
value: '98765',
});
const documentRadioField = document?.fields.find(
@@ -326,7 +326,7 @@ test.describe('Template Field Prefill API v2', () => {
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
await expect(page.getByText('98765', { exact: true })).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
@@ -380,7 +380,7 @@ test.describe('Template Field Prefill API v2', () => {
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
@@ -400,7 +400,7 @@ test.describe('Template Field Prefill API v2', () => {
});
// Add NUMBER field
const numberField = await prisma.field.create({
await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
@@ -633,7 +633,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
}
// Wait for the document to be signed.
await page.waitForTimeout(5000);
await page.waitForTimeout(10000);
const finalDocument = await prisma.document.findFirst({
where: { id: createdDocument?.id },
@@ -283,10 +283,10 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
}).toPass();
// Delete document.
await page.getByRole('menuitem', { name: 'Hide' }).click();
await page.getByRole('button', { name: 'Hide' }).click();
await page.waitForTimeout(1000);
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
await page.waitForTimeout(2000);
await expect(async () => {
await page
@@ -300,8 +300,10 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
}).toPass();
// Delete document.
await page.getByRole('menuitem', { name: 'Hide' }).click();
await page.getByRole('button', { name: 'Hide' }).click();
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
await page.waitForTimeout(2000);
// Check document counts.
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible();
@@ -49,9 +49,11 @@ test.describe('Signing Certificate Tests', () => {
}
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.getByRole('button', { name: 'Sign' }).click({ force: true });
await page.waitForURL(`/sign/${recipient.token}/complete`);
await page.waitForTimeout(10000);
await expect(async () => {
const { status } = await getDocumentByToken({
token: recipient.token,
@@ -3,8 +3,7 @@ import path from 'node:path';
import { prisma } from '@documenso/prisma';
import { DocumentVisibility, FolderType, TeamMemberRole } from '@documenso/prisma/client';
import { seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedBlankDocument, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -1328,7 +1327,7 @@ test('[TEAMS]: team admin can move manager document to admin folder', async ({ p
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
await managerDocRow.getByTestId('document-table-action-btn').click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
await page.getByRole('button', { name: 'Admin Folder' }).click();
@@ -1379,7 +1378,7 @@ test('[TEAMS]: team admin can move manager document to manager folder', async ({
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
await managerDocRow.getByTestId('document-table-action-btn').click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
await page.getByRole('button', { name: 'Manager Folder' }).click();
@@ -1430,7 +1429,7 @@ test('[TEAMS]: team admin can move manager document to everyone folder', async (
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
await managerDocRow.getByTestId('document-table-action-btn').click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
await page.getByRole('button', { name: 'Everyone Folder' }).click();
@@ -31,7 +31,7 @@ test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForURL(`/settings/organisations`);
await expect(page.getByText('No results found')).toBeVisible();
await expectTextToBeVisible(page, 'No results found');
await page.getByRole('button', { name: 'Create organisation' }).click();
await page.getByLabel('Organisation Name*').fill('test');
@@ -1,8 +1,11 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import {
seedBlankDocument,
seedDocuments,
seedTeamDocuments,
} from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -314,9 +317,9 @@ test('[TEAMS]: delete pending team document', async ({ page }) => {
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
}).toPass();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
await checkDocumentTabCount(page, 'Pending', 1);
@@ -359,9 +362,9 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
}).toPass();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
await checkDocumentTabCount(page, 'Completed', 0);
@@ -80,18 +80,27 @@ test('[TEAMS]: check signature modes can be disabled', async ({ page }) => {
await page.getByRole('button', { name: 'Update' }).first().click();
// Wait for the update to complete
const toast = page.locator('li[role="status"][data-state="open"]').first();
await expect(toast).toBeVisible();
await expect(toast.getByText('Document preferences updated', { exact: true })).toBeVisible();
const document = await seedTeamDocumentWithMeta(team);
// Go to document and check that the signatured tabs are correct.
// Go to document and check that the signature tabs are correct.
await page.goto(`/sign/${document.recipients[0].token}`);
await page.getByTestId('signature-pad-dialog-button').click();
// Wait for signature dialog to fully load
await page.waitForSelector('[role="dialog"]');
// Check the tab values
for (const tab of allTabs) {
if (tabs.includes(tab)) {
await expect(page.getByRole('tab', { name: tab })).toBeVisible();
} else {
await expect(page.getByRole('tab', { name: tab })).not.toBeVisible();
// await expect(page.getByRole('tab', { name: tab })).not.toBeVisible();
await expect(page.getByRole('tab', { name: tab })).toHaveCount(0);
}
}
}
@@ -297,10 +297,22 @@ test('[TEMPLATE]: should create a document from a template with custom document'
},
});
const expectedDocumentDataType =
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3'
? DocumentDataType.S3_PATH
: DocumentDataType.BYTES_64;
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
expect(document.documentData.type).toEqual(expectedDocumentDataType);
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
} else {
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
expect(document.documentData.data).toBeTruthy();
expect(document.documentData.initialData).toBeTruthy();
}
});
/**
@@ -378,11 +390,23 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
},
});
const expectedDocumentDataType =
process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3'
? DocumentDataType.S3_PATH
: DocumentDataType.BYTES_64;
expect(document.teamId).toEqual(team.id);
expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
expect(document.documentData.type).toEqual(expectedDocumentDataType);
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
} else {
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
expect(document.documentData.data).toBeTruthy();
expect(document.documentData.initialData).toBeTruthy();
}
});
/**
@@ -178,4 +178,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
// Add a longer waiting period to ensure document status is updated
await page.waitForTimeout(3000);
});
@@ -27,6 +27,9 @@ test('[USER] can sign up with email and password', async ({ page }: { page: Page
await page.waitForURL('/unverified-account');
// Wait to ensure token is created in the database
await page.waitForTimeout(2000);
const { token } = await extractUserVerificationToken(email);
const team = await prisma.team.findFirstOrThrow({
+23 -8
View File
@@ -17,9 +17,14 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
await page.goto('http://localhost:3000/signin');
await page.getByRole('link', { name: 'Forgot your password?' }).click();
await page.getByRole('textbox', { name: 'Email' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
await page.getByRole('button', { name: 'Reset Password' }).click();
await expect(page.locator('body')).toContainText('Reset email sent');
await expect(page.locator('body')).toContainText('Reset email sent', { timeout: 10000 });
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
where: {
@@ -33,16 +38,26 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
await page.goto(`http://localhost:3000/reset-password/${foundToken.token}`);
// Assert that password cannot be same as old password.
await page.getByRole('textbox', { name: 'Password', exact: true }).fill(oldPassword);
await page.getByRole('textbox', { name: 'Repeat Password' }).fill(oldPassword);
await page.getByLabel('Password', { exact: true }).fill(oldPassword);
await page.getByLabel('Repeat Password').fill(oldPassword);
// Ensure both fields are filled before clicking
await expect(page.getByLabel('Password', { exact: true })).toHaveValue(oldPassword);
await expect(page.getByLabel('Repeat Password')).toHaveValue(oldPassword);
await page.getByRole('button', { name: 'Reset Password' }).click();
await expect(page.locator('body')).toContainText(
'Your new password cannot be the same as your old password.',
);
// Assert password reset.
await page.getByRole('textbox', { name: 'Password', exact: true }).fill(newPassword);
await page.getByRole('textbox', { name: 'Repeat Password' }).fill(newPassword);
await page.getByLabel('Password', { exact: true }).fill(newPassword);
await page.getByLabel('Repeat Password').fill(newPassword);
// Ensure both fields are filled before clicking
await expect(page.getByLabel('Password', { exact: true })).toHaveValue(newPassword);
await expect(page.getByLabel('Repeat Password')).toHaveValue(newPassword);
await page.getByRole('button', { name: 'Reset Password' }).click();
await expect(page.locator('body')).toContainText('Your password has been updated successfully.');
@@ -73,9 +88,9 @@ test('[USER] can reset password via user settings', async ({ page }: { page: Pag
redirectPath: '/settings/security',
});
await page.getByRole('textbox', { name: 'Current password' }).fill(oldPassword);
await page.getByRole('textbox', { name: 'New password' }).fill(newPassword);
await page.getByRole('textbox', { name: 'Repeat password' }).fill(newPassword);
await page.getByLabel('Current password').fill(oldPassword);
await page.getByLabel('New password').fill(newPassword);
await page.getByLabel('Repeat password').fill(newPassword);
await page.getByRole('button', { name: 'Update password' }).click();
await expect(page.locator('body')).toContainText('Password updated');
+7 -3
View File
@@ -24,19 +24,23 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 4 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: [['html'], ['list']],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: 'on',
video: 'retain-on-failure',
/* Add explicit timeouts for actions */
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
timeout: 30_000,
timeout: 60_000,
/* Configure projects for major browsers */
projects: [
@@ -30,3 +30,26 @@ export const getRecipientsForDocument = async ({
return recipients;
};
export interface GetAllRecipientsByDocumentIdOptions {
documentId: number;
}
export const getAllRecipientsByDocumentId = async ({
documentId,
}: GetAllRecipientsByDocumentIdOptions) => {
const recipients = await prisma.recipient.findMany({
where: {
documentId,
},
select: {
role: true,
signingStatus: true,
},
orderBy: {
id: 'asc',
},
});
return recipients;
};
+2 -2
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-10 12:05\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -1795,7 +1795,7 @@ msgstr "Porównaj szczegóły wszystkich planów i funkcji"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
msgid "Complete"
msgstr "Zakończono"
msgstr "Zakończ"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Approval"
+22 -1
View File
@@ -1,8 +1,29 @@
import type { Document } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
const status = typeof document === 'string' ? document : document.status;
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
};
export const isDocumentBeingProcessed = (document: {
status: DocumentStatus;
deletedAt: Date | null;
recipients: Array<{
role: RecipientRole;
signingStatus: SigningStatus;
}>;
}) => {
if (document.status !== DocumentStatus.PENDING || document.deletedAt !== null) {
return false;
}
const recipients = document.recipients.filter((r) => r.role !== RecipientRole.CC);
if (recipients.length === 0) {
return false;
}
return recipients.every((r) => r.signingStatus === SigningStatus.SIGNED);
};
@@ -16,7 +16,7 @@ export type DocumentDialogProps = {
/**
* A dialog which renders the provided document.
*/
export default function DocumentDialog({ trigger, documentData, ...props }: DocumentDialogProps) {
export const DocumentDialog = ({ trigger, documentData, ...props }: DocumentDialogProps) => {
const [documentLoaded, setDocumentLoaded] = useState(false);
const onDocumentLoad = () => {
@@ -58,4 +58,4 @@ export default function DocumentDialog({ trigger, documentData, ...props }: Docu
</DialogPortal>
</Dialog>
);
}
};
@@ -166,7 +166,6 @@ export const AddFieldsFormPartial = ({
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const [lastActiveField, setLastActiveField] = useState<TAddFieldsFormSchema['fields'][0] | null>(
null,
);
@@ -465,6 +464,7 @@ export const AddFieldsFormPartial = ({
append({
...copiedField,
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
pageX: copiedField.pageX + 3,
@@ -661,6 +661,8 @@ export const AddFieldsFormPartial = ({
passive={isFieldWithinBounds && !!selectedField}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}
onMouseEnter={() => setLastActiveField(field)}
onMouseLeave={() => setLastActiveField(null)}
onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
@@ -6,6 +6,7 @@ import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
import { useSearchParams } from 'react-router';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
@@ -35,6 +36,8 @@ export type FieldItemProps = {
onAdvancedSettings?: () => void;
onFocus?: () => void;
onBlur?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
recipientIndex?: number;
hasErrors?: boolean;
active?: boolean;
@@ -69,6 +72,7 @@ export const FieldItem = ({
onFieldDeactivate,
}: FieldItemProps) => {
const { _ } = useLingui();
const [searchParams] = useSearchParams();
const [coords, setCoords] = useState({
pageX: 0,
@@ -81,6 +85,8 @@ export const FieldItem = ({
const signerStyles = useRecipientColors(recipientIndex);
const isDevMode = searchParams.get('devmode') === 'true';
const advancedField = [
'NUMBER',
'RADIO',
@@ -233,6 +239,8 @@ export const FieldItem = ({
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
onDragStart={() => onFieldActivate?.()}
onResizeStart={() => onFieldActivate?.()}
onMouseEnter={() => onFocus?.()}
onMouseLeave={() => onBlur?.()}
enableResizing={!fixedSize}
resizeHandleStyles={{
bottom: { bottom: -8, cursor: 'ns-resize' },
@@ -303,6 +311,12 @@ export const FieldItem = ({
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
</div>
</div>
{isDevMode && (
<div className="text-muted-foreground absolute -top-6 left-0 right-0 text-center text-[10px]">
{`x: ${field.pageX.toFixed(2)}, y: ${field.pageY.toFixed(2)}`}
</div>
)}
</div>
{!disabled && settingsActive && (
@@ -209,6 +209,7 @@ export const AddTemplateFieldsFormPartial = ({
append({
...copiedField,
formId: nanoid(12),
nativeId: undefined,
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
signerId: selectedSigner?.id ?? copiedField.signerId,
signerToken: selectedSigner?.token ?? copiedField.signerToken,