Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e20561e91 | |||
| a2ec5f0fa1 | |||
| de8d13a4c1 | |||
| 495d61a11d | |||
| 90fdba8000 | |||
| aa1cada79b | |||
| 790b385849 | |||
| baa2c51123 | |||
| 1e585e06e6 | |||
| 5624484631 | |||
| 810e00da03 | |||
| eeeee2fa0e | |||
| c50a31a503 | |||
| 7360709795 |
@@ -65,6 +65,47 @@ jobs:
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
|
||||
- name: Build the chromium docker image
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile.chromium \
|
||||
--progress=plain \
|
||||
--build-arg TAG="$GIT_SHA" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:latest-chromium" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:latest-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
.
|
||||
|
||||
- name: Push the chromium docker image to DockerHub
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:latest-chromium"
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium"
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
|
||||
- name: Push the chromium docker image to GitHub Container Registry
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:latest-chromium"
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium"
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium"
|
||||
|
||||
create_and_publish_manifest:
|
||||
name: Create and publish manifest
|
||||
runs-on: ubuntu-latest
|
||||
@@ -125,6 +166,43 @@ jobs:
|
||||
docker manifest push documenso/documenso:$GIT_SHA
|
||||
docker manifest push documenso/documenso:$APP_VERSION
|
||||
|
||||
- name: Create and push DockerHub chromium manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:latest-chromium \
|
||||
--amend documenso/documenso-amd64:latest-chromium \
|
||||
--amend documenso/documenso-arm64:latest-chromium
|
||||
|
||||
docker manifest push documenso/documenso:latest-chromium
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:rc-chromium \
|
||||
--amend documenso/documenso-amd64:rc-chromium \
|
||||
--amend documenso/documenso-arm64:rc-chromium
|
||||
|
||||
docker manifest push documenso/documenso:rc-chromium
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$GIT_SHA-chromium \
|
||||
--amend documenso/documenso-amd64:$GIT_SHA-chromium \
|
||||
--amend documenso/documenso-arm64:$GIT_SHA-chromium
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$APP_VERSION-chromium \
|
||||
--amend documenso/documenso-amd64:$APP_VERSION-chromium \
|
||||
--amend documenso/documenso-arm64:$APP_VERSION-chromium
|
||||
|
||||
docker manifest push documenso/documenso:$GIT_SHA-chromium
|
||||
docker manifest push documenso/documenso:$APP_VERSION-chromium
|
||||
|
||||
- name: Create and push Github Container Registry manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
@@ -161,3 +239,40 @@ jobs:
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA
|
||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION
|
||||
|
||||
- name: Create and push Github Container Registry chromium manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:latest-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:latest-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:latest-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:latest-chromium
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:rc-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:rc-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:rc-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:rc-chromium
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$GIT_SHA-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$GIT_SHA-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$GIT_SHA-chromium
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$APP_VERSION-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$APP_VERSION-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$APP_VERSION-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA-chromium
|
||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION-chromium
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
|
||||
@@ -61,6 +61,6 @@ You can access the following services:
|
||||
- Main application - http://localhost:3000
|
||||
- Incoming Mail Access - http://localhost:9000
|
||||
- Database Connection Details:
|
||||
- Port: 54320
|
||||
- Connection: Use your favourite database client to connect to the database.
|
||||
- Port: 54320
|
||||
- Connection: Use your favorite database client to connect to the database.
|
||||
- S3 Storage Dashboard - http://localhost:9001
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Rate Limits
|
||||
description: Learn about the rate limits for the Documenso Public API.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Rate Limits
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Telemetry
|
||||
description: Learn about the telemetry data that Documenso collects from self-hosted instances.
|
||||
---
|
||||
|
||||
# Telemetry
|
||||
|
||||
Documenso collects anonymous telemetry data from self-hosted instances to help us understand how the software is being used and make improvements to the product. This telemetry is enabled by default, but you can easily disable it if you prefer.
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Learn how to use webhooks to receive real-time notifications about
|
||||
|
||||
# Webhooks
|
||||
|
||||
Webhooks are HTTP callbacks triggered by specific events. When the user subscribes to a specific event, and that event occurs, the webhook makes an HTTP request to the URL provided by the user. The request can be a simple notification or carry a payload with more information about the event.
|
||||
Webhooks are HTTP callbacks triggered by specific events. When you subscribe to a specific event and that event occurs, the webhook makes an HTTP request to the URL you provide. The request can be a simple notification or carry a payload with more information about the event.
|
||||
|
||||
Some of the common use cases for webhooks include:
|
||||
|
||||
@@ -25,13 +25,13 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
|
||||
|
||||
## Create a webhook subscription
|
||||
|
||||
You can create a webhook subscription from the user settings page. Click on your avatar in the top right corner of the dashboard and select "**[User settings](https://app.documenso.com/settings)**" from the dropdown menu.
|
||||
You can create a webhook subscription from the team settings page. Click your avatar in the top right corner of the dashboard and select "Team settings" from the dropdown menu.
|
||||
|
||||

|
||||

|
||||
|
||||
Then, navigate to the "**[Webhooks](https://app.documenso.com/settings/webhooks)**" tab, where you can see a list of your existing webhooks and create new ones.
|
||||
Then, navigate to the "Webhooks" tab, which takes you to the webhooks main page.
|
||||
|
||||

|
||||

|
||||
|
||||
Clicking on the "**Create Webhook**" button opens a modal to create a new webhook subscription.
|
||||
|
||||
@@ -41,7 +41,7 @@ To create a new webhook subscription, you need to provide the following informat
|
||||
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`.
|
||||
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
|
||||
|
||||

|
||||

|
||||
|
||||
After you have filled in the required information, click on the "**Create Webhook**" button to save your subscription.
|
||||
|
||||
@@ -49,7 +49,22 @@ The screenshot below illustrates a newly created webhook subscription.
|
||||
|
||||

|
||||
|
||||
You can edit or delete your webhook subscriptions by clicking the "**Edit**" or "**Delete**" buttons next to the webhook.
|
||||
You can edit, view the logs, or delete your webhook subscriptions by clicking the three dots (...) under the "Action" column. You can also access the webhook logs by clicking on the webhook subscription directly.
|
||||
|
||||

|
||||
|
||||
You can go even further and check the execution details of each call by clicking on a specific webhook call.
|
||||
|
||||

|
||||
|
||||
This page shows the details of the webhook call such as:
|
||||
|
||||
- status
|
||||
- event
|
||||
- date when the webhook was sent
|
||||
- response code
|
||||
- request body
|
||||
- response body and headers
|
||||
|
||||
## Webhook fields
|
||||
|
||||
@@ -619,18 +634,26 @@ Example payload for the `document.rejected` event:
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Events Testing
|
||||
## Webhook events testing
|
||||
|
||||
You can trigger test webhook events to test the webhook functionality. To trigger a test webhook, navigate to the [Webhooks page](/developers/webhooks) and click on the "Test Webhook" button.
|
||||
You can trigger test webhook events to test the webhook functionality. To do so, navigate to the webhook subscription details page and click the "Test" button.
|
||||
|
||||

|
||||

|
||||
|
||||
This opens a dialog where you can select the event type to test.
|
||||
|
||||

|
||||

|
||||
|
||||
Choose the appropriate event and click "Send Test Webhook." You’ll shortly receive a test payload from Documenso with sample data.
|
||||
Choose the event you want to test and click "Send". You’ll then receive a test payload from Documenso with sample data.
|
||||
|
||||
## Webhook events resending
|
||||
|
||||
To resend a webhook call, you need to navigate to the webhook call page and click the "Resend" button.
|
||||
|
||||

|
||||
|
||||
This will send the webhook event to the webhook URL again.
|
||||
|
||||
## Availability
|
||||
|
||||
Webhooks are available to individual users and teams.
|
||||
Webhooks are available to teams only.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Signature Levels
|
||||
description: Learn about the different signature levels for Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Signature Levels
|
||||
@@ -26,20 +31,20 @@ ensures the legal validity and enforceability of electronic signatures and recor
|
||||
|
||||
### Main Requirements
|
||||
|
||||
- [x] Intent to Sign: "Parties must demonstrate their intent to sign [..]"
|
||||
- [x] Consent: "The ESIGN Act requires that all parties involved in a transaction consent to the use of electronic signatures and records [..]"
|
||||
- [x] Consumer Disclosures: Before obtaining their consent, financial institutions must provide the consumer a clear and conspicuous statement informing the consumer [..]
|
||||
- [x] Record Retention: Electronic Records must be maintained for later access by signers.
|
||||
- [x] Security: The ESIGN Act does not mandate specific security measures, but it does require that parties take reasonable steps to ensure the security and integrity of electronic signatures and records. This may include implementing encryption, access controls, and authentication measures.
|
||||
- [x] **Intent to Sign**: "Parties must demonstrate their intent to sign [..]"
|
||||
- [x] **Consent**: "The ESIGN Act requires that all parties involved in a transaction consent to the use of electronic signatures and records [..]"
|
||||
- [x] **Consumer Disclosures**: Before obtaining their consent, financial institutions must provide the consumer a clear and conspicuous statement informing the consumer [..]
|
||||
- [x] **Record Retention**: Electronic Records must be maintained for later access by signers.
|
||||
- [x] **Security**: The ESIGN Act does not mandate specific security measures, but it does require that parties take reasonable steps to ensure the security and integrity of electronic signatures and records. This may include implementing encryption, access controls, and authentication measures.
|
||||
|
||||
## UETA (Uniform Electronic Transactions Act)
|
||||
|
||||
<Callout type="info" emoji="✅">
|
||||
Status: Compliant
|
||||
</Callout>
|
||||
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of electronic
|
||||
signatures and records in electronic transactions, ensuring they have the same validity and enforceability
|
||||
as paper documents and handwritten signatures.
|
||||
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of
|
||||
electronic signatures and records in electronic transactions, ensuring they have the same validity
|
||||
and enforceability as paper documents and handwritten signatures.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
@@ -50,9 +55,9 @@ _See [ESIGN](/users/compliance/signature-levels#-esign-electronic-signatures-in-
|
||||
<Callout type="info" emoji="✅">
|
||||
Status: Compliant for Level 1 - SES (Simple Electronic Signatures)
|
||||
</Callout>
|
||||
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that standardizes
|
||||
electronic identification and trust services for secure and seamless electronic transactions across European
|
||||
member states.
|
||||
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that
|
||||
standardizes electronic identification and trust services for secure and seamless electronic
|
||||
transactions across European member states.
|
||||
|
||||
### Level 1 - SES (Simple Electronic Signatures)
|
||||
|
||||
@@ -69,8 +74,8 @@ eIDAS SES (Simple Electronic Signature) is a basic electronic signature with min
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/9) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique identification
|
||||
of the signer and data integrity.
|
||||
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique
|
||||
identification of the signer and data integrity.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
@@ -85,8 +90,8 @@ of the signer and data integrity.
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/32) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a handwritten
|
||||
signature within the EU.
|
||||
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a
|
||||
handwritten signature within the EU.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Standards and Regulations
|
||||
description: Learn about the different standards and regulations for Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
## 21 CFR Part 11
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Email Domains
|
||||
description: Learn how to create and manage email domains in Documenso.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Email Domains
|
||||
|
||||
@@ -7,28 +7,28 @@ import { Callout } from 'nextra/components';
|
||||
|
||||
# Fair Use Policy
|
||||
|
||||
### Why
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using.
|
||||
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using. This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
|
||||
### Spirit of the Plan
|
||||
|
||||
> Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
|
||||
<Callout type="info">
|
||||
What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
|
||||
pricing. We won’t block your account without reaching out. [Message
|
||||
us](mailto:support@documenso.com) for questions. It's probably fine, though.
|
||||
pricing. We won’t block your account without reaching out. You can [message
|
||||
us](mailto:support@documenso.com) for questions.
|
||||
</Callout>
|
||||
|
||||
### DO
|
||||
|
||||
- Sign as many documents with the individual plan for your single business or organization you are part of
|
||||
- Use the API and Zapier to automate all your signing to sign as much as possible
|
||||
- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
|
||||
- Sign as many documents as you need with the individual plan for your single business or organization you are part of
|
||||
- Use the API and automation tools to automate all your signing workflows
|
||||
- Experiment with the plans and integrations, testing what you want to build
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use the individual account's API to power a platform
|
||||
- Run a huge company, signing thousands of documents per day on a two-user team plan using the API
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win, and it's probably fine
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win
|
||||
|
||||
@@ -10,7 +10,12 @@ 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, Teams and Platform.
|
||||
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 4 plans available:
|
||||
|
||||
- Free
|
||||
- Individual
|
||||
- Teams
|
||||
- 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.
|
||||
|
||||
@@ -24,7 +29,7 @@ To create a free account, navigate to the [registration page](https://documen.so
|
||||
|
||||
### Optional: Claim a Premium Username
|
||||
|
||||
You can claim a premium username by upgrading to a paid plan. After upgrading to a paid plan, you can update your [public profile](https://app.documenso.com/settings/public-profile).
|
||||
You can claim a premium username by upgrading to a paid plan. After upgrading to a paid plan, you can update your [public profile](/users/profile).
|
||||
|
||||
### Optional: Create a Team
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Community Edition
|
||||
description: Learn about the Community Edition of Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Community Edition
|
||||
@@ -32,10 +37,10 @@ Documenso and the Community Edition are licensed under [AGPL3](https://github.co
|
||||
|
||||
### Conditions
|
||||
|
||||
ℹ️ License and copyright notice
|
||||
ℹ️ State changes
|
||||
ℹ️ Disclose source
|
||||
ℹ️ Network use is distribution
|
||||
- License and copyright notice
|
||||
- State changes
|
||||
- Disclose source
|
||||
- Network use is distribution
|
||||
|
||||
<Callout type="warning">
|
||||
It's important to remember that you must keep the AGPL3 license for your modified or non-modified
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Enterprise Edition
|
||||
description: Learn about the Enterprise Edition of Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Enterprise Edition
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Licenses
|
||||
description: Learn about the different licenses for self-hosting Documenso.
|
||||
---
|
||||
|
||||
# Self-Hosting Licenses
|
||||
|
||||
Documenso comes in two versions for self-hosting:
|
||||
|
||||
@@ -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 "Settings" or "Team Settings". Then, navigate to the "Public Profile" tab to configure your profile.
|
||||
Click on your profile picture in the top right corner and select "Team Settings". Then, navigate to the "Public Profile" tab to configure your profile.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -9,30 +9,30 @@ description: Learn what types of support we offer.
|
||||
|
||||
If you are a developer or free user, you can reach out to the community or raise an issue:
|
||||
|
||||
### [Create Github Issues](https://github.com/documenso/documenso/issues)
|
||||
**[Create Github Issues](https://github.com/documenso/documenso/issues)**
|
||||
|
||||
The community and the core team address GitHub issues. Be sure to check if a similar issue already exists. Please note that while we want to address everything immediately, we must prioritize.
|
||||
|
||||
### [Join our Discord](https://documen.so/discord)
|
||||
**[Join our Discord](https://documen.so/discord)**
|
||||
|
||||
You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
|
||||
|
||||
## Paid Account Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
**Email: support@documenso.com**
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Private Discord channel
|
||||
**Private Discord channel**
|
||||
|
||||
If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
|
||||
|
||||
## Enterprise Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
**Email: support@documenso.com**
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Slack
|
||||
**Slack**
|
||||
|
||||
If your team is on Slack, we can create a private workspace to support you more closely.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Templates
|
||||
description: Learn how to create and use templates in Documenso.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Document Templates
|
||||
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 590 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@@ -137,12 +137,12 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
||||
<div className="bg-muted/70 rounded-lg border p-4">
|
||||
<div className="rounded-lg border bg-muted/70 p-4">
|
||||
<h3 className="text-sm font-medium">
|
||||
<Trans>CSV Structure</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
For each recipient, provide their email (required) and name (optional) in separate
|
||||
columns. Download the template CSV below for the correct format.
|
||||
@@ -153,7 +153,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Current recipients:</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="text-muted-foreground mt-2 list-inside list-disc text-sm">
|
||||
<ul className="mt-2 list-inside list-disc text-sm text-muted-foreground">
|
||||
{recipients.map((recipient, index) => (
|
||||
<li key={index}>
|
||||
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
||||
@@ -167,7 +167,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Download Template CSV</Trans>
|
||||
</Button>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>Pre-formatted CSV template with example data.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -200,14 +200,14 @@ export const TemplateBulkSendDialog = ({
|
||||
) : (
|
||||
<div className="flex h-10 items-center rounded-md border px-3">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<FileIcon className="text-muted-foreground h-4 w-4" />
|
||||
<FileIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-sm">{value.name}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-destructive hover:text-destructive p-0 text-xs"
|
||||
className="p-0 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => onChange(null)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
@@ -220,9 +220,9 @@ export const TemplateBulkSendDialog = ({
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{error && <p className="text-destructive text-sm">{error.message}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>
|
||||
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
||||
template defaults.
|
||||
@@ -247,7 +247,7 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<label
|
||||
htmlFor="send-immediately"
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
>
|
||||
<Trans>Send documents to recipients immediately</Trans>
|
||||
</label>
|
||||
|
||||
@@ -58,6 +58,7 @@ export type TDocumentPreferencesFormSchema = {
|
||||
includeSigningCertificate: boolean | null;
|
||||
includeAuditLog: boolean | null;
|
||||
signatureTypes: DocumentSignatureType[];
|
||||
delegateDocumentOwnership: boolean | null;
|
||||
aiFeaturesEnabled: boolean | null;
|
||||
};
|
||||
|
||||
@@ -73,6 +74,7 @@ type SettingsSubset = Pick<
|
||||
| 'typedSignatureEnabled'
|
||||
| 'uploadSignatureEnabled'
|
||||
| 'drawSignatureEnabled'
|
||||
| 'delegateDocumentOwnership'
|
||||
| 'aiFeaturesEnabled'
|
||||
>;
|
||||
|
||||
@@ -109,6 +111,7 @@ export const DocumentPreferencesForm = ({
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
}),
|
||||
delegateDocumentOwnership: z.boolean().nullable(),
|
||||
aiFeaturesEnabled: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
@@ -125,6 +128,7 @@ export const DocumentPreferencesForm = ({
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
},
|
||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
||||
@@ -515,6 +519,52 @@ export const DocumentPreferencesForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="delegateDocumentOwnership"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Delegate Document Ownership</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Enable team API tokens to delegate document ownership to another team member.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAiFeaturesConfigured && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -368,7 +368,7 @@ export const SignInForm = ({
|
||||
<p className="mt-2 text-right">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
className="text-sm text-muted-foreground duration-200 hover:opacity-70"
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
</Link>
|
||||
@@ -390,11 +390,11 @@ export const SignInForm = ({
|
||||
<>
|
||||
{hasSocialAuthEnabled && (
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="bg-transparent text-muted-foreground">
|
||||
<Trans>Or continue with</Trans>
|
||||
</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -403,7 +403,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
@@ -417,7 +417,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithMicrosoftClick}
|
||||
>
|
||||
@@ -435,7 +435,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
@@ -452,7 +452,7 @@ export const SignInForm = ({
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
loading={isPasskeyLoading}
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
onClick={onSignInWithPasskey}
|
||||
>
|
||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
export type DocumentLogsTableProps = {
|
||||
documentId: number;
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
@@ -26,7 +27,7 @@ const dateFormat: DateTimeFormatOptions = {
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -93,7 +94,9 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
{
|
||||
header: _(msg`Action`),
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDocumentAuditLogAction(_, row.original, userId).description}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`IP Address`),
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -85,6 +86,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,11 +75,12 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
documentRootPath,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { document, recipients, documentRootPath } = loaderData;
|
||||
const { document, recipients, documentRootPath, userId } = loaderData;
|
||||
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
@@ -171,15 +172,15 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
<section className="mt-6">
|
||||
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
|
||||
{documentInformation.map((info, i) => (
|
||||
<div className="text-foreground text-sm" key={i}>
|
||||
<div className="text-sm text-foreground" key={i}>
|
||||
<h3 className="font-semibold">{_(info.description)}</h3>
|
||||
<p className="text-muted-foreground truncate">{info.value}</p>
|
||||
<p className="truncate text-muted-foreground">{info.value}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-foreground text-sm">
|
||||
<div className="text-sm text-foreground">
|
||||
<h3 className="font-semibold">Recipients</h3>
|
||||
<ul className="text-muted-foreground list-inside list-disc">
|
||||
<ul className="list-inside list-disc text-muted-foreground">
|
||||
{recipients.map((recipient) => (
|
||||
<li key={`recipient-${recipient.id}`}>
|
||||
<span>{formatRecipientText(recipient)}</span>
|
||||
@@ -191,7 +192,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<DocumentLogsTable documentId={document.id} />
|
||||
<DocumentLogsTable documentId={document.id} userId={userId} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function TeamsSettingsPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -75,6 +76,7 @@ export default function TeamsSettingsPage() {
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
}),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
||||
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -53,6 +54,8 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
throw redirect('/');
|
||||
}
|
||||
|
||||
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
|
||||
|
||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||
@@ -81,6 +84,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
deletedAt: envelope.deletedAt,
|
||||
documentMeta: envelope.documentMeta,
|
||||
},
|
||||
hidePoweredBy: organisationClaim.flags.hidePoweredBy,
|
||||
documentLanguage,
|
||||
messages,
|
||||
};
|
||||
@@ -95,7 +99,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||
*/
|
||||
export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
const { auditLogs, document, documentLanguage, messages } = loaderData;
|
||||
const { auditLogs, document, documentLanguage, hidePoweredBy, messages } = loaderData;
|
||||
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
@@ -188,11 +192,13 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
</div>
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
{!hidePoweredBy && (
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,11 +193,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current User Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<Trans>Your Account</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(user.avatar)}
|
||||
avatarFallback={extractInitials(user.name || user.email)}
|
||||
@@ -215,11 +215,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Organisation Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<Trans>Requesting Organisation</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(organisation.avatar)}
|
||||
avatarFallback={extractInitials(organisation.name)}
|
||||
@@ -237,7 +237,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Warnings Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<Trans>Important: What This Means</Trans>
|
||||
</h3>
|
||||
@@ -253,7 +253,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Eye className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Full account access:
|
||||
</span>{' '}
|
||||
View all your profile information, settings, and activity
|
||||
@@ -264,7 +264,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Settings className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Account management:
|
||||
</span>{' '}
|
||||
Modify your account settings, permissions, and preferences
|
||||
@@ -275,7 +275,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Database className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">Data access:</span>{' '}
|
||||
<span className="font-semibold text-muted-foreground">Data access:</span>{' '}
|
||||
Access all data associated with your account
|
||||
</Trans>
|
||||
</span>
|
||||
@@ -304,7 +304,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={`accept-conditions`}
|
||||
>
|
||||
<Trans>I agree to link my account with this organization</Trans>
|
||||
|
||||
@@ -107,5 +107,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.3.0"
|
||||
"version": "2.3.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ARG TAG=latest
|
||||
FROM documenso/documenso:${TAG}
|
||||
|
||||
# Install @playwright/browser-chromium which bundles Playwright + Chromium
|
||||
RUN npm install @playwright/browser-chromium
|
||||
@@ -5,7 +5,7 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.2",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"build": "turbo run build",
|
||||
@@ -51,9 +51,9 @@
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@lingui/cli": "^5.6.0",
|
||||
"@prisma/client": "^6.19.0",
|
||||
"@trpc/client": "11.7.1",
|
||||
"@trpc/react-query": "11.7.1",
|
||||
"@trpc/server": "11.7.1",
|
||||
"@trpc/client": "11.8.1",
|
||||
"@trpc/react-query": "11.8.1",
|
||||
"@trpc/server": "11.8.1",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@ts-rest/open-api": "^3.52.1",
|
||||
"@ts-rest/serverless": "^3.52.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import type { Team, User } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
|
||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
|
||||
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
|
||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||
|
||||
@@ -562,6 +563,200 @@ test.describe('API V2 Envelopes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope find endpoint', () => {
|
||||
const createEnvelope = async (
|
||||
request: APIRequestContext,
|
||||
token: string,
|
||||
payload: TCreateEnvelopePayload,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
const pdfData = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
|
||||
);
|
||||
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()) as TCreateEnvelopeResponse;
|
||||
};
|
||||
|
||||
test('should find envelopes with pagination', async ({ request }) => {
|
||||
// Create 3 envelopes
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document 1',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document 2',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'Template 1',
|
||||
});
|
||||
|
||||
// Find all envelopes
|
||||
const res = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.length).toBe(3);
|
||||
expect(response.count).toBe(3);
|
||||
expect(response.currentPage).toBe(1);
|
||||
expect(response.totalPages).toBe(1);
|
||||
|
||||
// Test pagination
|
||||
const paginatedRes = await request.get(`${baseUrl}/envelope?perPage=2&page=1`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(paginatedRes.ok()).toBeTruthy();
|
||||
const paginatedResponse = (await paginatedRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(paginatedResponse.data.length).toBe(2);
|
||||
expect(paginatedResponse.count).toBe(3);
|
||||
expect(paginatedResponse.totalPages).toBe(2);
|
||||
});
|
||||
|
||||
test('should filter envelopes by type', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document Only',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'Template Only',
|
||||
});
|
||||
|
||||
// Filter by DOCUMENT type
|
||||
const documentRes = await request.get(`${baseUrl}/envelope?type=DOCUMENT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(documentRes.ok()).toBeTruthy();
|
||||
const documentResponse = (await documentRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(documentResponse.data.every((e) => e.type === EnvelopeType.DOCUMENT)).toBe(true);
|
||||
|
||||
// Filter by TEMPLATE type
|
||||
const templateRes = await request.get(`${baseUrl}/envelope?type=TEMPLATE`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(templateRes.ok()).toBeTruthy();
|
||||
const templateResponse = (await templateRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(templateResponse.data.every((e) => e.type === EnvelopeType.TEMPLATE)).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter envelopes by status', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Draft Document',
|
||||
});
|
||||
|
||||
// Filter by DRAFT status (default for new envelopes)
|
||||
const res = await request.get(`${baseUrl}/envelope?status=DRAFT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.every((e) => e.status === DocumentStatus.DRAFT)).toBe(true);
|
||||
});
|
||||
|
||||
test('should search envelopes by query', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Unique Searchable Title',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Another Document',
|
||||
});
|
||||
|
||||
const res = await request.get(`${baseUrl}/envelope?query=Unique%20Searchable`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.length).toBe(1);
|
||||
expect(response.data[0].title).toBe('Unique Searchable Title');
|
||||
});
|
||||
|
||||
test('should not return envelopes from other users', async ({ request }) => {
|
||||
// Create envelope for userA
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserA Document',
|
||||
});
|
||||
|
||||
// Create envelope for userB
|
||||
await createEnvelope(request, tokenB, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserB Document',
|
||||
});
|
||||
|
||||
// userA should only see their own envelopes
|
||||
const resA = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(resA.ok()).toBeTruthy();
|
||||
const responseA = (await resA.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(responseA.data.every((e) => e.title !== 'UserB Document')).toBe(true);
|
||||
|
||||
// userB should only see their own envelopes
|
||||
const resB = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
});
|
||||
|
||||
expect(resB.ok()).toBeTruthy();
|
||||
const responseB = (await resB.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(responseB.data.every((e) => e.title !== 'UserA Document')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return envelope with expected schema fields', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Schema Test Document',
|
||||
});
|
||||
|
||||
const res = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
const envelope = response.data.find((e) => e.title === 'Schema Test Document');
|
||||
|
||||
expect(envelope).toBeDefined();
|
||||
expect(envelope?.id).toBeDefined();
|
||||
expect(envelope?.type).toBe(EnvelopeType.DOCUMENT);
|
||||
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
|
||||
expect(envelope?.recipients).toBeDefined();
|
||||
expect(envelope?.user).toBeDefined();
|
||||
expect(envelope?.team).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Empty recipient tests', () => {
|
||||
test('Create template envelope with empty email recipient', async ({ request }) => {
|
||||
const payload = {
|
||||
|
||||
@@ -12,14 +12,17 @@ import {
|
||||
} from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentStatus,
|
||||
DocumentVisibility,
|
||||
EnvelopeType,
|
||||
FieldType,
|
||||
FolderType,
|
||||
Prisma,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
TeamMemberRole,
|
||||
} from '@documenso/prisma/client';
|
||||
import {
|
||||
seedBlankDocument,
|
||||
@@ -28,14 +31,18 @@ import {
|
||||
seedPendingDocument,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
|
||||
import { seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||
import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
||||
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
|
||||
import type {
|
||||
TUseEnvelopePayload,
|
||||
TUseEnvelopeResponse,
|
||||
} from '@documenso/trpc/server/envelope-router/use-envelope.types';
|
||||
|
||||
import { apiSignin } from '../../fixtures/authentication';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
|
||||
test.describe.configure({
|
||||
@@ -2990,6 +2997,566 @@ test.describe('Document API V2', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope get-many endpoint', () => {
|
||||
test('should block unauthorized access to envelope get-many endpoint', async ({
|
||||
request,
|
||||
}) => {
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [doc1.id, doc2.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
test('should allow authorized access to envelope get-many endpoint', async ({ request }) => {
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [doc1.id, doc2.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data.length).toBe(2);
|
||||
expect(data.map((d: { id: string }) => d.id).sort()).toEqual([doc1.id, doc2.id].sort());
|
||||
});
|
||||
|
||||
test('should only return authorized envelopes when mixing owned and unowned', async ({
|
||||
request,
|
||||
}) => {
|
||||
const docA = await seedBlankDocument(userA, teamA.id);
|
||||
const docB = await seedBlankDocument(userB, teamB.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [docA.id, docB.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0].id).toBe(docA.id);
|
||||
});
|
||||
|
||||
test('should block unauthorized access with documentId type', async ({ request }) => {
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'documentId',
|
||||
ids: [
|
||||
mapSecondaryIdToDocumentId(doc1.secondaryId),
|
||||
mapSecondaryIdToDocumentId(doc2.secondaryId),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
test('should allow authorized access with documentId type', async ({ request }) => {
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'documentId',
|
||||
ids: [
|
||||
mapSecondaryIdToDocumentId(doc1.secondaryId),
|
||||
mapSecondaryIdToDocumentId(doc2.secondaryId),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should block unauthorized access with templateId type', async ({ request }) => {
|
||||
const template1 = await seedBlankTemplate(userA, teamA.id);
|
||||
const template2 = await seedBlankTemplate(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'templateId',
|
||||
ids: [
|
||||
mapSecondaryIdToTemplateId(template1.secondaryId),
|
||||
mapSecondaryIdToTemplateId(template2.secondaryId),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
test('should allow authorized access with templateId type', async ({ request }) => {
|
||||
const template1 = await seedBlankTemplate(userA, teamA.id);
|
||||
const template2 = await seedBlankTemplate(userA, teamA.id);
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'templateId',
|
||||
ids: [
|
||||
mapSecondaryIdToTemplateId(template1.secondaryId),
|
||||
mapSecondaryIdToTemplateId(template2.secondaryId),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const { data } = await res.json();
|
||||
expect(data.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should reject requests exceeding max ID limit', async ({ request }) => {
|
||||
const ids = Array.from({ length: 21 }, () => 'envelope_fake123');
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/get-many`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope get-many tRPC endpoint (teamId manipulation)', () => {
|
||||
test('should block access when user manipulates x-team-id to another team', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create documents for userA in teamA
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
// Sign in as userB
|
||||
await apiSignin({ page, email: userB.email });
|
||||
|
||||
const res = await page
|
||||
.context()
|
||||
.request.post(`${WEBAPP_BASE_URL}/api/trpc/envelope.getMany`, {
|
||||
headers: {
|
||||
'x-team-id': String(teamA.id),
|
||||
},
|
||||
data: {
|
||||
json: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [doc1.id, doc2.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Make tRPC request with manipulated x-team-id pointing to teamA (which userB doesn't belong to)
|
||||
expect(res.ok()).toBeFalsy();
|
||||
// Team not found
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should allow access when user uses their own team id', async ({ page }) => {
|
||||
// Create documents for userA in teamA
|
||||
const doc1 = await seedBlankDocument(userA, teamA.id);
|
||||
const doc2 = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
// Sign in as userA
|
||||
await apiSignin({ page, email: userA.email });
|
||||
|
||||
const res = await page
|
||||
.context()
|
||||
.request.post(`${WEBAPP_BASE_URL}/api/trpc/envelope.getMany`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-team-id': String(teamA.id),
|
||||
},
|
||||
data: {
|
||||
json: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [doc1.id, doc2.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const items = data.result.data.json.data;
|
||||
|
||||
expect(items.length).toBe(2);
|
||||
expect(items.map((d: { id: string }) => d.id).sort()).toEqual([doc1.id, doc2.id].sort());
|
||||
});
|
||||
|
||||
test('should block access when switching team id mid-request to access other team data', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create a document for userA in teamA
|
||||
const docA = await seedBlankDocument(userA, teamA.id);
|
||||
// Create a document for userB in teamB
|
||||
const docB = await seedBlankDocument(userB, teamB.id);
|
||||
|
||||
// Sign in as userB
|
||||
await apiSignin({ page, email: userB.email });
|
||||
|
||||
const res = await page
|
||||
.context()
|
||||
.request.post(`${WEBAPP_BASE_URL}/api/trpc/envelope.getMany`, {
|
||||
headers: {
|
||||
'x-team-id': String(teamA.id),
|
||||
},
|
||||
data: {
|
||||
json: {
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: [docA.id, docB.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// UserB tries to access both documents by manipulating teamId to teamA
|
||||
// Should fail - userB is not a member of teamA
|
||||
expect(res.ok()).toBeFalsy();
|
||||
// Team not found
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope find endpoint', () => {
|
||||
test('should block unauthorized access to envelope find endpoint', async ({ request }) => {
|
||||
await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
expect(data.data.every((doc) => doc.userId !== userA.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should allow authorized access to envelope find endpoint', async ({ request }) => {
|
||||
await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
expect(data.data.length).toBeGreaterThan(0);
|
||||
expect(data.data.some((doc) => doc.userId === userA.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should respect team document visibility for ADMIN role', async ({ request }) => {
|
||||
const adminMember = await seedTeamMember({
|
||||
teamId: teamA.id,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
});
|
||||
|
||||
const { token: adminToken } = await createApiToken({
|
||||
userId: adminMember.id,
|
||||
teamId: teamA.id,
|
||||
tokenName: 'adminMember',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.ADMIN,
|
||||
title: 'Admin Only Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
title: 'Manager and Above Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.EVERYONE,
|
||||
title: 'Everyone Document',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
const titles = data.data.map((doc) => doc.title);
|
||||
expect(titles).toContain('Admin Only Document');
|
||||
expect(titles).toContain('Manager and Above Document');
|
||||
expect(titles).toContain('Everyone Document');
|
||||
});
|
||||
|
||||
test('should respect team document visibility for MANAGER role', async ({ request }) => {
|
||||
const managerMember = await seedTeamMember({
|
||||
teamId: teamA.id,
|
||||
role: TeamMemberRole.MANAGER,
|
||||
});
|
||||
|
||||
const { token: managerToken } = await createApiToken({
|
||||
userId: managerMember.id,
|
||||
teamId: teamA.id,
|
||||
tokenName: 'managerMember',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.ADMIN,
|
||||
title: 'Admin Only Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
title: 'Manager and Above Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
visibility: DocumentVisibility.EVERYONE,
|
||||
title: 'Everyone Document',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope`, {
|
||||
headers: { Authorization: `Bearer ${managerToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
const titles = data.data.map((doc) => doc.title);
|
||||
expect(titles).not.toContain('Admin Only Document');
|
||||
expect(titles).toContain('Manager and Above Document');
|
||||
expect(titles).toContain('Everyone Document');
|
||||
});
|
||||
|
||||
test('should filter envelopes by folderId with authorization', async ({ request }) => {
|
||||
const folder = await prisma.folder.create({
|
||||
data: {
|
||||
userId: userA.id,
|
||||
teamId: teamA.id,
|
||||
name: 'Test Folder',
|
||||
type: FolderType.DOCUMENT,
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
folderId: folder.id,
|
||||
title: 'Document in Folder',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'Document Not in Folder',
|
||||
},
|
||||
});
|
||||
|
||||
const resWithFolder = await request.get(
|
||||
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?folderId=${folder.id}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
},
|
||||
);
|
||||
|
||||
expect(resWithFolder.ok()).toBeTruthy();
|
||||
const dataWithFolder = (await resWithFolder.json()) as TFindEnvelopesResponse;
|
||||
expect(dataWithFolder.data.every((doc) => doc.folderId === folder.id)).toBe(true);
|
||||
expect(dataWithFolder.data.some((doc) => doc.title === 'Document in Folder')).toBe(true);
|
||||
|
||||
const resUnauthorized = await request.get(
|
||||
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?folderId=${folder.id}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
},
|
||||
);
|
||||
|
||||
expect(resUnauthorized.ok()).toBeTruthy();
|
||||
const dataUnauthorized = (await resUnauthorized.json()) as TFindEnvelopesResponse;
|
||||
expect(
|
||||
dataUnauthorized.data.every(
|
||||
(doc) => doc.folderId !== folder.id || doc.userId !== userA.id,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter envelopes by type with authorization', async ({ request }) => {
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserA Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'UserA Template',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userB, teamB.id, {
|
||||
createDocumentOptions: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserB Document',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope?type=DOCUMENT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
expect(data.data.every((doc) => doc.type === EnvelopeType.DOCUMENT)).toBe(true);
|
||||
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
|
||||
expect(data.data.some((doc) => doc.title === 'UserA Document')).toBe(true);
|
||||
expect(data.data.every((doc) => doc.title !== 'UserB Document')).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter envelopes by status with authorization', async ({ request }) => {
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'Draft Document',
|
||||
status: DocumentStatus.DRAFT,
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'Completed Document',
|
||||
status: DocumentStatus.COMPLETED,
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userB, teamB.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'UserB Draft',
|
||||
status: DocumentStatus.DRAFT,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/envelope?status=DRAFT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
expect(data.data.every((doc) => doc.status === DocumentStatus.DRAFT)).toBe(true);
|
||||
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
|
||||
expect(data.data.some((doc) => doc.title === 'Draft Document')).toBe(true);
|
||||
expect(data.data.every((doc) => doc.title !== 'UserB Draft')).toBe(true);
|
||||
expect(data.data.every((doc) => doc.title !== 'Completed Document')).toBe(true);
|
||||
});
|
||||
|
||||
test('should search envelopes by query with authorization', async ({ request }) => {
|
||||
await seedBlankDocument(userA, teamA.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'Unique Searchable Title UserA',
|
||||
},
|
||||
});
|
||||
|
||||
await seedBlankDocument(userB, teamB.id, {
|
||||
createDocumentOptions: {
|
||||
title: 'Unique Searchable Title UserB',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.get(
|
||||
`${WEBAPP_BASE_URL}/api/v2-beta/envelope?query=Unique%20Searchable`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const data = (await res.json()) as TFindEnvelopesResponse;
|
||||
expect(data.data.every((doc) => doc.userId === userA.id)).toBe(true);
|
||||
expect(data.data.some((doc) => doc.title.includes('UserA'))).toBe(true);
|
||||
expect(data.data.every((doc) => !doc.title.includes('UserB'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope update endpoint', () => {
|
||||
test('should block unauthorized access to envelope update endpoint', async ({ request }) => {
|
||||
const doc = await seedBlankDocument(userA, teamA.id);
|
||||
|
||||
@@ -3,3 +3,8 @@ Copyright (c) 2023 Documenso, Inc
|
||||
|
||||
- The Stripe Billing Module
|
||||
- Document Action Reauthentication (Passkeys and 2FA)
|
||||
- 21 CFR
|
||||
- Email domains
|
||||
- Embed authoring
|
||||
- Embed authoring white label
|
||||
- Enterprise level support + possible SLAs and license changes
|
||||
|
||||
@@ -286,7 +286,7 @@ const detectFieldsFromPage = async ({
|
||||
});
|
||||
|
||||
const result = await generateObject({
|
||||
model: vertex('gemini-3-pro-preview'),
|
||||
model: vertex('gemini-3-flash-preview'),
|
||||
system: SYSTEM_PROMPT,
|
||||
schema: ZSubmitDetectedFieldsInputSchema,
|
||||
messages,
|
||||
|
||||
@@ -207,7 +207,7 @@ const detectRecipientsFromImages = async ({
|
||||
});
|
||||
|
||||
const result = await generateObject({
|
||||
model: vertex('gemini-2.5-flash'),
|
||||
model: vertex('gemini-3-flash-preview'),
|
||||
system: SYSTEM_PROMPT,
|
||||
schema: ZDetectedRecipientsSchema,
|
||||
messages,
|
||||
|
||||
@@ -81,6 +81,7 @@ export type CreateEnvelopeOptions = {
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
recipients?: CreateEnvelopeRecipientOptions[];
|
||||
folderId?: string;
|
||||
delegatedDocumentOwner?: string;
|
||||
};
|
||||
attachments?: Array<{
|
||||
label: string;
|
||||
@@ -114,6 +115,7 @@ export const createEnvelope = async ({
|
||||
publicTitle,
|
||||
publicDescription,
|
||||
visibility: visibilityOverride,
|
||||
delegatedDocumentOwner,
|
||||
} = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
@@ -256,6 +258,43 @@ export const createEnvelope = async ({
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
const getValidatedDelegatedOwner = async () => {
|
||||
if (
|
||||
!settings.delegateDocumentOwnership ||
|
||||
!delegatedDocumentOwner ||
|
||||
requestMetadata.source === 'app'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const delegatedOwner = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: delegatedDocumentOwner,
|
||||
},
|
||||
});
|
||||
|
||||
if (!delegatedOwner) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Delegated document owner must be a member of the team',
|
||||
});
|
||||
}
|
||||
|
||||
const isTeamMember = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId: delegatedOwner.id }),
|
||||
});
|
||||
|
||||
if (!isTeamMember) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Delegated document owner must be a member of the team',
|
||||
});
|
||||
}
|
||||
|
||||
return delegatedOwner;
|
||||
};
|
||||
|
||||
const delegatedOwner = await getValidatedDelegatedOwner();
|
||||
const envelopeOwnerId = delegatedOwner?.id ?? userId;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
@@ -285,7 +324,7 @@ export const createEnvelope = async ({
|
||||
})),
|
||||
},
|
||||
},
|
||||
userId,
|
||||
userId: envelopeOwnerId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
@@ -393,6 +432,9 @@ export const createEnvelope = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
id: envelopeOwnerId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
@@ -403,6 +445,25 @@ export const createEnvelope = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
// Create audit log for delegated owner if validation passed
|
||||
if (delegatedOwner) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
delegatedOwnerName: delegatedOwner.name,
|
||||
delegatedOwnerEmail: delegatedOwner.email,
|
||||
teamName: team.name,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import type {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
Envelope,
|
||||
EnvelopeType,
|
||||
Prisma,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type FindEnvelopesOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
type?: EnvelopeType;
|
||||
templateId?: number;
|
||||
source?: DocumentSource;
|
||||
status?: DocumentStatus;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Pick<Envelope, 'createdAt'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
query?: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const findEnvelopes = async ({
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
templateId,
|
||||
source,
|
||||
status,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
query = '',
|
||||
folderId,
|
||||
}: FindEnvelopesOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = query
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
}
|
||||
: {};
|
||||
|
||||
const visibilityFilter: Prisma.EnvelopeWhereInput = {
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
};
|
||||
|
||||
const teamEmailFilters: Prisma.EnvelopeWhereInput[] = [];
|
||||
|
||||
if (team.teamEmail) {
|
||||
teamEmailFilters.push(
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
...visibilityFilter,
|
||||
},
|
||||
{
|
||||
userId,
|
||||
},
|
||||
...teamEmailFilters,
|
||||
],
|
||||
},
|
||||
{
|
||||
folderId: folderId ?? null,
|
||||
deletedAt: null,
|
||||
},
|
||||
searchFilter,
|
||||
],
|
||||
};
|
||||
|
||||
if (type) {
|
||||
whereClause.type = type;
|
||||
}
|
||||
|
||||
if (templateId) {
|
||||
whereClause.templateId = templateId;
|
||||
}
|
||||
|
||||
if (source) {
|
||||
whereClause.source = source;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const maskedData = data.map((envelope) =>
|
||||
maskRecipientTokensForDocument({
|
||||
document: envelope,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedData = maskedData.map((envelope) => ({
|
||||
...envelope,
|
||||
recipients: envelope.Recipient,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
import type { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdsOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdsQuery } from '../../utils/envelope';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetEnvelopesByIdsOptions = {
|
||||
/**
|
||||
* The envelope IDs to fetch with their type.
|
||||
*/
|
||||
ids: EnvelopeIdsOptions;
|
||||
|
||||
/**
|
||||
* The user ID who has been authenticated.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unvalidated team ID from the request.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches multiple envelopes by their IDs with proper access control.
|
||||
*
|
||||
* Only returns envelopes that the user has valid access to based on:
|
||||
* 1. Document ownership (userId matches)
|
||||
* 2. Team membership with appropriate visibility level
|
||||
* 3. Team email ownership
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getEnvelopesByIds = async ({
|
||||
ids,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetEnvelopesByIdsOptions) => {
|
||||
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
|
||||
ids,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
});
|
||||
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
folder: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return envelopes.map((envelope) => ({
|
||||
...envelope,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export type GetEnvelopesByIdsResponse = Awaited<ReturnType<typeof getEnvelopesByIds>>;
|
||||
|
||||
export type GetMultipleEnvelopeWhereInputOptions = {
|
||||
/**
|
||||
* The envelope IDs to fetch with their type.
|
||||
*/
|
||||
ids: EnvelopeIdsOptions;
|
||||
|
||||
/**
|
||||
* The user ID who has been authenticated.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unknown teamId from the request.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a multiple envelope Prisma query.
|
||||
*
|
||||
* This will return a query that allows a user to get documents if they have valid access to them.
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getMultipleEnvelopeWhereInput = async ({
|
||||
ids,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetMultipleEnvelopeWhereInputOptions) => {
|
||||
// Backup validation incase something goes wrong.
|
||||
if (!ids.ids || !userId || !teamId || type === undefined) {
|
||||
console.error(`[CRTICAL ERROR]: MUST NEVER HAPPEN`);
|
||||
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope IDs not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that the user belongs to the team provided.
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const envelopeOrInput: Prisma.EnvelopeWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent from the team email.
|
||||
if (team.teamEmail) {
|
||||
envelopeOrInput.push({
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
// NOTE: DO NOT PUT ANY CODE AFTER THIS POINT.
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
|
||||
const envelopeWhereInput: Prisma.EnvelopeWhereInput = {
|
||||
...unsafeBuildEnvelopeIdsQuery(ids, type),
|
||||
OR: envelopeOrInput,
|
||||
};
|
||||
|
||||
// Final backup validation incase something goes wrong.
|
||||
if (
|
||||
!envelopeWhereInput.OR ||
|
||||
envelopeWhereInput.OR.length < 2 ||
|
||||
!userId ||
|
||||
!teamId ||
|
||||
!team.id ||
|
||||
teamId !== team.id
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Query not valid',
|
||||
});
|
||||
}
|
||||
|
||||
// Do not modify this return directly, all adjustments need to be made prior to the above if statement.
|
||||
return {
|
||||
envelopeWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* !: This is a workaround to fix the memory leak in the skia-canvas library.
|
||||
* !: Internals are ported from the original `konva/skia-backend.js` file.
|
||||
*/
|
||||
import { Konva } from 'konva/lib/_CoreInternals';
|
||||
import { Canvas, DOMMatrix, Image, Path2D } from 'skia-canvas';
|
||||
|
||||
// @ts-expect-error skia-canvas satisfies the requirements
|
||||
global.DOMMatrix = DOMMatrix;
|
||||
|
||||
// @ts-expect-error skia-canvas satisfies the requirements
|
||||
global.Path2D = Path2D;
|
||||
Path2D.prototype.toString = () => '[object Path2D]';
|
||||
|
||||
Konva.Util['createCanvasElement'] = () => {
|
||||
const node = new Canvas(300, 300);
|
||||
node.gpu = false;
|
||||
|
||||
if (!('style' in node) || !node['style']) {
|
||||
Object.assign(node, { style: {} });
|
||||
}
|
||||
|
||||
node.toString = () => '[object HTMLCanvasElement]';
|
||||
const ctx = node.getContext('2d');
|
||||
|
||||
Object.defineProperty(ctx, 'canvas', {
|
||||
get: () => node,
|
||||
});
|
||||
|
||||
return node as unknown as HTMLCanvasElement;
|
||||
};
|
||||
|
||||
Konva.Util.createImageElement = () => {
|
||||
const node = new Image();
|
||||
node.toString = () => '[object HTMLImageElement]';
|
||||
|
||||
return node as unknown as HTMLImageElement;
|
||||
};
|
||||
|
||||
Konva._renderBackend = 'skia-canvas';
|
||||
|
||||
export default Konva;
|
||||
@@ -1,5 +1,5 @@
|
||||
// sort-imports-ignore
|
||||
import 'konva/skia-backend';
|
||||
import '../konva/skia-backend';
|
||||
|
||||
import Konva from 'konva';
|
||||
import path from 'node:path';
|
||||
@@ -23,6 +23,7 @@ export const insertFieldInPDFV2 = async ({
|
||||
}: InsertFieldInPDFV2Options) => {
|
||||
const fontPath = path.join(process.cwd(), 'public/fonts');
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
FontLibrary.use({
|
||||
['Caveat']: [path.join(fontPath, 'caveat.ttf')],
|
||||
['Noto Sans']: [path.join(fontPath, 'noto-sans.ttf')],
|
||||
@@ -31,8 +32,8 @@ export const insertFieldInPDFV2 = async ({
|
||||
['Noto Sans Korean']: [path.join(fontPath, 'noto-sans-korean.ttf')],
|
||||
});
|
||||
|
||||
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||
const layer = new Konva.Layer();
|
||||
let stage: Konva.Stage | null = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||
let layer: Konva.Layer | null = new Konva.Layer();
|
||||
|
||||
// Render the fields onto the layer.
|
||||
for (const field of fields) {
|
||||
@@ -60,5 +61,13 @@ export const insertFieldInPDFV2 = async ({
|
||||
const canvas = layer.canvas._canvas as unknown as Canvas;
|
||||
|
||||
// Embed the SVG into the PDF
|
||||
return await canvas.toBuffer('pdf');
|
||||
const pdf = await canvas.toBuffer('pdf');
|
||||
|
||||
stage.destroy();
|
||||
layer.destroy();
|
||||
|
||||
stage = null;
|
||||
layer = null;
|
||||
|
||||
return pdf;
|
||||
};
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Standard-Zeitzone"
|
||||
msgid "Default Value"
|
||||
msgstr "Standardwert"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "löschen"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Dokument geöffnet"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Dokument ausstehend"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Z. B. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Aktiviere die Signaturreihenfolge"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "SSO-Portal aktivieren"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Authentifizierungsmethode erben"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Signaturfelder werden gesucht"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Verknüpfte Konten verwalten"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Organisation verwalten"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Organisationen verwalten"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Nächster Empfängername"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Wiederholen"
|
||||
msgid "Return"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Hier zur Documenso-Anmeldeseite zurückkehren"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "Der Dokumenteninhaber wurde über diese Ablehnung informiert. Es sind de
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "Der Dokumenteneigentümer wurde über Ihre Entscheidung informiert. Er kann Sie bei Bedarf mit weiteren Anweisungen kontaktieren."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "Das Dokument wurde erstellt, konnte aber nicht an die Empfänger versendet werden."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Jährlich"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Ihr Verifizierungscode:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3104,6 +3104,10 @@ msgstr "Default Time Zone"
|
||||
msgid "Default Value"
|
||||
msgstr "Default Value"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr "Delegate Document Ownership"
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "delete"
|
||||
@@ -3667,6 +3671,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Document opened"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr "Document ownership delegated"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Document pending"
|
||||
@@ -4008,6 +4017,7 @@ msgstr "E.g. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4273,6 +4283,10 @@ msgstr "Enable signing order"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Enable SSO portal"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr "Enable team API tokens to delegate document ownership to another team member."
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5157,6 +5171,7 @@ msgstr "Inherit authentication method"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5549,6 +5564,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Looking for signature fields"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5609,6 +5625,10 @@ msgstr "Manage linked accounts"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Manage organisation"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr "Manage Organisation"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Manage organisations"
|
||||
@@ -5955,6 +5975,7 @@ msgstr "Next Recipient Name"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
@@ -7327,6 +7348,11 @@ msgstr "Retry"
|
||||
msgid "Return"
|
||||
msgstr "Return"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr "Return Home"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Return to Documenso sign in page here"
|
||||
@@ -8830,6 +8856,12 @@ msgstr "The document owner has been notified of this rejection. No further actio
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr "The document ownership was delegated to {0} on behalf of {1}"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "The document was created but could not be sent to recipients."
|
||||
@@ -10837,6 +10869,7 @@ msgstr "Yearly"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Yes"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Zona horaria predeterminada"
|
||||
msgid "Default Value"
|
||||
msgstr "Valor Predeterminado"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "eliminar"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Documento abierto"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Documento pendiente"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Ej.: 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Habilitar orden de firma"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Habilitar portal SSO"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Heredar método de autenticación"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Buscando campos de firma"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Gestionar cuentas vinculadas"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Administrar organización"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Administrar organizaciones"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Nombre del próximo destinatario"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Reintentar"
|
||||
msgid "Return"
|
||||
msgstr "Regresar"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Regrese a la página de inicio de sesión de Documenso aquí"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "El propietario del documento ha sido notificado de este rechazo. No se r
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "El documento fue creado pero no se pudo enviar a los destinatarios."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Anual"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Su código de verificación:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "su-dominio.com otro-dominio.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Fuseau horaire par défaut"
|
||||
msgid "Default Value"
|
||||
msgstr "Valeur par défaut"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "supprimer"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Document ouvert"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Document en attente"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Par ex. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Activer l'ordre de signature"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Activer le portail SSO"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Hériter de la méthode d'authentification"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Recherche de champs de signature"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Gérer les comptes liés"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Gérer l'organisation"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Gérer les organisations"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Nom du destinataire suivant"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "Non"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Réessayer"
|
||||
msgid "Return"
|
||||
msgstr "Retour"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Revenir à la page de connexion de Documenso ici"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "Le propriétaire du document a été informé de ce rejet. Aucune action
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "Le propriétaire du document a été informé de votre décision. Il peut vous contacter pour des instructions supplémentaires si nécessaire."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "Le document a été créé mais n'a pas pu être envoyé aux destinataires."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Annuel"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Votre code de vérification :"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Fuso Orario Predefinito"
|
||||
msgid "Default Value"
|
||||
msgstr "Valore predefinito"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "elimina"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Documento aperto"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Documento in sospeso"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Es. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Abilita ordine di firma"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Abilita portale SSO"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Ereditare metodo di autenticazione"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Ricerca dei campi firma in corso"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Gestisci account collegati"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Gestisci l'organizzazione"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Gestisci le organizzazioni"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Nome prossimo destinatario"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Riprova"
|
||||
msgid "Return"
|
||||
msgstr "Ritorna"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Torna alla pagina di accesso a Documenso qui"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "Il proprietario del documento è stato informato di questo rifiuto. Non
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "Il proprietario del documento è stato informato della tua decisione. Potrebbe contattarti per ulteriori istruzioni, se necessario."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "Il documento è stato creato ma non è stato possibile inviarlo ai destinatari."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Annuale"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Sì"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Il tuo codice di verifica:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "tuo-dominio.com altro-dominio.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "既定のタイムゾーン"
|
||||
msgid "Default Value"
|
||||
msgstr "既定値"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "delete"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "ドキュメントが開かれました"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "文書は保留中です"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "例: 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "署名順序を有効にする"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "SSO ポータルを有効にする"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "認証方法を継承"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "署名フィールドを探しています"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "リンク済みアカウントを管理"
|
||||
msgid "Manage organisation"
|
||||
msgstr "組織を管理"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "組織を管理"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "次の受信者の名前"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "いいえ"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "再試行"
|
||||
msgid "Return"
|
||||
msgstr "戻る"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Documenso のサインインページへ戻る"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "この却下内容について、ドキュメントの所有者に通知
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "お客様の決定について、ドキュメントの所有者に通知しました。必要に応じて、追加の手順について連絡がある場合があります。"
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "文書は作成されましたが、受信者に送信できませんでした。"
|
||||
@@ -10842,6 +10874,7 @@ msgstr "年額"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "はい"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "認証コード:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "기본 시간대"
|
||||
msgid "Default Value"
|
||||
msgstr "기본값"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "delete"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "문서가 열렸습니다."
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "문서 보류 중"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "예: 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "서명 순서 활성화"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "SSO 포털 활성화"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "인증 방식 상속"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "서명 필드를 찾는 중"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "연결된 계정 관리"
|
||||
msgid "Manage organisation"
|
||||
msgstr "조직 관리"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "조직 관리"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "다음 수신자 이름"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "아니요"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "재시도"
|
||||
msgid "Return"
|
||||
msgstr "돌아가기"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "여기를 눌러 Documenso 로그인 페이지로 돌아가기"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "문서 소유자에게 이 거부 사실이 통보되었습니다. 현
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "문서 소유자에게 귀하의 결정이 이미 전달되었습니다. 필요할 경우 추가 안내를 위해 연락을 드릴 수 있습니다."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "문서는 생성되었지만 수신자에게 발송되지 않았습니다."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "연간"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "예"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "인증 코드:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Standaardtijdzone"
|
||||
msgid "Default Value"
|
||||
msgstr "Standaardwaarde"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "verwijderen"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Document geopend"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Document in behandeling"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Bijv. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Ondertekeningsvolgorde inschakelen"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "SSO-portaal inschakelen"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Authenticatiemethode overnemen"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Zoeken naar handtekeningvelden"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Gekoppelde accounts beheren"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Organisatie beheren"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Organisaties beheren"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Naam volgende ontvanger"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "Nee"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Opnieuw proberen"
|
||||
msgid "Return"
|
||||
msgstr "Terugkeren"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Ga hier terug naar de Documenso-aanmeldpagina"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "De documenteigenaar is op de hoogte gebracht van deze weigering. Er is o
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "De documenteigenaar is op de hoogte gebracht van je beslissing. Indien nodig kan diegene contact met je opnemen met verdere instructies."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "Het document is aangemaakt, maar kon niet naar ontvangers worden verzonden."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Jaarlijks"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Uw verificatiecode:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "Domyślna strefa czasowa"
|
||||
msgid "Default Value"
|
||||
msgstr "Domyślna wartość"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "usuń"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Otwarto dokument"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Dokument oczekujący"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "Np. 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "Włącz kolejność podpisywania"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Włącz logowanie SSO"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "Odziedzicz metodę uwierzytelniania"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Wyszukiwanie pól podpisu"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "Zarządzaj połączonymi kontami"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Zarządzaj organizacją"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Zarządzaj organizacjami"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "Nazwa następnego odbiorcy"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "Nie"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "Spróbuj ponownie"
|
||||
msgid "Return"
|
||||
msgstr "Wróć"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Powrót do strony logowania Documenso"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "Właściciel został poinformowany o odrzuceniu dokumentu. Może się z
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "Właściciel dokumentu został poinformowany o Twojej decyzji. Może się z Tobą skontaktować, jeśli będzie to konieczne."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "Dokument został utworzony, ale nie mógł zostać wysłany do odbiorców."
|
||||
@@ -10842,6 +10874,7 @@ msgstr "Rocznie"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Tak"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "Twój kod weryfikacyjny:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -3104,6 +3104,10 @@ msgstr "Fuso Horário Padrão"
|
||||
msgid "Default Value"
|
||||
msgstr "Valor Padrão"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "excluir"
|
||||
@@ -3667,6 +3671,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "Documento aberto"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "Documento pendente"
|
||||
@@ -4008,6 +4017,7 @@ msgstr "Ex: 100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4273,6 +4283,10 @@ msgstr "Ativar ordem de assinatura"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "Ativar portal SSO"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5157,6 +5171,7 @@ msgstr "Herdar método de autenticação"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5549,6 +5564,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "Procurando campos de assinatura"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5609,6 +5625,10 @@ msgstr "Gerenciar contas vinculadas"
|
||||
msgid "Manage organisation"
|
||||
msgstr "Gerenciar organização"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "Gerenciar organizações"
|
||||
@@ -5955,6 +5975,7 @@ msgstr "Nome do Próximo Destinatário"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "Não"
|
||||
|
||||
@@ -7327,6 +7348,11 @@ msgstr "Tentar novamente"
|
||||
msgid "Return"
|
||||
msgstr "Retornar"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "Retornar para a página de login do Documenso aqui"
|
||||
@@ -8830,6 +8856,12 @@ msgstr "O proprietário do documento foi notificado sobre esta rejeição. Nenhu
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "O proprietário do documento foi notificado sobre sua decisão. Eles podem entrar em contato com você com mais instruções, se necessário."
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "O documento foi criado, mas não pôde ser enviado aos destinatários."
|
||||
@@ -10837,6 +10869,7 @@ msgstr "Anual"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "Sim"
|
||||
|
||||
|
||||
@@ -3109,6 +3109,10 @@ msgstr "默认时区"
|
||||
msgid "Default Value"
|
||||
msgstr "默认值"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Delegate Document Ownership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
|
||||
msgid "delete"
|
||||
msgstr "delete"
|
||||
@@ -3672,6 +3676,11 @@ msgctxt "Audit log format"
|
||||
msgid "Document opened"
|
||||
msgstr "文档已打开"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document ownership delegated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
msgid "Document pending"
|
||||
msgstr "文档待处理"
|
||||
@@ -4013,6 +4022,7 @@ msgstr "例如:100"
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -4278,6 +4288,10 @@ msgstr "启用签署顺序"
|
||||
msgid "Enable SSO portal"
|
||||
msgstr "启用 SSO 门户"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Enable team API tokens to delegate document ownership to another team member."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
@@ -5162,6 +5176,7 @@ msgstr "继承认证方式"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/email-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
@@ -5554,6 +5569,7 @@ msgid "Looking for signature fields"
|
||||
msgstr "正在查找签名字段"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
msgid "Manage"
|
||||
@@ -5614,6 +5630,10 @@ msgstr "管理关联账户"
|
||||
msgid "Manage organisation"
|
||||
msgstr "管理组织"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Manage Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx
|
||||
msgid "Manage organisations"
|
||||
msgstr "管理组织"
|
||||
@@ -5960,6 +5980,7 @@ msgstr "下一位收件人姓名"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "No"
|
||||
msgstr "否"
|
||||
|
||||
@@ -7332,6 +7353,11 @@ msgstr "重试"
|
||||
msgid "Return"
|
||||
msgstr "返回"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
|
||||
msgid "Return Home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
|
||||
msgid "Return to Documenso sign in page here"
|
||||
msgstr "在此返回 Documenso 登录页面"
|
||||
@@ -8835,6 +8861,12 @@ msgstr "文档所有者已收到此次拒签的通知。当前您无需再进行
|
||||
msgid "The document owner has been notified of your decision. They may contact you with further instructions if necessary."
|
||||
msgstr "文档所有者已收到您的决定通知。如有需要,他们可能会联系您并提供进一步说明。"
|
||||
|
||||
#. placeholder {0}: data.delegatedOwnerName || data.delegatedOwnerEmail
|
||||
#. placeholder {1}: data.teamName
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "The document ownership was delegated to {0} on behalf of {1}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "The document was created but could not be sent to recipients."
|
||||
msgstr "文档已创建,但无法发送给收件人。"
|
||||
@@ -10842,6 +10874,7 @@ msgstr "按年"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
@@ -11621,4 +11654,3 @@ msgstr "您的验证码:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
||||
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
||||
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
||||
'DOCUMENT_DELEGATED_OWNER_CREATED', // When the document delegated owner is created.
|
||||
|
||||
// ACCESS AUTH 2FA events.
|
||||
'DOCUMENT_ACCESS_AUTH_2FA_REQUESTED', // When ACCESS AUTH 2FA is requested.
|
||||
@@ -681,6 +682,18 @@ export const ZDocumentAuditLogEventDocumentMovedToTeamSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document delegated owner created.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentDelegatedOwnerCreatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED),
|
||||
data: z.object({
|
||||
delegatedOwnerName: z.string().nullable(),
|
||||
delegatedOwnerEmail: z.string(),
|
||||
teamName: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
@@ -701,6 +714,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||
ZDocumentAuditLogEventDocumentDeletedSchema,
|
||||
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
||||
ZDocumentAuditLogEventDocumentDelegatedOwnerCreatedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldsAutoInsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||
|
||||
@@ -115,5 +115,40 @@ export type TEnvelopeLite = z.infer<typeof ZEnvelopeLiteSchema>;
|
||||
/**
|
||||
* A version of the envelope response schema when returning multiple envelopes at once from a single API endpoint.
|
||||
*/
|
||||
// export const ZEnvelopeManySchema = X
|
||||
// export type TEnvelopeMany = z.infer<typeof ZEnvelopeManySchema>;
|
||||
export const ZEnvelopeManySchema = EnvelopeSchema.pick({
|
||||
internalVersion: true,
|
||||
type: true,
|
||||
status: true,
|
||||
source: true,
|
||||
visibility: true,
|
||||
templateType: true,
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
externalId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
deletedAt: true,
|
||||
title: true,
|
||||
authOptions: true,
|
||||
formValues: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
folderId: true,
|
||||
templateId: true,
|
||||
}).extend({
|
||||
user: z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
}),
|
||||
recipients: ZEnvelopeRecipientLiteSchema.array(),
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
}).nullable(),
|
||||
});
|
||||
|
||||
export type TEnvelopeMany = z.infer<typeof ZEnvelopeManySchema>;
|
||||
|
||||
@@ -530,6 +530,13 @@ export const formatDocumentAuditLogAction = (
|
||||
anonymous: msg`Envelope item deleted`,
|
||||
identified: msg`${prefix} deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED }, ({ data }) => ({
|
||||
anonymous: msg({
|
||||
message: `Document ownership delegated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`The document ownership was delegated to ${data.delegatedOwnerName || data.delegatedOwnerEmail} on behalf of ${data.teamName}`,
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,6 +18,8 @@ const ZDocumentIdSchema = z.string().regex(/^document_\d+$/);
|
||||
const ZTemplateIdSchema = z.string().regex(/^template_\d+$/);
|
||||
const ZEnvelopeIdSchema = z.string().regex(/^envelope_.{2,}$/);
|
||||
|
||||
const MAX_ENVELOPE_IDS_PER_REQUEST = 20;
|
||||
|
||||
export type EnvelopeIdOptions =
|
||||
| {
|
||||
type: 'envelopeId';
|
||||
@@ -32,6 +34,20 @@ export type EnvelopeIdOptions =
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type EnvelopeIdsOptions =
|
||||
| {
|
||||
type: 'envelopeId';
|
||||
ids: string[];
|
||||
}
|
||||
| {
|
||||
type: 'documentId';
|
||||
ids: number[];
|
||||
}
|
||||
| {
|
||||
type: 'templateId';
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an unknown document or template ID.
|
||||
*
|
||||
@@ -89,6 +105,87 @@ export const unsafeBuildEnvelopeIdQuery = (
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses multiple document or template IDs and builds a query filter.
|
||||
*
|
||||
* This is UNSAFE because it does not validate access, it only validates ID format and builds the query.
|
||||
*
|
||||
* @throws AppError if any ID is invalid or if the array exceeds the maximum limit
|
||||
*/
|
||||
export const unsafeBuildEnvelopeIdsQuery = (
|
||||
options: EnvelopeIdsOptions,
|
||||
expectedEnvelopeType: EnvelopeType | null,
|
||||
) => {
|
||||
if (!options.ids || options.ids.length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'At least one ID is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (options.ids.length > MAX_ENVELOPE_IDS_PER_REQUEST) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Cannot request more than ${MAX_ENVELOPE_IDS_PER_REQUEST} envelopes at once`,
|
||||
});
|
||||
}
|
||||
|
||||
return match(options)
|
||||
.with({ type: 'envelopeId' }, (value) => {
|
||||
const validatedIds: string[] = [];
|
||||
|
||||
for (const id of value.ids) {
|
||||
const parsed = ZEnvelopeIdSchema.safeParse(id);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid envelope ID: ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
validatedIds.push(parsed.data);
|
||||
}
|
||||
|
||||
if (expectedEnvelopeType) {
|
||||
return {
|
||||
id: { in: validatedIds },
|
||||
type: expectedEnvelopeType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: { in: validatedIds },
|
||||
};
|
||||
})
|
||||
.with({ type: 'documentId' }, (value) => {
|
||||
if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.DOCUMENT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid document ID type',
|
||||
});
|
||||
}
|
||||
|
||||
const secondaryIds = value.ids.map((id) => mapDocumentIdToSecondaryId(id));
|
||||
|
||||
return {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
secondaryId: { in: secondaryIds },
|
||||
};
|
||||
})
|
||||
.with({ type: 'templateId' }, (value) => {
|
||||
if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.TEMPLATE) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid template ID type',
|
||||
});
|
||||
}
|
||||
|
||||
const secondaryIds = value.ids.map((id) => mapTemplateIdToSecondaryId(id));
|
||||
|
||||
return {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
secondaryId: { in: secondaryIds },
|
||||
};
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a legacy document ID number to an envelope secondary ID.
|
||||
*
|
||||
|
||||
@@ -117,6 +117,7 @@ export const generateDefaultOrganisationSettings = (): Omit<
|
||||
documentLanguage: 'en',
|
||||
documentTimezone: null, // Null means local timezone.
|
||||
documentDateFormat: DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
delegateDocumentOwnership: false,
|
||||
|
||||
includeSenderDetails: true,
|
||||
includeSigningCertificate: true,
|
||||
|
||||
@@ -184,6 +184,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
||||
documentLanguage: null,
|
||||
documentTimezone: null,
|
||||
documentDateFormat: null,
|
||||
delegateDocumentOwnership: null,
|
||||
|
||||
includeSenderDetails: null,
|
||||
includeSigningCertificate: null,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganisationGlobalSettings" ADD COLUMN "delegateDocumentOwnership" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "delegateDocumentOwnership" BOOLEAN;
|
||||
@@ -818,6 +818,7 @@ model OrganisationGlobalSettings {
|
||||
includeAuditLog Boolean @default(false)
|
||||
documentTimezone String? // Nullable to allow using local timezones if not set.
|
||||
documentDateFormat String @default("yyyy-MM-dd hh:mm a")
|
||||
delegateDocumentOwnership Boolean @default(false)
|
||||
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
uploadSignatureEnabled Boolean @default(true)
|
||||
@@ -844,10 +845,11 @@ model TeamGlobalSettings {
|
||||
id String @id
|
||||
team Team?
|
||||
|
||||
documentVisibility DocumentVisibility?
|
||||
documentLanguage String?
|
||||
documentTimezone String?
|
||||
documentDateFormat String?
|
||||
documentVisibility DocumentVisibility?
|
||||
documentLanguage String?
|
||||
documentTimezone String?
|
||||
documentDateFormat String?
|
||||
delegateDocumentOwnership Boolean?
|
||||
|
||||
includeSenderDetails Boolean?
|
||||
includeSigningCertificate Boolean?
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
"@trpc/client": "11.7.1",
|
||||
"@trpc/react-query": "11.7.1",
|
||||
"@trpc/server": "11.7.1",
|
||||
"@trpc/client": "11.8.1",
|
||||
"@trpc/react-query": "11.8.1",
|
||||
"@trpc/server": "11.8.1",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"formidable": "^3.5.4",
|
||||
"luxon": "^3.7.2",
|
||||
@@ -29,4 +29,4 @@
|
||||
"devDependencies": {
|
||||
"@types/formidable": "^3.4.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
|
||||
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetDocumentsByIdsRequestSchema,
|
||||
ZGetDocumentsByIdsResponseSchema,
|
||||
getDocumentsByIdsMeta,
|
||||
} from './get-documents-by-ids.types';
|
||||
|
||||
export const getDocumentsByIdsRoute = authenticatedProcedure
|
||||
.meta(getDocumentsByIdsMeta)
|
||||
.input(ZGetDocumentsByIdsRequestSchema)
|
||||
.output(ZGetDocumentsByIdsResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { documentIds } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentIds,
|
||||
},
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
|
||||
ids: {
|
||||
type: 'documentId',
|
||||
ids: documentIds,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
});
|
||||
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: envelopes.map((envelope) => mapEnvelopesToDocumentMany(envelope)),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentManySchema } from '@documenso/lib/types/document';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const getDocumentsByIdsMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/get-many',
|
||||
summary: 'Get multiple documents',
|
||||
description: 'Retrieve multiple documents by their IDs',
|
||||
tags: ['Document'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZGetDocumentsByIdsRequestSchema = z.object({
|
||||
documentIds: z.array(z.number()).min(1),
|
||||
});
|
||||
|
||||
export const ZGetDocumentsByIdsResponseSchema = z.object({
|
||||
data: z.array(ZDocumentManySchema),
|
||||
});
|
||||
|
||||
export type TGetDocumentsByIdsRequest = z.infer<typeof ZGetDocumentsByIdsRequestSchema>;
|
||||
export type TGetDocumentsByIdsResponse = z.infer<typeof ZGetDocumentsByIdsResponseSchema>;
|
||||
@@ -19,6 +19,7 @@ import { findDocumentsInternalRoute } from './find-documents-internal';
|
||||
import { findInboxRoute } from './find-inbox';
|
||||
import { getDocumentRoute } from './get-document';
|
||||
import { getDocumentByTokenRoute } from './get-document-by-token';
|
||||
import { getDocumentsByIdsRoute } from './get-documents-by-ids';
|
||||
import { getInboxCountRoute } from './get-inbox-count';
|
||||
import { redistributeDocumentRoute } from './redistribute-document';
|
||||
import { searchDocumentRoute } from './search-document';
|
||||
@@ -27,6 +28,7 @@ import { updateDocumentRoute } from './update-document';
|
||||
|
||||
export const documentRouter = router({
|
||||
get: getDocumentRoute,
|
||||
getMany: getDocumentsByIdsRoute,
|
||||
find: findDocumentsRoute,
|
||||
create: createDocumentRoute,
|
||||
update: updateDocumentRoute,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
folderId,
|
||||
meta,
|
||||
attachments,
|
||||
delegatedDocumentOwner,
|
||||
} = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
@@ -144,6 +145,7 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
recipients: recipientsToCreate,
|
||||
folderId,
|
||||
envelopeItems,
|
||||
delegatedDocumentOwner,
|
||||
},
|
||||
attachments,
|
||||
meta,
|
||||
|
||||
@@ -41,6 +41,11 @@ export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||
export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
type: z.nativeEnum(EnvelopeType),
|
||||
delegatedDocumentOwner: z
|
||||
.string()
|
||||
.email()
|
||||
.describe('The email of the user who will own the document.')
|
||||
.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { findEnvelopes } from '@documenso/lib/server-only/envelope/find-envelopes';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindEnvelopesRequestSchema,
|
||||
ZFindEnvelopesResponseSchema,
|
||||
findEnvelopesMeta,
|
||||
} from './find-envelopes.types';
|
||||
|
||||
export const findEnvelopesRoute = authenticatedProcedure
|
||||
.meta(findEnvelopesMeta)
|
||||
.input(ZFindEnvelopesRequestSchema)
|
||||
.output(ZFindEnvelopesResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
|
||||
const {
|
||||
query,
|
||||
type,
|
||||
templateId,
|
||||
page,
|
||||
perPage,
|
||||
orderByDirection,
|
||||
orderByColumn,
|
||||
source,
|
||||
status,
|
||||
folderId,
|
||||
} = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
query,
|
||||
type,
|
||||
templateId,
|
||||
source,
|
||||
status,
|
||||
folderId,
|
||||
page,
|
||||
perPage,
|
||||
},
|
||||
});
|
||||
|
||||
return await findEnvelopes({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type,
|
||||
templateId,
|
||||
query,
|
||||
source,
|
||||
status,
|
||||
page,
|
||||
perPage,
|
||||
folderId,
|
||||
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DocumentSource, DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEnvelopeManySchema } from '@documenso/lib/types/envelope';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const findEnvelopesMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/envelope',
|
||||
summary: 'Find envelopes',
|
||||
description: 'Find envelopes based on search criteria',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZFindEnvelopesRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
type: z
|
||||
.nativeEnum(EnvelopeType)
|
||||
.describe('Filter envelopes by type (DOCUMENT or TEMPLATE).')
|
||||
.optional(),
|
||||
templateId: z
|
||||
.number()
|
||||
.describe('Filter envelopes by the template ID used to create it.')
|
||||
.optional(),
|
||||
source: z
|
||||
.nativeEnum(DocumentSource)
|
||||
.describe('Filter envelopes by how it was created.')
|
||||
.optional(),
|
||||
status: z
|
||||
.nativeEnum(DocumentStatus)
|
||||
.describe('Filter envelopes by the current status.')
|
||||
.optional(),
|
||||
folderId: z.string().describe('Filter envelopes by folder ID.').optional(),
|
||||
orderByColumn: z.enum(['createdAt']).optional(),
|
||||
orderByDirection: z.enum(['asc', 'desc']).describe('Sort direction.').default('desc'),
|
||||
});
|
||||
|
||||
export const ZFindEnvelopesResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZEnvelopeManySchema.array(),
|
||||
});
|
||||
|
||||
export type TFindEnvelopesRequest = z.infer<typeof ZFindEnvelopesRequestSchema>;
|
||||
export type TFindEnvelopesResponse = z.infer<typeof ZFindEnvelopesResponseSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getEnvelopesByIds } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetEnvelopesByIdsRequestSchema,
|
||||
ZGetEnvelopesByIdsResponseSchema,
|
||||
getEnvelopesByIdsMeta,
|
||||
} from './get-envelopes-by-ids.types';
|
||||
|
||||
export const getEnvelopesByIdsRoute = authenticatedProcedure
|
||||
.meta(getEnvelopesByIdsMeta)
|
||||
.input(ZGetEnvelopesByIdsRequestSchema)
|
||||
.output(ZGetEnvelopesByIdsResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { ids } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
|
||||
const envelopes = await getEnvelopesByIds({
|
||||
ids,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: null,
|
||||
});
|
||||
|
||||
return {
|
||||
data: envelopes,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const getEnvelopesByIdsMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/envelope/get-many',
|
||||
summary: 'Get multiple envelopes',
|
||||
description: 'Retrieve multiple envelopes by their IDs',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZEnvelopeIdsSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('envelopeId'),
|
||||
ids: z.array(z.string()).min(1).max(20),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('documentId'),
|
||||
ids: z.array(z.number()).min(1).max(20),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('templateId'),
|
||||
ids: z.array(z.number()).min(1).max(20),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const ZGetEnvelopesByIdsRequestSchema = z.object({
|
||||
ids: ZEnvelopeIdsSchema,
|
||||
});
|
||||
|
||||
export const ZGetEnvelopesByIdsResponseSchema = z.object({
|
||||
data: z.array(ZEnvelopeSchema),
|
||||
});
|
||||
|
||||
export type TGetEnvelopesByIdsRequest = z.infer<typeof ZGetEnvelopesByIdsRequestSchema>;
|
||||
export type TGetEnvelopesByIdsResponse = z.infer<typeof ZGetEnvelopesByIdsResponseSchema>;
|
||||
@@ -18,10 +18,12 @@ import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-enve
|
||||
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
|
||||
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
|
||||
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
||||
import { findEnvelopesRoute } from './find-envelopes';
|
||||
import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs';
|
||||
import { getEnvelopeRoute } from './get-envelope';
|
||||
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
||||
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
||||
import { getEnvelopesByIdsRoute } from './get-envelopes-by-ids';
|
||||
import { redistributeEnvelopeRoute } from './redistribute-envelope';
|
||||
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
|
||||
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
|
||||
@@ -66,10 +68,12 @@ export const envelopeRouter = router({
|
||||
set: setEnvelopeFieldsRoute,
|
||||
sign: signEnvelopeFieldRoute,
|
||||
},
|
||||
find: findEnvelopesRoute,
|
||||
auditLog: {
|
||||
find: findEnvelopeAuditLogsRoute,
|
||||
},
|
||||
get: getEnvelopeRoute,
|
||||
getMany: getEnvelopesByIdsRoute,
|
||||
create: createEnvelopeRoute,
|
||||
use: useEnvelopeRoute,
|
||||
update: updateEnvelopeRoute,
|
||||
|
||||
@@ -36,6 +36,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
@@ -99,6 +100,10 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
const derivedDrawSignatureEnabled =
|
||||
drawSignatureEnabled ?? organisation.organisationGlobalSettings.drawSignatureEnabled;
|
||||
|
||||
const derivedDelegateDocumentOwnership =
|
||||
delegateDocumentOwnership ??
|
||||
organisation.organisationGlobalSettings.delegateDocumentOwnership;
|
||||
|
||||
if (
|
||||
derivedTypedSignatureEnabled === false &&
|
||||
derivedUploadSignatureEnabled === false &&
|
||||
@@ -140,6 +145,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership: derivedDelegateDocumentOwnership,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
|
||||
@@ -22,6 +22,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
typedSignatureEnabled: z.boolean().optional(),
|
||||
uploadSignatureEnabled: z.boolean().optional(),
|
||||
drawSignatureEnabled: z.boolean().optional(),
|
||||
delegateDocumentOwnership: z.boolean().nullish(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().optional(),
|
||||
|
||||
@@ -39,6 +39,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
@@ -150,6 +151,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
typedSignatureEnabled: z.boolean().nullish(),
|
||||
uploadSignatureEnabled: z.boolean().nullish(),
|
||||
drawSignatureEnabled: z.boolean().nullish(),
|
||||
delegateDocumentOwnership: z.boolean().nullish(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().nullish(),
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
|
||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
|
||||
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTemplatesByIdsRequestSchema,
|
||||
ZGetTemplatesByIdsResponseSchema,
|
||||
getTemplatesByIdsMeta,
|
||||
} from './get-templates-by-ids.types';
|
||||
|
||||
export const getTemplatesByIdsRoute = authenticatedProcedure
|
||||
.meta(getTemplatesByIdsMeta)
|
||||
.input(ZGetTemplatesByIdsRequestSchema)
|
||||
.output(ZGetTemplatesByIdsResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { templateIds } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateIds,
|
||||
},
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
|
||||
ids: {
|
||||
type: 'templateId',
|
||||
ids: templateIds,
|
||||
},
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
});
|
||||
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
select: {
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const templates = envelopes.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
const firstTemplateDocumentData = envelope.envelopeItems[0].documentData;
|
||||
|
||||
return {
|
||||
id: legacyTemplateId,
|
||||
envelopeId: envelope.id,
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
team: envelope.team
|
||||
? {
|
||||
id: envelope.team.id,
|
||||
url: envelope.team.url,
|
||||
}
|
||||
: null,
|
||||
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
recipients: envelope.recipients.map((recipient) =>
|
||||
mapRecipientToLegacyRecipient(recipient, envelope),
|
||||
),
|
||||
templateMeta: envelope.documentMeta
|
||||
? {
|
||||
signingOrder: envelope.documentMeta.signingOrder,
|
||||
distributionMethod: envelope.documentMeta.distributionMethod,
|
||||
}
|
||||
: null,
|
||||
directLink: envelope.directLink
|
||||
? {
|
||||
token: envelope.directLink.token,
|
||||
enabled: envelope.directLink.enabled,
|
||||
}
|
||||
: null,
|
||||
templateDocumentDataId: firstTemplateDocumentData.id, // Backwards compatibility.
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: templates,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZTemplateManySchema } from '@documenso/lib/types/template';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const getTemplatesByIdsMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/template/get-many',
|
||||
summary: 'Get multiple templates',
|
||||
description: 'Retrieve multiple templates by their IDs',
|
||||
tags: ['Template'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZGetTemplatesByIdsRequestSchema = z.object({
|
||||
templateIds: z.array(z.number()).min(1),
|
||||
});
|
||||
|
||||
export const ZGetTemplatesByIdsResponseSchema = z.object({
|
||||
data: z.array(ZTemplateManySchema),
|
||||
});
|
||||
|
||||
export type TGetTemplatesByIdsRequest = z.infer<typeof ZGetTemplatesByIdsRequestSchema>;
|
||||
export type TGetTemplatesByIdsResponse = z.infer<typeof ZGetTemplatesByIdsResponseSchema>;
|
||||
@@ -30,6 +30,7 @@ import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
|
||||
|
||||
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
|
||||
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
|
||||
import { getTemplatesByIdsRoute } from './get-templates-by-ids';
|
||||
import {
|
||||
ZBulkSendTemplateMutationSchema,
|
||||
ZCreateDocumentFromDirectTemplateRequestSchema,
|
||||
@@ -154,6 +155,11 @@ export const templateRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
getMany: getTemplatesByIdsRoute,
|
||||
|
||||
/**
|
||||
* Wait until RR7 so we can passthrough documents.
|
||||
*
|
||||
|
||||
@@ -626,7 +626,7 @@ export const AddFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
'dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white text-muted-foreground ring-2 transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles?.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
@@ -728,7 +728,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground font-signature flex items-center justify-center gap-x-1.5 text-lg font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 font-signature text-lg font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Trans>Signature</Trans>
|
||||
@@ -752,7 +752,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
@@ -777,7 +777,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
@@ -802,7 +802,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
@@ -827,7 +827,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
@@ -852,7 +852,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Type className="h-4 w-4" />
|
||||
@@ -877,7 +877,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Hash className="h-4 w-4" />
|
||||
@@ -902,7 +902,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Disc className="h-4 w-4" />
|
||||
@@ -927,7 +927,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
@@ -953,7 +953,7 @@ export const AddFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
|
||||
@@ -581,7 +581,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
'dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white text-muted-foreground ring-2 transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles?.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
@@ -650,7 +650,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
|
||||
'mb-12 mt-2 justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||
selectedSignerStyles?.comboxBoxTrigger,
|
||||
)}
|
||||
>
|
||||
@@ -681,7 +681,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<span className="inline-block px-4 text-muted-foreground">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
@@ -689,14 +689,14 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
{/* Note: This is duplicated in `add-fields.tsx` */}
|
||||
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
@@ -720,7 +720,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
className={cn('truncate text-foreground/70', {
|
||||
'text-foreground/80': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
@@ -768,7 +768,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground font-signature flex items-center justify-center gap-x-1.5 text-lg font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 font-signature text-lg font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Trans>Signature</Trans>
|
||||
@@ -793,7 +793,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
@@ -819,7 +819,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
@@ -845,7 +845,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
@@ -871,7 +871,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
@@ -897,7 +897,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Type className="h-4 w-4" />
|
||||
@@ -923,7 +923,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Hash className="h-4 w-4" />
|
||||
@@ -949,7 +949,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Disc className="h-4 w-4" />
|
||||
@@ -975,7 +975,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
@@ -1002,7 +1002,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
<CardContent className="p-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||
'flex items-center justify-center gap-x-1.5 text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
|
||||