Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b041c23b4 | |||
| 7b6e948aa2 | |||
| f6d81b22bd | |||
| c861dd2ee2 | |||
| 7eabae4b4b | |||
| ae4272a6b6 | |||
| fd672943d1 | |||
| c2ea5e5859 | |||
| c1217c5a58 | |||
| 27eb2d65d4 | |||
| ef407cb0b4 | |||
| 1e20561e91 | |||
| a2ec5f0fa1 | |||
| de8d13a4c1 | |||
| 495d61a11d | |||
| 90fdba8000 | |||
| aa1cada79b | |||
| 790b385849 | |||
| baa2c51123 | |||
| 1e585e06e6 | |||
| 5624484631 | |||
| 810e00da03 | |||
| eeeee2fa0e | |||
| c50a31a503 | |||
| 7360709795 | |||
| df678d7d69 | |||
| 6739242554 | |||
| a5e5eecf8b | |||
| b0248c20eb | |||
| f129968968 | |||
| c5c87e3fd1 | |||
| 24a74c7b57 | |||
| f0a5a7e816 | |||
| 8462cd13fd | |||
| 576846de32 | |||
| 06071ea035 | |||
| b45a2691ba | |||
| f31cc575d0 | |||
| 05d7015ef0 | |||
| 2ca5d6cfaa | |||
| 04814ca14e | |||
| dd1dccdb6a | |||
| df4316ac5c | |||
| 02f1264eea | |||
| 928edb8645 | |||
| 54b0e4964e | |||
| 68e6ccdd19 | |||
| 09ab7e9a09 | |||
| 3bb0777914 | |||
| 4d6389e901 | |||
| 51e3d5030d | |||
| 0cebdec637 | |||
| 43486d8448 | |||
| 4d3d1b8d14 | |||
| 0387f3c20a | |||
| c5032d0c43 | |||
| 3bd34964cd | |||
| fe93b11a2c | |||
| 7638faf27b | |||
| 8fca029d96 |
@@ -65,6 +65,47 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
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:
|
create_and_publish_manifest:
|
||||||
name: Create and publish manifest
|
name: Create and publish manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -125,6 +166,43 @@ jobs:
|
|||||||
docker manifest push documenso/documenso:$GIT_SHA
|
docker manifest push documenso/documenso:$GIT_SHA
|
||||||
docker manifest push documenso/documenso:$APP_VERSION
|
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
|
- name: Create and push Github Container Registry manifest
|
||||||
run: |
|
run: |
|
||||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
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:$GIT_SHA
|
||||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION
|
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]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
},
|
||||||
|
"prisma.pinToPrisma6": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "^15.5.7",
|
"next": "15.5.9",
|
||||||
"next-plausible": "^3.12.5",
|
"next-plausible": "^3.12.5",
|
||||||
"nextra": "^3",
|
"nextra": "^3",
|
||||||
"nextra-theme-docs": "^3",
|
"nextra-theme-docs": "^3",
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
"pagefind": "^1.2.0",
|
"pagefind": "^1.2.0",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,22 @@ description: Learn how to get the coordinates of a field in a document.
|
|||||||
|
|
||||||
## Field Coordinates
|
## Field Coordinates
|
||||||
|
|
||||||
Field coordinates represent the position of a field in a document. They are returned in the `pageX` and `pageY` properties of the field.
|
Field coordinates represent the position of a field in a document. They are returned in the `pageX`, `pageY`, `width` and `height` properties of the field.
|
||||||
|
|
||||||
To enable field coordinates, you can use the `devmode` query parameter.
|
To enable field coordinates, you can use the `devmode` query parameter.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://app.documenso.com/documents/<document-id>/edit?devmode=true
|
# Legacy editor
|
||||||
|
|
||||||
|
https://app.documenso.com/t/<team-url>/documents/<envelope-id>/legacy_editor?devmode=true
|
||||||
```
|
```
|
||||||
|
|
||||||
You should then see the coordinates on top of each field.
|

|
||||||
|
|
||||||

|
```bash
|
||||||
|
# New editor
|
||||||
|
|
||||||
|
https://app.documenso.com/t/<team-url>/documents/<envelope-id>/edit?step=addFields&devmode=true
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -61,6 +61,6 @@ You can access the following services:
|
|||||||
- Main application - http://localhost:3000
|
- Main application - http://localhost:3000
|
||||||
- Incoming Mail Access - http://localhost:9000
|
- Incoming Mail Access - http://localhost:9000
|
||||||
- Database Connection Details:
|
- Database Connection Details:
|
||||||
- Port: 54320
|
- Port: 54320
|
||||||
- Connection: Use your favourite database client to connect to the database.
|
- Connection: Use your favorite database client to connect to the database.
|
||||||
- S3 Storage Dashboard - http://localhost:9001
|
- 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';
|
import { Callout } from 'nextra/components';
|
||||||
|
|
||||||
# Rate Limits
|
# Rate Limits
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ This method avoids file permission issues by creating the certificate directly i
|
|||||||
|
|
||||||
# Generate certificate inside container using environment variable
|
# Generate certificate inside container using environment variable
|
||||||
docker exec -e CERT_PASS="$CERT_PASS" -it documenso-production-documenso-1 bash -c "
|
docker exec -e CERT_PASS="$CERT_PASS" -it documenso-production-documenso-1 bash -c "
|
||||||
|
mkdir -p /app/certs && \
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
-keyout /tmp/private.key \
|
-keyout /tmp/private.key \
|
||||||
-out /tmp/certificate.crt \
|
-out /tmp/certificate.crt \
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Telemetry
|
||||||
|
description: Learn about the telemetry data that Documenso collects from self-hosted instances.
|
||||||
|
---
|
||||||
|
|
||||||
# Telemetry
|
# 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.
|
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
|
||||||
|
|
||||||
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:
|
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
|
## 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.
|
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`.
|
- 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.
|
- 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.
|
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
|
## 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.
|
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
|
## 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';
|
import { Callout } from 'nextra/components';
|
||||||
|
|
||||||
# Signature Levels
|
# Signature Levels
|
||||||
@@ -26,20 +31,20 @@ ensures the legal validity and enforceability of electronic signatures and recor
|
|||||||
|
|
||||||
### Main Requirements
|
### Main Requirements
|
||||||
|
|
||||||
- [x] Intent to Sign: "Parties must demonstrate their intent to sign [..]"
|
- [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] **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] **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] **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] **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)
|
## UETA (Uniform Electronic Transactions Act)
|
||||||
|
|
||||||
<Callout type="info" emoji="✅">
|
<Callout type="info" emoji="✅">
|
||||||
Status: Compliant
|
Status: Compliant
|
||||||
</Callout>
|
</Callout>
|
||||||
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of electronic
|
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of
|
||||||
signatures and records in electronic transactions, ensuring they have the same validity and enforceability
|
electronic signatures and records in electronic transactions, ensuring they have the same validity
|
||||||
as paper documents and handwritten signatures.
|
and enforceability as paper documents and handwritten signatures.
|
||||||
|
|
||||||
### Main Requirements
|
### Main Requirements
|
||||||
|
|
||||||
@@ -50,9 +55,9 @@ _See [ESIGN](/users/compliance/signature-levels#-esign-electronic-signatures-in-
|
|||||||
<Callout type="info" emoji="✅">
|
<Callout type="info" emoji="✅">
|
||||||
Status: Compliant for Level 1 - SES (Simple Electronic Signatures)
|
Status: Compliant for Level 1 - SES (Simple Electronic Signatures)
|
||||||
</Callout>
|
</Callout>
|
||||||
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that standardizes
|
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that
|
||||||
electronic identification and trust services for secure and seamless electronic transactions across European
|
standardizes electronic identification and trust services for secure and seamless electronic
|
||||||
member states.
|
transactions across European member states.
|
||||||
|
|
||||||
### Level 1 - SES (Simple Electronic Signatures)
|
### 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
|
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.
|
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||||
</Callout>
|
</Callout>
|
||||||
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique identification
|
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique
|
||||||
of the signer and data integrity.
|
identification of the signer and data integrity.
|
||||||
|
|
||||||
### Main Requirements
|
### 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
|
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.
|
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||||
</Callout>
|
</Callout>
|
||||||
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a handwritten
|
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a
|
||||||
signature within the EU.
|
handwritten signature within the EU.
|
||||||
|
|
||||||
### Main Requirements
|
### 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';
|
import { Callout } from 'nextra/components';
|
||||||
|
|
||||||
## 21 CFR Part 11
|
## 21 CFR Part 11
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ The dropdown/select field collects a single choice from a list of options.
|
|||||||
|
|
||||||
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
||||||
|
|
||||||
{/*  */}
|

|
||||||
|
|
||||||
The dropdown/select field settings include:
|
The dropdown/select field settings include:
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Email Domains
|
||||||
|
description: Learn how to create and manage email domains in Documenso.
|
||||||
|
---
|
||||||
|
|
||||||
import { Callout, Steps } from 'nextra/components';
|
import { Callout, Steps } from 'nextra/components';
|
||||||
|
|
||||||
# Email Domains
|
# Email Domains
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ import { Callout } from 'nextra/components';
|
|||||||
|
|
||||||
# Fair Use Policy
|
# 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
|
### 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">
|
<Callout type="info">
|
||||||
What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
|
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
|
pricing. We won’t block your account without reaching out. You can [message
|
||||||
us](mailto:support@documenso.com) for questions. It's probably fine, though.
|
us](mailto:support@documenso.com) for questions.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
### DO
|
### DO
|
||||||
|
|
||||||
- Sign as many documents with the individual plan for your single business or organization you are part of
|
- 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 Zapier to automate all your signing to sign as much as possible
|
- Use the API and automation tools to automate all your signing workflows
|
||||||
- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
|
- Experiment with the plans and integrations, testing what you want to build
|
||||||
|
|
||||||
### DON'T
|
### DON'T
|
||||||
|
|
||||||
- Use the individual account's API to power a platform
|
- 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
|
- 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>
|
<Steps>
|
||||||
### Pick a Plan
|
### 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.
|
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
|
### 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
|
### Optional: Create a Team
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Community Edition
|
||||||
|
description: Learn about the Community Edition of Documenso.
|
||||||
|
---
|
||||||
|
|
||||||
import { Callout } from 'nextra/components';
|
import { Callout } from 'nextra/components';
|
||||||
|
|
||||||
# Community Edition
|
# Community Edition
|
||||||
@@ -32,10 +37,10 @@ Documenso and the Community Edition are licensed under [AGPL3](https://github.co
|
|||||||
|
|
||||||
### Conditions
|
### Conditions
|
||||||
|
|
||||||
ℹ️ License and copyright notice
|
- License and copyright notice
|
||||||
ℹ️ State changes
|
- State changes
|
||||||
ℹ️ Disclose source
|
- Disclose source
|
||||||
ℹ️ Network use is distribution
|
- Network use is distribution
|
||||||
|
|
||||||
<Callout type="warning">
|
<Callout type="warning">
|
||||||
It's important to remember that you must keep the AGPL3 license for your modified or non-modified
|
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';
|
import { Callout } from 'nextra/components';
|
||||||
|
|
||||||
# Enterprise Edition
|
# Enterprise Edition
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Licenses
|
||||||
|
description: Learn about the different licenses for self-hosting Documenso.
|
||||||
|
---
|
||||||
|
|
||||||
# Self-Hosting Licenses
|
# Self-Hosting Licenses
|
||||||
|
|
||||||
Documenso comes in two versions for self-hosting:
|
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
|
### 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:
|
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.
|
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).
|
You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
|
||||||
|
|
||||||
## Paid Account Support
|
## 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.
|
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.
|
If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
|
||||||
|
|
||||||
## Enterprise Support
|
## 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.
|
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.
|
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';
|
import { Callout, Steps } from 'nextra/components';
|
||||||
|
|
||||||
# Document Templates
|
# Document Templates
|
||||||
|
|||||||
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
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 |
@@ -12,11 +12,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"next": "^15.5.7"
|
"next": "15.5.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "18.3.27",
|
"@types/react": "18.3.27",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ export const AdminOrganisationMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating{' '}
|
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||||
<span className="font-bold">{organisationMemberName}.</span>
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
type AiFeaturesEnableDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onEnabled: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiFeaturesEnableDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onEnabled,
|
||||||
|
}: AiFeaturesEnableDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
|
const isTeamAdmin = team.currentTeamRole === TeamMemberRole.ADMIN;
|
||||||
|
const isOrganisationAdmin = organisation.currentOrganisationRole === OrganisationMemberRole.ADMIN;
|
||||||
|
const canEnableAiFeatures = isTeamAdmin || isOrganisationAdmin;
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { mutateAsync: updateTeamSettings, isPending: isUpdatingTeamSettings } =
|
||||||
|
trpc.team.settings.update.useMutation();
|
||||||
|
const { mutateAsync: updateOrganisationSettings, isPending: isUpdatingOrganisationSettings } =
|
||||||
|
trpc.organisation.settings.update.useMutation();
|
||||||
|
|
||||||
|
const isSubmitting = isUpdatingTeamSettings || isUpdatingOrganisationSettings;
|
||||||
|
|
||||||
|
const onEnableClick = async () => {
|
||||||
|
if (!canEnableAiFeatures) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTeamAdmin) {
|
||||||
|
await updateTeamSettings({
|
||||||
|
teamId: team.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateOrganisationSettings({
|
||||||
|
organisationId: organisation.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnabled();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to enable AI features', err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: t`We couldn't enable AI features right now. Please try again.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
Turn on AI detection to automatically find recipients and fields in your documents. AI
|
||||||
|
providers do not retain your data for training.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Alert variant="neutral">
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
Your document content will be sent securely to our AI provider solely for detection
|
||||||
|
and will not be stored or used for training.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
You're an admin. You can enable AI features for this team right away. Everyone on
|
||||||
|
the team will see AI detection once enabled.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
AI features are disabled for your team. Please ask your team owner or organisation
|
||||||
|
owner to enable them.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<Button type="button" onClick={() => void onEnableClick()} loading={isSubmitting}>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
||||||
@@ -232,10 +232,19 @@ export const AiFieldDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.fieldsDetected}
|
||||||
{progress.fieldsDetected} field(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # field found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # fields found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -279,7 +288,11 @@ export const AiFieldDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>We found {detectedFields.length} field(s) in your document.</Trans>
|
<Plural
|
||||||
|
value={detectedFields.length}
|
||||||
|
one="We found # field in your document."
|
||||||
|
other="We found # fields in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
@@ -190,10 +190,19 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.recipientsDetected}
|
||||||
{progress.recipientsDetected} recipient(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipient found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipients found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -237,9 +246,11 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Plural
|
||||||
We found {detectedRecipients.length} recipient(s) in your document.
|
value={detectedRecipients.length}
|
||||||
</Trans>
|
one="We found # recipient in your document."
|
||||||
|
other="We found # recipients in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -353,8 +353,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Reply To Email</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Reply To Email{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -372,8 +374,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Subject</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Subject{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -390,8 +394,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex flex-row items-center">
|
<FormLabel className="flex flex-row items-center">
|
||||||
<Trans>Message</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Message{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const EnvelopeItemDeleteDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You cannot delete this item because the document has been sent to recipients
|
You cannot delete this item because the document has been sent to recipients.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
|
|||||||
<Trans>
|
<Trans>
|
||||||
Once you update your DNS records, it may take up to 48 hours for it to be propogated.
|
Once you update your DNS records, it may take up to 48 hours for it to be propogated.
|
||||||
Once the DNS propagation is complete you will need to come back and press the "Sync"
|
Once the DNS propagation is complete you will need to come back and press the "Sync"
|
||||||
domains button
|
domains button.
|
||||||
</Trans>
|
</Trans>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export const OrganisationMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating{' '}
|
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||||
<span className="font-bold">{organisationMemberName}.</span>
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { DocumentSigningDisclosure } from '../general/document-signing/document-
|
|||||||
|
|
||||||
export type SignFieldSignatureDialogProps = {
|
export type SignFieldSignatureDialogProps = {
|
||||||
initialSignature?: string;
|
initialSignature?: string;
|
||||||
|
fullName?: string;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
uploadSignatureEnabled?: boolean;
|
uploadSignatureEnabled?: boolean;
|
||||||
drawSignatureEnabled?: boolean;
|
drawSignatureEnabled?: boolean;
|
||||||
@@ -28,6 +29,7 @@ export const SignFieldSignatureDialog = createCallable<
|
|||||||
>(
|
>(
|
||||||
({
|
({
|
||||||
call,
|
call,
|
||||||
|
fullName,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
@@ -46,6 +48,7 @@ export const SignFieldSignatureDialog = createCallable<
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
|
fullName={fullName}
|
||||||
value={localSignature ?? ''}
|
value={localSignature ?? ''}
|
||||||
onChange={({ value }) => setLocalSignature(value)}
|
onChange={({ value }) => setLocalSignature(value)}
|
||||||
typedSignatureEnabled={typedSignatureEnabled}
|
typedSignatureEnabled={typedSignatureEnabled}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -30,6 +31,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -53,26 +61,34 @@ export const TeamDeleteDialog = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { refreshSession } = useSession();
|
const { refreshSession } = useSession();
|
||||||
|
|
||||||
|
const currentOrganisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const deleteMessage = _(msg`delete ${teamName}`);
|
const deleteMessage = _(msg`delete ${teamName}`);
|
||||||
|
|
||||||
|
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
|
||||||
|
|
||||||
const ZDeleteTeamFormSchema = z.object({
|
const ZDeleteTeamFormSchema = z.object({
|
||||||
teamName: z.literal(deleteMessage, {
|
teamName: z.literal(deleteMessage, {
|
||||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||||
}),
|
}),
|
||||||
|
transferTeamId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ZDeleteTeamFormSchema),
|
resolver: zodResolver(ZDeleteTeamFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
teamName: '',
|
teamName: '',
|
||||||
|
transferTeamId: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
|
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async (data: z.infer<typeof ZDeleteTeamFormSchema>) => {
|
||||||
try {
|
try {
|
||||||
await deleteTeam({ teamId });
|
const transferTeamId = data.transferTeamId ? parseInt(data.transferTeamId, 10) : undefined;
|
||||||
|
|
||||||
|
await deleteTeam({ teamId, transferTeamId: transferTeamId || undefined });
|
||||||
|
|
||||||
await refreshSession();
|
await refreshSession();
|
||||||
|
|
||||||
@@ -168,6 +184,43 @@ export const TeamDeleteDialog = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{filteredTeams.length > 0 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="transferTeamId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Transfer documents to a different team</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select {...field} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={_(msg`Don't transfer (Delete all documents)`)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="-1">
|
||||||
|
<Trans>Don't transfer (Delete all documents)</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
{filteredTeams.map((team) => (
|
||||||
|
<SelectItem key={team.id} value={team.id.toString()}>
|
||||||
|
{team.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const TeamMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating <span className="font-bold">{memberName}.</span>
|
You are currently updating <span className="font-bold">{memberName}</span>.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ export const TemplateBulkSendDialog = ({
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
<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">
|
<h3 className="text-sm font-medium">
|
||||||
<Trans>CSV Structure</Trans>
|
<Trans>CSV Structure</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
For each recipient, provide their email (required) and name (optional) in separate
|
For each recipient, provide their email (required) and name (optional) in separate
|
||||||
columns. Download the template CSV below for the correct format.
|
columns. Download the template CSV below for the correct format.
|
||||||
@@ -153,7 +153,7 @@ export const TemplateBulkSendDialog = ({
|
|||||||
<Trans>Current recipients:</Trans>
|
<Trans>Current recipients:</Trans>
|
||||||
</p>
|
</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) => (
|
{recipients.map((recipient, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
||||||
@@ -167,7 +167,7 @@ export const TemplateBulkSendDialog = ({
|
|||||||
<Trans>Download Template CSV</Trans>
|
<Trans>Download Template CSV</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-xs text-muted-foreground">
|
||||||
<Trans>Pre-formatted CSV template with example data.</Trans>
|
<Trans>Pre-formatted CSV template with example data.</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,14 +200,14 @@ export const TemplateBulkSendDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-10 items-center rounded-md border px-3">
|
<div className="flex h-10 items-center rounded-md border px-3">
|
||||||
<div className="flex flex-1 items-center gap-2">
|
<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>
|
<span className="flex-1 truncate text-sm">{value.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="link"
|
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)}
|
onClick={() => onChange(null)}
|
||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -220,9 +220,9 @@ export const TemplateBulkSendDialog = ({
|
|||||||
)}
|
)}
|
||||||
</FormControl>
|
</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>
|
<Trans>
|
||||||
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
||||||
template defaults.
|
template defaults.
|
||||||
@@ -247,7 +247,7 @@ export const TemplateBulkSendDialog = ({
|
|||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor="send-immediately"
|
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>
|
<Trans>Send documents to recipients immediately</Trans>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import { FilePlus, Loader } from 'lucide-react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
type TemplateCreateDialogProps = {
|
|
||||||
folderId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { user } = useSession();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
|
||||||
|
|
||||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
|
||||||
|
|
||||||
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
|
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
||||||
|
|
||||||
const onFileDrop = async (files: File[]) => {
|
|
||||||
const file = files[0];
|
|
||||||
|
|
||||||
if (isUploadingFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUploadingFile(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
title: file.name,
|
|
||||||
folderId: folderId,
|
|
||||||
} satisfies TCreateTemplatePayloadSchema;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('payload', JSON.stringify(payload));
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const { envelopeId: id } = await createTemplate(formData);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Template document uploaded`),
|
|
||||||
description: _(
|
|
||||||
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
|
|
||||||
),
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowTemplateCreateDialog(false);
|
|
||||||
|
|
||||||
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`Please try again later.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsUploadingFile(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={showTemplateCreateDialog}
|
|
||||||
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="cursor-pointer" disabled={!user.emailVerified}>
|
|
||||||
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
<Trans>Template (Legacy)</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent className="w-full max-w-xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans>New Template</Trans>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<Trans>
|
|
||||||
Templates allow you to quickly generate documents with pre-filled recipients and
|
|
||||||
fields.
|
|
||||||
</Trans>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
|
|
||||||
|
|
||||||
{isUploadingFile && (
|
|
||||||
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
|
|
||||||
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button type="button" variant="secondary" disabled={isUploadingFile}>
|
|
||||||
<Trans>Close</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -219,9 +219,8 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
|
|||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
A secret that will be sent to your URL so you can verify that the request
|
A secret that will be sent to your URL so you can verify that the request
|
||||||
has been sent by Documenso
|
has been sent by Documenso.
|
||||||
</Trans>
|
</Trans>
|
||||||
.
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={
|
typedSignatureEnabled={
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export type TDocumentPreferencesFormSchema = {
|
|||||||
includeSigningCertificate: boolean | null;
|
includeSigningCertificate: boolean | null;
|
||||||
includeAuditLog: boolean | null;
|
includeAuditLog: boolean | null;
|
||||||
signatureTypes: DocumentSignatureType[];
|
signatureTypes: DocumentSignatureType[];
|
||||||
|
delegateDocumentOwnership: boolean | null;
|
||||||
aiFeaturesEnabled: boolean | null;
|
aiFeaturesEnabled: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ type SettingsSubset = Pick<
|
|||||||
| 'typedSignatureEnabled'
|
| 'typedSignatureEnabled'
|
||||||
| 'uploadSignatureEnabled'
|
| 'uploadSignatureEnabled'
|
||||||
| 'drawSignatureEnabled'
|
| 'drawSignatureEnabled'
|
||||||
|
| 'delegateDocumentOwnership'
|
||||||
| 'aiFeaturesEnabled'
|
| 'aiFeaturesEnabled'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ export const DocumentPreferencesForm = ({
|
|||||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||||
message: msg`At least one signature type must be enabled`.id,
|
message: msg`At least one signature type must be enabled`.id,
|
||||||
}),
|
}),
|
||||||
|
delegateDocumentOwnership: z.boolean().nullable(),
|
||||||
aiFeaturesEnabled: z.boolean().nullable(),
|
aiFeaturesEnabled: z.boolean().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +128,7 @@ export const DocumentPreferencesForm = ({
|
|||||||
includeSigningCertificate: settings.includeSigningCertificate,
|
includeSigningCertificate: settings.includeSigningCertificate,
|
||||||
includeAuditLog: settings.includeAuditLog,
|
includeAuditLog: settings.includeAuditLog,
|
||||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||||
|
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
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 && (
|
{isAiFeaturesConfigured && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
|||||||
<Label htmlFor="email" className="text-muted-foreground">
|
<Label htmlFor="email" className="text-muted-foreground">
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
|
<Input id="email" type="email" className="mt-2 bg-muted" value={user.email} disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -124,6 +124,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={user.name ?? ''}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -244,11 +244,11 @@ export const SignInForm = ({
|
|||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AuthenticationErrorCode.InvalidCredentials,
|
AuthenticationErrorCode.InvalidCredentials,
|
||||||
() => msg`The email or password provided is incorrect`,
|
() => msg`The email or password provided is incorrect.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
AuthenticationErrorCode.InvalidTwoFactorCode,
|
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||||
() => msg`The two-factor authentication code provided is incorrect`,
|
() => msg`The two-factor authentication code provided is incorrect.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => handleFallbackErrorMessages(error.code));
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
@@ -368,7 +368,7 @@ export const SignInForm = ({
|
|||||||
<p className="mt-2 text-right">
|
<p className="mt-2 text-right">
|
||||||
<Link
|
<Link
|
||||||
to="/forgot-password"
|
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>
|
<Trans>Forgot your password?</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -390,11 +390,11 @@ export const SignInForm = ({
|
|||||||
<>
|
<>
|
||||||
{hasSocialAuthEnabled && (
|
{hasSocialAuthEnabled && (
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
<span className="text-muted-foreground bg-transparent">
|
<span className="bg-transparent text-muted-foreground">
|
||||||
<Trans>Or continue with</Trans>
|
<Trans>Or continue with</Trans>
|
||||||
</span>
|
</span>
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ export const SignInForm = ({
|
|||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="border bg-background text-muted-foreground"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithGoogleClick}
|
onClick={onSignInWithGoogleClick}
|
||||||
>
|
>
|
||||||
@@ -417,7 +417,7 @@ export const SignInForm = ({
|
|||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="border bg-background text-muted-foreground"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithMicrosoftClick}
|
onClick={onSignInWithMicrosoftClick}
|
||||||
>
|
>
|
||||||
@@ -435,7 +435,7 @@ export const SignInForm = ({
|
|||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="border bg-background text-muted-foreground"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithOIDCClick}
|
onClick={onSignInWithOIDCClick}
|
||||||
>
|
>
|
||||||
@@ -452,7 +452,7 @@ export const SignInForm = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
loading={isPasskeyLoading}
|
loading={isPasskeyLoading}
|
||||||
className="bg-background text-muted-foreground border"
|
className="border bg-background text-muted-foreground"
|
||||||
onClick={onSignInWithPasskey}
|
onClick={onSignInWithPasskey}
|
||||||
>
|
>
|
||||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
|||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.UNAUTHORIZED,
|
AppErrorCode.UNAUTHORIZED,
|
||||||
() => msg`You do not have permission to create a token for this team`,
|
() => msg`You do not have permission to create a token for this team.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
||||||
|
|
||||||
@@ -212,12 +212,12 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FormLabel className="text-muted-foreground mt-2">
|
<FormLabel className="mt-2 text-muted-foreground">
|
||||||
<Trans>Never expire</Trans>
|
<Trans>Never expire</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="block md:py-1.5">
|
<div className="block md:py-1.5">
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-background mt-2"
|
className="mt-2 bg-background"
|
||||||
checked={noExpirationDate}
|
checked={noExpirationDate}
|
||||||
onCheckedChange={setNoExpirationDate}
|
onCheckedChange={setNoExpirationDate}
|
||||||
/>
|
/>
|
||||||
@@ -254,14 +254,14 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
|||||||
>
|
>
|
||||||
<Card gradient>
|
<Card gradient>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
Your token was created successfully! Make sure to copy it because you won't be
|
Your token was created successfully! Make sure to copy it because you won't be
|
||||||
able to see it again!
|
able to see it again!
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
<p className="my-4 rounded-md bg-muted-foreground/10 px-2.5 py-1 font-mono text-sm">
|
||||||
{newlyCreatedToken.token}
|
{newlyCreatedToken.token}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import type { Field } from '@prisma/client';
|
import type { Field } from '@prisma/client';
|
||||||
@@ -57,8 +55,6 @@ export const DirectTemplateConfigureForm = ({
|
|||||||
initialEmail,
|
initialEmail,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: DirectTemplateConfigureFormProps) => {
|
}: DirectTemplateConfigureFormProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const { sessionData } = useOptionalSession();
|
const { sessionData } = useOptionalSession();
|
||||||
const user = sessionData?.user;
|
const user = sessionData?.user;
|
||||||
|
|
||||||
@@ -77,17 +73,7 @@ export const DirectTemplateConfigureForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TDirectTemplateConfigureFormSchema>({
|
const form = useForm<TDirectTemplateConfigureFormSchema>({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(ZDirectTemplateConfigureFormSchema),
|
||||||
ZDirectTemplateConfigureFormSchema.superRefine((items, ctx) => {
|
|
||||||
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: _(msg`Email cannot already exist in the template`),
|
|
||||||
path: ['email'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: initialEmail || '',
|
email: initialEmail || '',
|
||||||
},
|
},
|
||||||
@@ -138,7 +124,7 @@ export const DirectTemplateConfigureForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{!fieldState.error && (
|
{!fieldState.error && (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-xs text-muted-foreground">
|
||||||
<Trans>Enter your email address to receive the completed document.</Trans>
|
<Trans>Enter your email address to receive the completed document.</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(value) => setSignature(value)}
|
onChange={(value) => setSignature(value)}
|
||||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||||
@@ -433,7 +434,7 @@ export const DirectTemplateSigningForm = ({
|
|||||||
|
|
||||||
<div className="mt-4 flex gap-x-4">
|
<div className="mt-4 flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="w-full bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ export const DocumentSigningForm = ({
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-2 pb-2 sm:px-4 sm:pb-6">
|
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-2 pb-2 sm:px-4 sm:pb-6">
|
||||||
<div className="pointer-events-auto w-full max-w-[760px]">
|
<div className="pointer-events-auto w-full max-w-[760px]">
|
||||||
<div className="bg-card border-border overflow-hidden rounded-xl border shadow-2xl">
|
<div className="overflow-hidden rounded-xl border border-border bg-card shadow-2xl">
|
||||||
{/* Main Header Bar */}
|
{/* Main Header Bar */}
|
||||||
<div className="flex items-center justify-between gap-4 p-4">
|
<div className="flex items-center justify-between gap-4 p-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -48,15 +48,15 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<LucideChevronDown className="text-muted-foreground h-5 w-5 flex-shrink-0" />
|
<LucideChevronDown className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<LucideChevronUp className="text-muted-foreground h-5 w-5 flex-shrink-0" />
|
<LucideChevronUp className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-foreground text-lg font-semibold">
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
{match(recipient.role)
|
{match(recipient.role)
|
||||||
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
||||||
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
|
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
|
||||||
@@ -65,7 +65,7 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
.otherwise(() => null)}
|
.otherwise(() => null)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground -mt-0.5 text-sm">
|
<p className="-mt-0.5 text-sm text-muted-foreground">
|
||||||
{recipientFieldsRemaining.length === 0 ? (
|
{recipientFieldsRemaining.length === 0 ? (
|
||||||
match(recipient.role)
|
match(recipient.role)
|
||||||
.with(RecipientRole.VIEWER, () => (
|
.with(RecipientRole.VIEWER, () => (
|
||||||
@@ -102,11 +102,11 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
{recipient.role !== RecipientRole.VIEWER &&
|
{recipient.role !== RecipientRole.VIEWER &&
|
||||||
recipient.role !== RecipientRole.ASSISTANT && (
|
recipient.role !== RecipientRole.ASSISTANT && (
|
||||||
<div className="px-4 pb-3">
|
<div className="px-4 pb-3">
|
||||||
<div className="bg-muted relative h-[4px] rounded-md">
|
<div className="relative h-[4px] rounded-md bg-muted">
|
||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-signing-mobile-widget-progress-bar"
|
layoutId="document-signing-mobile-widget-progress-bar"
|
||||||
className="bg-documenso absolute inset-y-0 left-0"
|
className="absolute inset-y-0 left-0 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
@@ -117,11 +117,11 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
|
|
||||||
{/* Expandable Content */}
|
{/* Expandable Content */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-border animate-in slide-in-from-bottom-2 border-t p-4 duration-200">
|
<div className="border-t border-border p-4 duration-200 animate-in slide-in-from-bottom-2">
|
||||||
<EnvelopeSignerForm />
|
<EnvelopeSignerForm />
|
||||||
|
|
||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground mt-2 inline-block rounded px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100 lg:hidden">
|
<div className="mt-2 inline-block rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:hidden">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-flow-container-step"
|
layoutId="document-flow-container-step"
|
||||||
className="absolute inset-y-0 left-0 bg-documenso"
|
className="absolute inset-y-0 left-0 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -56,8 +56,11 @@ export const DocumentSigningSignatureField = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [fontSize, setFontSize] = useState(2);
|
const [fontSize, setFontSize] = useState(2);
|
||||||
|
|
||||||
const { signature: providedSignature, setSignature: setProvidedSignature } =
|
const {
|
||||||
useRequiredDocumentSigningContext();
|
fullName,
|
||||||
|
signature: providedSignature,
|
||||||
|
setSignature: setProvidedSignature,
|
||||||
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
@@ -236,13 +239,13 @@ export const DocumentSigningSignatureField = ({
|
|||||||
type="Signature"
|
type="Signature"
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-background">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="h-5 w-5 animate-spin text-primary md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state === 'empty' && (
|
{state === 'empty' && (
|
||||||
<p className="group-hover:text-primary font-signature text-muted-foreground group-hover:text-recipient-green text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200">
|
<p className="font-signature text-[clamp(0.575rem,25cqw,1.2rem)] text-xl text-muted-foreground duration-200 group-hover:text-primary group-hover:text-recipient-green">
|
||||||
<Trans>Signature</Trans>
|
<Trans>Signature</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -259,7 +262,7 @@ export const DocumentSigningSignatureField = ({
|
|||||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
|
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
|
||||||
<p
|
<p
|
||||||
ref={signatureRef}
|
ref={signatureRef}
|
||||||
className="font-signature text-muted-foreground w-full overflow-hidden break-all text-center leading-tight duration-200"
|
className="w-full overflow-hidden break-all text-center font-signature leading-tight text-muted-foreground duration-200"
|
||||||
style={{ fontSize: `${fontSize}rem` }}
|
style={{ fontSize: `${fontSize}rem` }}
|
||||||
>
|
>
|
||||||
{signature?.typedSignature}
|
{signature?.typedSignature}
|
||||||
@@ -272,12 +275,13 @@ export const DocumentSigningSignatureField = ({
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans>
|
<Trans>
|
||||||
Sign as {recipient.name}{' '}
|
Sign as {recipient.name}{' '}
|
||||||
<div className="text-muted-foreground h-5">({recipient.email})</div>
|
<div className="h-5 text-muted-foreground">({recipient.email})</div>
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
fullName={fullName}
|
||||||
value={localSignature ?? ''}
|
value={localSignature ?? ''}
|
||||||
onChange={({ value }) => setLocalSignature(value)}
|
onChange={({ value }) => setLocalSignature(value)}
|
||||||
typedSignatureEnabled={typedSignatureEnabled}
|
typedSignatureEnabled={typedSignatureEnabled}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
documentId,
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
}: DocumentPageViewRecentActivityProps) => {
|
}: DocumentPageViewRecentActivityProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -48,9 +48,9 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
|
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
|
<section className="flex flex-col rounded-xl border border-border bg-widget dark:bg-background">
|
||||||
<div className="flex flex-row items-center justify-between border-b px-4 py-3">
|
<div className="flex flex-row items-center justify-between border-b px-4 py-3">
|
||||||
<h1 className="text-foreground font-medium">
|
<h1 className="font-medium text-foreground">
|
||||||
<Trans>Recent activity</Trans>
|
<Trans>Recent activity</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -59,18 +59,18 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex h-full items-center justify-center py-16">
|
<div className="flex h-full items-center justify-center py-16">
|
||||||
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
|
<Loader className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoadingError && (
|
{isLoadingError && (
|
||||||
<div className="flex h-full flex-col items-center justify-center py-16">
|
<div className="flex h-full flex-col items-center justify-center py-16">
|
||||||
<p className="text-foreground/80 text-sm">
|
<p className="text-sm text-foreground/80">
|
||||||
<Trans>Unable to load document history</Trans>
|
<Trans>Unable to load document history</Trans>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={async () => refetch()}
|
onClick={async () => refetch()}
|
||||||
className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
|
className="mt-2 text-sm text-foreground/70 hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Trans>Click here to retry</Trans>
|
<Trans>Click here to retry</Trans>
|
||||||
</button>
|
</button>
|
||||||
@@ -83,16 +83,16 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
{hasNextPage && (
|
{hasNextPage && (
|
||||||
<li className="relative flex gap-x-4">
|
<li className="relative flex gap-x-4">
|
||||||
<div className="absolute -bottom-6 left-0 top-0 flex w-6 justify-center">
|
<div className="absolute -bottom-6 left-0 top-0 flex w-6 justify-center">
|
||||||
<div className="bg-border w-px" />
|
<div className="w-px bg-border" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-widget relative flex h-6 w-6 flex-none items-center justify-center">
|
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget">
|
||||||
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
|
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => fetchNextPage()}
|
onClick={async () => fetchNextPage()}
|
||||||
className="text-foreground/70 hover:text-muted-foreground text-xs"
|
className="text-xs text-foreground/70 hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
{isFetchingNextPage ? _(msg`Loading...`) : _(msg`Load older activity`)}
|
{isFetchingNextPage ? _(msg`Loading...`) : _(msg`Load older activity`)}
|
||||||
</button>
|
</button>
|
||||||
@@ -101,7 +101,7 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
|
|
||||||
{documentAuditLogs.length === 0 && (
|
{documentAuditLogs.length === 0 && (
|
||||||
<div className="flex items-center justify-center py-4">
|
<div className="flex items-center justify-center py-4">
|
||||||
<p className="text-muted-foreground/70 text-sm">
|
<p className="text-sm text-muted-foreground/70">
|
||||||
<Trans>No recent activity</Trans>
|
<Trans>No recent activity</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,44 +115,44 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
'absolute left-0 top-0 flex w-6 justify-center',
|
'absolute left-0 top-0 flex w-6 justify-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="bg-border w-px" />
|
<div className="w-px bg-border" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-widget text-foreground/40 relative flex h-6 w-6 flex-none items-center justify-center">
|
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget text-foreground/40">
|
||||||
{match(auditLog.type)
|
{match(auditLog.type)
|
||||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, () => (
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, () => (
|
||||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
||||||
<CheckCheckIcon className="h-3 w-3" aria-hidden="true" />
|
<CheckCheckIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => (
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => (
|
||||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
||||||
<CheckIcon className="h-3 w-3" aria-hidden="true" />
|
<CheckIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => (
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => (
|
||||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
||||||
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
|
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
|
||||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
||||||
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
|
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5"
|
className="flex-auto truncate py-0.5 text-xs leading-5 text-muted-foreground dark:text-muted-foreground/70"
|
||||||
title={formatDocumentAuditLogAction(_, auditLog, userId).description}
|
title={formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
||||||
>
|
>
|
||||||
{formatDocumentAuditLogAction(_, auditLog, userId).description}
|
{formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5">
|
<time className="flex-none py-0.5 text-xs leading-5 text-muted-foreground dark:text-muted-foreground/70">
|
||||||
{DateTime.fromJSDate(auditLog.createdAt).toRelative({ style: 'short' })}
|
{DateTime.fromJSDate(auditLog.createdAt).toRelative({ style: 'short' })}
|
||||||
</time>
|
</time>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||||
|
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
||||||
import {
|
import {
|
||||||
@@ -31,9 +32,13 @@ import { useCurrentTeam } from '~/providers/team';
|
|||||||
|
|
||||||
export type DocumentUploadButtonLegacyProps = {
|
export type DocumentUploadButtonLegacyProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
type: EnvelopeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => {
|
export const DocumentUploadButtonLegacy = ({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
}: DocumentUploadButtonLegacyProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
@@ -54,8 +59,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||||
|
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||||
|
|
||||||
const disabledMessage = useMemo(() => {
|
const disabledMessage = useMemo(() => {
|
||||||
|
if (!user.emailVerified) {
|
||||||
|
return msg`Verify your email to upload documents.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors for templates.
|
||||||
|
if (type === EnvelopeType.TEMPLATE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (organisation.subscription && remaining.documents === 0) {
|
if (organisation.subscription && remaining.documents === 0) {
|
||||||
return msg`Document upload disabled due to unpaid invoices`;
|
return msg`Document upload disabled due to unpaid invoices`;
|
||||||
}
|
}
|
||||||
@@ -64,11 +79,8 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
return msg`You have reached your document limit.`;
|
return msg`You have reached your document limit.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.emailVerified) {
|
|
||||||
return msg`Verify your email to upload documents.`;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [remaining.documents, user.emailVerified, team]);
|
}, [remaining.documents, user.emailVerified, team, type]);
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
@@ -80,44 +92,62 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
meta: {
|
meta: {
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
},
|
},
|
||||||
} satisfies TCreateDocumentPayloadSchema;
|
} satisfies TCreateDocumentPayloadSchema | TCreateTemplatePayloadSchema;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('payload', JSON.stringify(payload));
|
formData.append('payload', JSON.stringify(payload));
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const { envelopeId: id } = await createDocument(formData);
|
// Handle legacy document creation.
|
||||||
|
if (type === EnvelopeType.DOCUMENT) {
|
||||||
|
const { envelopeId: id } = await createDocument(formData);
|
||||||
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
|
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document uploaded`),
|
title: _(msg`Document uploaded`),
|
||||||
description: _(msg`Your document has been uploaded successfully.`),
|
description: _(msg`Your document has been uploaded successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.capture('App: Document Uploaded', {
|
analytics.capture('App: Document Uploaded', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
documentId: id,
|
documentId: id,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle legacy template creation.
|
||||||
|
if (type === EnvelopeType.TEMPLATE) {
|
||||||
|
const { envelopeId: id } = await createTemplate(formData);
|
||||||
|
|
||||||
|
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Template document uploaded`),
|
||||||
|
description: _(
|
||||||
|
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
|
||||||
|
),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => msg`You have reached the limit of the number of files per envelope`,
|
() => msg`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => msg`An error occurred while uploading your document.`);
|
.otherwise(() => msg`An error occurred while uploading your document.`);
|
||||||
|
|
||||||
@@ -149,17 +179,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
<div>
|
<div>
|
||||||
<DocumentUploadButtonPrimitive
|
<DocumentUploadButtonPrimitive
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={remaining.documents === 0 || !user.emailVerified}
|
disabled={disabledMessage !== undefined}
|
||||||
disabledMessage={disabledMessage}
|
disabledMessage={disabledMessage}
|
||||||
onDrop={async (files) => onFileDrop(files[0])}
|
onDrop={async (files) => onFileDrop(files[0])}
|
||||||
onDropRejected={onFileDropRejected}
|
onDropRejected={onFileDropRejected}
|
||||||
type={EnvelopeType.DOCUMENT}
|
type={type}
|
||||||
internalVersion="1"
|
internalVersion="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
{team?.id === undefined &&
|
{team?.id === undefined &&
|
||||||
|
type === EnvelopeType.DOCUMENT &&
|
||||||
remaining.documents > 0 &&
|
remaining.documents > 0 &&
|
||||||
Number.isFinite(remaining.documents) && (
|
Number.isFinite(remaining.documents) && (
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
||||||
import { Link, useSearchParams } from 'react-router';
|
import { Link, useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual } from 'remeda';
|
import { isDeepEqual } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/al
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
||||||
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
||||||
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
||||||
@@ -81,6 +82,8 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const selectedField = useMemo(
|
const selectedField = useMemo(
|
||||||
() => structuredClone(editorFields.selectedField),
|
() => structuredClone(editorFields.selectedField),
|
||||||
@@ -135,6 +138,22 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDetectClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full">
|
<div className="relative flex h-full">
|
||||||
<div className="flex w-full flex-col overflow-y-auto">
|
<div className="flex w-full flex-col overflow-y-auto">
|
||||||
@@ -230,34 +249,36 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Button
|
||||||
<>
|
type="button"
|
||||||
<Button
|
variant="outline"
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
className="mt-4 w-full"
|
||||||
size="sm"
|
onClick={onDetectClick}
|
||||||
className="mt-4 w-full"
|
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||||
onClick={() => setIsAiFieldDialogOpen(true)}
|
title={
|
||||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
envelope.status !== DocumentStatus.DRAFT
|
||||||
title={
|
? _(msg`You can only detect fields in draft envelopes`)
|
||||||
envelope.status !== DocumentStatus.DRAFT
|
: undefined
|
||||||
? _(msg`You can only detect fields in draft envelopes`)
|
}
|
||||||
: undefined
|
>
|
||||||
}
|
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||||
>
|
<Trans>Detect with AI</Trans>
|
||||||
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
</Button>
|
||||||
<Trans>Detect with AI</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<AiFieldDetectionDialog
|
<AiFieldDetectionDialog
|
||||||
open={isAiFieldDialogOpen}
|
open={isAiFieldDialogOpen}
|
||||||
onOpenChange={setIsAiFieldDialogOpen}
|
onOpenChange={setIsAiFieldDialogOpen}
|
||||||
onComplete={onFieldDetectionComplete}
|
onComplete={onFieldDetectionComplete}
|
||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Field details section. */}
|
{/* Field details section. */}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
type SensorAPI,
|
type SensorAPI,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { plural } from '@lingui/core/macro';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual, prop, sortBy } from 'remeda';
|
import { isDeepEqual, prop, sortBy } from 'remeda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
@@ -96,11 +98,19 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
|
||||||
// AI recipient detection dialog state
|
// AI recipient detection dialog state
|
||||||
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const onAiDialogOpenChange = (open: boolean) => {
|
const onAiDialogOpenChange = (open: boolean) => {
|
||||||
|
if (open && !team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
setIsAiDialogOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsAiDialogOpen(open);
|
setIsAiDialogOpen(open);
|
||||||
|
|
||||||
if (!open && searchParams.get('ai') === 'true') {
|
if (!open && searchParams.get('ai') === 'true') {
|
||||||
@@ -117,6 +127,22 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDetectRecipientsClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
@@ -329,8 +355,14 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Recipients added`,
|
title: plural(detectedRecipients.length, {
|
||||||
description: t`${detectedRecipients.length} recipient(s) have been added from AI detection.`,
|
one: `Recipient added`,
|
||||||
|
other: `Recipients added`,
|
||||||
|
}),
|
||||||
|
description: plural(detectedRecipients.length, {
|
||||||
|
one: `# recipient have been added from AI detection.`,
|
||||||
|
other: `# recipients have been added from AI detection.`,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -625,25 +657,27 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
type="button"
|
||||||
type="button"
|
size="sm"
|
||||||
size="sm"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
onClick={onDetectRecipientsClick}
|
||||||
onClick={() => setIsAiDialogOpen(true)}
|
>
|
||||||
>
|
<SparklesIcon className="h-4 w-4" />
|
||||||
<SparklesIcon className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
{team.preferences.aiFeaturesEnabled ? (
|
||||||
<Trans>Detect recipients with AI</Trans>
|
<Trans>Detect recipients with AI</Trans>
|
||||||
</TooltipContent>
|
) : (
|
||||||
</Tooltip>
|
<Trans>Enable AI detection</Trans>
|
||||||
)}
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -1100,6 +1134,12 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
|
|
||||||
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
|
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
|
||||||
{/* Sidebar. */}
|
{/* Sidebar. */}
|
||||||
<div className="bg-accent/20 flex w-80 flex-col border-r">
|
<div className="flex w-80 flex-col border-r bg-accent/20">
|
||||||
<DialogHeader className="p-6 pb-4">
|
<DialogHeader className="p-6 pb-4">
|
||||||
<DialogTitle>Document Settings</DialogTitle>
|
<DialogTitle>Document Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -390,7 +390,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
Controls the language for the document, including the language
|
Controls the language for the document, including the language
|
||||||
to be used for email notifications, and the final certificate
|
to be used for email notifications, and the final certificate
|
||||||
@@ -441,7 +441,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
}))}
|
}))}
|
||||||
selectedValues={field.value}
|
selectedValues={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
className="bg-background w-full"
|
className="w-full bg-background"
|
||||||
emptySelectionPlaceholder="Select signature types"
|
emptySelectionPlaceholder="Select signature types"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -518,7 +518,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
Add an external ID to the document. This can be used to identify
|
Add an external ID to the document. This can be used to identify
|
||||||
the document in external systems.
|
the document in external systems.
|
||||||
@@ -548,7 +548,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
Add a URL to redirect the user to once the document is signed
|
Add a URL to redirect the user to once the document is signed
|
||||||
</Trans>
|
</Trans>
|
||||||
@@ -576,7 +576,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||||
<h2>
|
<h2>
|
||||||
<strong>
|
<strong>
|
||||||
<Trans>Document Distribution Method</Trans>
|
<Trans>Document Distribution Method</Trans>
|
||||||
@@ -687,8 +687,10 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Reply To Email</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Reply To Email{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -726,20 +728,21 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex flex-row items-center">
|
<FormLabel className="flex flex-row items-center">
|
||||||
<Trans>Message</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Message <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground p-4">
|
<TooltipContent className="p-4 text-muted-foreground">
|
||||||
<DocumentSendEmailMessageHelper />
|
<DocumentSendEmailMessageHelper />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea className="bg-background h-16 resize-none" {...field} />
|
<Textarea className="h-16 resize-none bg-background" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function EnvelopeEditor() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-flow-container-step"
|
layoutId="document-flow-container-step"
|
||||||
className="absolute inset-y-0 left-0 bg-documenso"
|
className="absolute inset-y-0 left-0 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
|
|
||||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||||
return (
|
return (
|
||||||
<fieldset className="embed--DocumentWidgetForm dark:bg-background border-border rounded-2xl sm:border sm:p-3">
|
<fieldset className="embed--DocumentWidgetForm rounded-2xl border-border sm:border sm:p-3 dark:bg-background">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
||||||
value={selectedAssistantRecipient?.id?.toString()}
|
value={selectedAssistantRecipient?.id?.toString()}
|
||||||
@@ -54,7 +54,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
.map((r) => (
|
.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={r.id}
|
key={r.id}
|
||||||
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
className="relative flex flex-col gap-4 rounded-lg border border-border bg-widget p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -69,15 +69,15 @@ export default function EnvelopeSignerForm() {
|
|||||||
{r.name}
|
{r.name}
|
||||||
|
|
||||||
{r.id === recipient.id && (
|
{r.id === recipient.id && (
|
||||||
<span className="text-muted-foreground ml-2">
|
<span className="ml-2 text-muted-foreground">
|
||||||
<Trans>(You)</Trans>
|
<Trans>(You)</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">{r.email}</p>
|
<p className="text-xs text-muted-foreground">{r.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-xs leading-[inherit]">
|
<div className="text-xs leading-[inherit] text-muted-foreground">
|
||||||
<Plural
|
<Plural
|
||||||
value={assistantFields.filter((field) => field.recipientId === r.id).length}
|
value={assistantFields.filter((field) => field.recipientId === r.id).length}
|
||||||
one="# field"
|
one="# field"
|
||||||
@@ -103,7 +103,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="full-name"
|
id="full-name"
|
||||||
className="bg-background mt-2"
|
className="mt-2 bg-background"
|
||||||
value={fullName}
|
value={fullName}
|
||||||
disabled={isNameLocked}
|
disabled={isNameLocked}
|
||||||
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
||||||
@@ -119,6 +119,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
|
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -28,36 +28,40 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
const { envelopeData, envelope, recipientFieldsRemaining, recipient } =
|
const { envelopeData, envelope, recipientFieldsRemaining, recipient } =
|
||||||
useRequiredEnvelopeSigningContext();
|
useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const isEmbedSigning = useEmbedSigningContext() !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="embed--DocumentWidgetHeader bg-background border-border max-w-screen flex flex-row justify-between border-b px-4 py-3 md:px-6">
|
<nav className="embed--DocumentWidgetHeader max-w-screen flex flex-row justify-between border-b border-border bg-background px-4 py-3 md:px-6">
|
||||||
{/* Left side - Logo and title */}
|
{/* Left side - Logo and title */}
|
||||||
<div className="flex min-w-0 flex-1 items-center space-x-2 md:w-auto md:flex-none">
|
<div className="flex min-w-0 flex-1 items-center space-x-2 md:w-auto md:flex-none">
|
||||||
<Link to="/" className="flex-shrink-0">
|
{!isEmbedSigning && (
|
||||||
{envelopeData.settings.brandingEnabled && envelopeData.settings.brandingLogo ? (
|
<Link to="/" className="flex-shrink-0">
|
||||||
<img
|
{envelopeData.settings.brandingEnabled && envelopeData.settings.brandingLogo ? (
|
||||||
src={`/api/branding/logo/team/${envelope.teamId}`}
|
<img
|
||||||
alt={`${envelope.team.name}'s Logo`}
|
src={`/api/branding/logo/team/${envelope.teamId}`}
|
||||||
className="h-6 w-auto"
|
alt={`${envelope.team.name}'s Logo`}
|
||||||
/>
|
className="h-6 w-auto"
|
||||||
) : (
|
/>
|
||||||
<>
|
) : (
|
||||||
<BrandingLogo className="hidden h-6 w-auto md:block" />
|
<>
|
||||||
<BrandingLogoIcon className="h-6 w-auto md:hidden" />
|
<BrandingLogo className="hidden h-6 w-auto md:block" />
|
||||||
</>
|
<BrandingLogoIcon className="h-6 w-auto md:hidden" />
|
||||||
)}
|
</>
|
||||||
</Link>
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
title={envelope.title}
|
title={envelope.title}
|
||||||
className="text-foreground min-w-0 truncate text-base font-semibold md:hidden"
|
className="min-w-0 truncate text-base font-semibold text-foreground md:hidden"
|
||||||
>
|
>
|
||||||
{envelope.title}
|
{envelope.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Separator orientation="vertical" className="hidden h-6 md:block" />
|
{!isEmbedSigning && <Separator orientation="vertical" className="hidden h-6 md:block" />}
|
||||||
|
|
||||||
<div className="hidden items-center space-x-2 md:flex">
|
<div className="hidden items-center space-x-2 md:flex">
|
||||||
<h1 className="text-foreground whitespace-nowrap text-sm font-medium">
|
<h1 className="whitespace-nowrap text-sm font-medium text-foreground">
|
||||||
{envelope.title}
|
{envelope.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
|
|
||||||
{/* Right side - Desktop content */}
|
{/* Right side - Desktop content */}
|
||||||
<div className="hidden items-center space-x-2 lg:flex">
|
<div className="hidden items-center space-x-2 lg:flex">
|
||||||
<p className="text-muted-foreground mr-2 flex-shrink-0 text-sm">
|
<p className="mr-2 flex-shrink-0 text-sm text-muted-foreground">
|
||||||
<Plural
|
<Plural
|
||||||
one="1 Field Remaining"
|
one="1 Field Remaining"
|
||||||
other="# Fields Remaining"
|
other="# Fields Remaining"
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
.with({ type: FieldType.SIGNATURE }, (field) => {
|
.with({ type: FieldType.SIGNATURE }, (field) => {
|
||||||
handleSignatureFieldClick({
|
handleSignatureFieldClick({
|
||||||
field,
|
field,
|
||||||
|
fullName,
|
||||||
signature,
|
signature,
|
||||||
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
|
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
|
||||||
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
|
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
|
||||||
|
|||||||
@@ -123,14 +123,14 @@ export const EnvelopeDropZoneWrapper = ({
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => t`You have reached the limit of the number of files per envelope`,
|
() => t`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => t`An error occurred during upload.`);
|
.otherwise(() => t`An error occurred during upload.`);
|
||||||
|
|
||||||
|
|||||||
@@ -126,14 +126,14 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => t`You have reached the limit of the number of files per envelope`,
|
() => t`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => t`An error occurred while uploading your document.`);
|
.otherwise(() => t`An error occurred while uploading your document.`);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
|||||||
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
||||||
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
||||||
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
|
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
|
||||||
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
|
|
||||||
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy';
|
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy';
|
||||||
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
@@ -70,7 +69,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
<div
|
<div
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 flex flex-1 items-center text-sm font-medium"
|
className="flex flex-1 items-center text-sm font-medium text-muted-foreground hover:text-muted-foreground/80"
|
||||||
data-testid="folder-grid-breadcrumbs"
|
data-testid="folder-grid-breadcrumbs"
|
||||||
>
|
>
|
||||||
<Link to={formatRootPath()} className="flex items-center">
|
<Link to={formatRootPath()} className="flex items-center">
|
||||||
@@ -100,10 +99,9 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
||||||
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
||||||
|
|
||||||
{type === FolderType.DOCUMENT ? (
|
{/* If you delete this, delete the component as well. */}
|
||||||
<DocumentUploadButtonLegacy /> // If you delete this, delete the component as well.
|
{organisation.organisationClaim.flags.allowLegacyEnvelopes && (
|
||||||
) : (
|
<DocumentUploadButtonLegacy type={type} />
|
||||||
<TemplateCreateDialog folderId={parentId ?? undefined} /> // If you delete this, delete the component as well.
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FolderCreateDialog type={type} />
|
<FolderCreateDialog type={type} />
|
||||||
@@ -113,7 +111,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
<div key={index} className="border-border bg-card h-full rounded-lg border px-4 py-5">
|
<div key={index} className="h-full rounded-lg border border-border bg-card px-4 py-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Skeleton className="h-8 w-8 rounded" />
|
<Skeleton className="h-8 w-8 rounded" />
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
@@ -194,7 +192,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
{foldersData.folders.length > 12 && (
|
{foldersData.folders.length > 12 && (
|
||||||
<div className="mt-2 flex items-center justify-center">
|
<div className="mt-2 flex items-center justify-center">
|
||||||
<Link
|
<Link
|
||||||
className="text-muted-foreground hover:text-foreground text-sm font-medium"
|
className="text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||||
to={formatViewAllFoldersPath()}
|
to={formatViewAllFoldersPath()}
|
||||||
>
|
>
|
||||||
View all folders
|
View all folders
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
|||||||
export default function DocumentEditSkeleton() {
|
export default function DocumentEditSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||||
<Link to="/" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
|
<Link to="/" className="flex grow-0 items-center text-documenso-700 hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Role, Subscription } from '@prisma/client';
|
import type { Role, Subscription } from '@prisma/client';
|
||||||
import { Edit, Loader } from 'lucide-react';
|
import { Edit, Loader } from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
@@ -82,7 +83,7 @@ export const AdminDashboardUsersTable = ({
|
|||||||
<Button className="w-24" asChild>
|
<Button className="w-24" asChild>
|
||||||
<Link to={`/admin/users/${row.original.id}`}>
|
<Link to={`/admin/users/${row.original.id}`}>
|
||||||
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
||||||
Edit
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
|||||||
|
|
||||||
export type DocumentLogsTableProps = {
|
export type DocumentLogsTableProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
userId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateFormat: DateTimeFormatOptions = {
|
const dateFormat: DateTimeFormatOptions = {
|
||||||
@@ -26,7 +27,7 @@ const dateFormat: DateTimeFormatOptions = {
|
|||||||
hourCycle: 'h12',
|
hourCycle: 'h12',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps) => {
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -93,7 +94,9 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
|||||||
{
|
{
|
||||||
header: _(msg`Action`),
|
header: _(msg`Action`),
|
||||||
accessorKey: 'type',
|
accessorKey: 'type',
|
||||||
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
|
cell: ({ row }) => (
|
||||||
|
<span>{formatDocumentAuditLogAction(i18n, row.original, userId).description}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`IP Address`),
|
header: _(msg`IP Address`),
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UA
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const parser = new UAParser();
|
const parser = new UAParser();
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{logs.map((log, index) => {
|
{logs.map((log, index) => {
|
||||||
parser.setUA(log.userAgent || '');
|
parser.setUA(log.userAgent || '');
|
||||||
const formattedAction = formatDocumentAuditLogAction(_, log);
|
const formattedAction = formatDocumentAuditLogAction(i18n, log);
|
||||||
const userAgentInfo = parser.getResult();
|
const userAgentInfo = parser.getResult();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,17 +95,17 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground text-sm font-medium uppercase tracking-wide print:text-[8pt]">
|
<div className="text-sm font-medium uppercase tracking-wide text-muted-foreground print:text-[8pt]">
|
||||||
{log.type.replace(/_/g, ' ')}
|
{log.type.replace(/_/g, ' ')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-foreground text-sm font-medium print:text-[8pt]">
|
<div className="text-sm font-medium text-foreground print:text-[8pt]">
|
||||||
{formattedAction.description}
|
{formattedAction.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground text-sm print:text-[8pt]">
|
<div className="text-sm text-muted-foreground print:text-[8pt]">
|
||||||
{DateTime.fromJSDate(log.createdAt)
|
{DateTime.fromJSDate(log.createdAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toLocaleString(dateFormat)}
|
.toLocaleString(dateFormat)}
|
||||||
@@ -117,27 +117,27 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
|||||||
{/* Details Section - Two column layout */}
|
{/* Details Section - Two column layout */}
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||||
{_(msg`User`)}
|
{_(msg`User`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-foreground mt-1 font-mono">{log.email || 'N/A'}</div>
|
<div className="mt-1 font-mono text-foreground">{log.email || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||||
{_(msg`IP Address`)}
|
{_(msg`IP Address`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-foreground mt-1 font-mono">{log.ipAddress || 'N/A'}</div>
|
<div className="mt-1 font-mono text-foreground">{log.ipAddress || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||||
{_(msg`User Agent`)}
|
{_(msg`User Agent`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-foreground mt-1">
|
<div className="mt-1 text-foreground">
|
||||||
{_(formatUserAgent(log.userAgent, userAgentInfo))}
|
{_(formatUserAgent(log.userAgent, userAgentInfo))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ export const OrganisationGroupsDataTable = () => {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>Manage</Link>
|
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>
|
||||||
|
<Trans>Manage</Trans>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<OrganisationGroupDeleteDialog
|
<OrganisationGroupDeleteDialog
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-4 text-sm">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
<div>
|
<div>
|
||||||
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
disabled={envelope.recipients.some(
|
disabled={envelope.recipients.some(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.signingStatus !== SigningStatus.SIGNED &&
|
recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
recipient.role !== RecipientRole.CC,
|
||||||
)}
|
)}
|
||||||
onClick={() => resealDocument({ id: envelope.id })}
|
onClick={() => resealDocument({ id: envelope.id })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
if (organisation.teams.length === 0) {
|
if (organisation.teams.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center px-4 py-16">
|
<div className="flex flex-col items-center justify-center px-4 py-16">
|
||||||
<div className="bg-muted mb-6 flex h-20 w-20 items-center justify-center rounded-full">
|
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-muted">
|
||||||
<UsersIcon className="text-muted-foreground h-10 w-10" />
|
<UsersIcon className="h-10 w-10 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="mb-2 text-xl font-semibold">
|
<h2 className="mb-2 text-xl font-semibold">
|
||||||
@@ -53,7 +53,7 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
organisation.currentOrganisationRole,
|
organisation.currentOrganisationRole,
|
||||||
) ? (
|
) ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-muted-foreground mb-8 max-w-md text-center text-sm">
|
<p className="mb-8 max-w-md text-center text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
Teams help you organise your work and collaborate with others. Create your first
|
Teams help you organise your work and collaborate with others. Create your first
|
||||||
team to get started.
|
team to get started.
|
||||||
@@ -73,21 +73,21 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
<h3 className="mb-2 font-medium">
|
<h3 className="mb-2 font-medium">
|
||||||
<Trans>What you can do with teams:</Trans>
|
<Trans>What you can do with teams:</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="text-muted-foreground space-y-2 text-sm">
|
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||||
<li className="flex flex-row items-center gap-2">
|
<li className="flex flex-row items-center gap-2">
|
||||||
<div className="bg-muted mt-0.5 flex h-5 w-5 items-center justify-center rounded-full font-bold">
|
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-muted font-bold">
|
||||||
<span className="text-xs">1</span>
|
<span className="text-xs">1</span>
|
||||||
</div>
|
</div>
|
||||||
<Trans>Organize your documents and templates</Trans>
|
<Trans>Organize your documents and templates</Trans>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex flex-row items-center gap-2">
|
<li className="flex flex-row items-center gap-2">
|
||||||
<div className="bg-muted mt-0.5 flex h-5 w-5 items-center justify-center rounded-full font-bold">
|
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-muted font-bold">
|
||||||
<span className="text-xs">2</span>
|
<span className="text-xs">2</span>
|
||||||
</div>
|
</div>
|
||||||
<Trans>Invite team members to collaborate</Trans>
|
<Trans>Invite team members to collaborate</Trans>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex flex-row items-center gap-2">
|
<li className="flex flex-row items-center gap-2">
|
||||||
<div className="bg-muted mt-0.5 flex h-5 w-5 items-center justify-center rounded-full font-bold">
|
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-muted font-bold">
|
||||||
<span className="text-xs">3</span>
|
<span className="text-xs">3</span>
|
||||||
</div>
|
</div>
|
||||||
<Trans>Manage permissions and access controls</Trans>
|
<Trans>Manage permissions and access controls</Trans>
|
||||||
@@ -96,7 +96,7 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground mb-8 max-w-md text-center text-sm">
|
<p className="mb-8 max-w-md text-center text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
You currently have no access to any teams within this organisation. Please contact
|
You currently have no access to any teams within this organisation. Please contact
|
||||||
your organisation to request access.
|
your organisation to request access.
|
||||||
@@ -114,20 +114,22 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
<Trans>{organisation.name} Teams</Trans>
|
<Trans>{organisation.name} Teams</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
<Trans>Select a team to view its dashboard</Trans>
|
<Trans>Select a team to view its dashboard</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to={`/o/${organisation.url}/settings`}>Manage Organisation</Link>
|
<Link to={`/o/${organisation.url}/settings`}>
|
||||||
|
<Trans>Manage Organisation</Trans>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{organisation.teams.map((team) => (
|
{organisation.teams.map((team) => (
|
||||||
<Link to={`/t/${team.url}`} key={team.id}>
|
<Link to={`/t/${team.url}`} key={team.id}>
|
||||||
<Card className="hover:bg-muted/50 border-border h-full border transition-all">
|
<Card className="h-full border border-border transition-all hover:bg-muted/50">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar className="h-10 w-10 border-2 border-solid">
|
<Avatar className="h-10 w-10 border-2 border-solid">
|
||||||
@@ -143,7 +145,7 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">{team.name}</h3>
|
<h3 className="font-medium">{team.name}</h3>
|
||||||
<div className="text-muted-foreground truncate text-xs">
|
<div className="truncate text-xs text-muted-foreground">
|
||||||
{formatTeamUrl(team.url)}
|
{formatTeamUrl(team.url)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,11 +154,11 @@ export default function OrganisationSettingsTeamsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex items-center gap-4">
|
<div className="mt-2 flex items-center gap-4">
|
||||||
<div className="text-muted-foreground flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<CalendarIcon className="h-3 w-3" />
|
<CalendarIcon className="h-3 w-3" />
|
||||||
{i18n.date(team.createdAt, { dateStyle: 'short' })}
|
{i18n.date(team.createdAt, { dateStyle: 'short' })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<UserIcon className="h-3 w-3" />
|
<UserIcon className="h-3 w-3" />
|
||||||
<span>{t(TEAM_MEMBER_ROLE_MAP[team.currentTeamRole])}</span>
|
<span>{t(TEAM_MEMBER_ROLE_MAP[team.currentTeamRole])}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,7 +180,9 @@ const TeamDropdownMenu = ({ team }: { team: TGetOrganisationSessionResponse[0]['
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
<MoreVerticalIcon className="h-4 w-4" />
|
<MoreVerticalIcon className="h-4 w-4" />
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">
|
||||||
|
<Trans>Open menu</Trans>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function OrganisationSettingsBrandingPage() {
|
|||||||
if (isLoadingOrganisation || !organisationWithSettings) {
|
if (isLoadingOrganisation || !organisationWithSettings) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center rounded-lg py-32">
|
<div className="flex items-center justify-center rounded-lg py-32">
|
||||||
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
|
<Loader className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,9 +87,9 @@ export default function OrganisationSettingsBrandingPage() {
|
|||||||
const settingsHeaderText = t`Branding Preferences`;
|
const settingsHeaderText = t`Branding Preferences`;
|
||||||
|
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||||
? t`Here you can set your general branding preferences`
|
? t`Here you can set your general branding preferences.`
|
||||||
: team
|
: team
|
||||||
? t`Here you can set branding preferences for your team`
|
? t`Here you can set branding preferences for your team.`
|
||||||
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
includeAuditLog,
|
||||||
signatureTypes,
|
signatureTypes,
|
||||||
|
delegateDocumentOwnership,
|
||||||
aiFeaturesEnabled,
|
aiFeaturesEnabled,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
|
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||||
aiFeaturesEnabled,
|
aiFeaturesEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -112,7 +114,7 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
|
|
||||||
const settingsHeaderText = t`Document Preferences`;
|
const settingsHeaderText = t`Document Preferences`;
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||||
? t`Here you can set your general document preferences`
|
? t`Here you can set your general document preferences.`
|
||||||
: t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
: t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function OrganisationSettingsGeneral() {
|
|||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={t`Email Preferences`}
|
title={t`Email Preferences`}
|
||||||
subtitle={t`You can manage your email preferences here`}
|
subtitle={t`You can manage your email preferences here.`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
|||||||
<DocumentRecipientLinkCopyDialog recipients={envelope.recipients} />
|
<DocumentRecipientLinkCopyDialog recipients={envelope.recipients} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link to={documentRootPath} className="flex items-center text-documenso-700 hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function DocumentEditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link to={documentRootPath} className="flex items-center text-documenso-700 hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -75,11 +75,12 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
},
|
},
|
||||||
recipients: envelope.recipients,
|
recipients: envelope.recipients,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
|
userId: user.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||||
const { document, recipients, documentRootPath } = loaderData;
|
const { document, recipients, documentRootPath, userId } = loaderData;
|
||||||
|
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
|||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link
|
<Link
|
||||||
to={`${documentRootPath}/${document.envelopeId}`}
|
to={`${documentRootPath}/${document.envelopeId}`}
|
||||||
className="flex items-center text-[#7AC455] hover:opacity-80"
|
className="flex items-center text-documenso-700 hover:opacity-80"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Document</Trans>
|
<Trans>Document</Trans>
|
||||||
@@ -171,15 +172,15 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
|||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
|
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
|
||||||
{documentInformation.map((info, i) => (
|
{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>
|
<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>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="text-foreground text-sm">
|
<div className="text-sm text-foreground">
|
||||||
<h3 className="font-semibold">Recipients</h3>
|
<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) => (
|
{recipients.map((recipient) => (
|
||||||
<li key={`recipient-${recipient.id}`}>
|
<li key={`recipient-${recipient.id}`}>
|
||||||
<span>{formatRecipientText(recipient)}</span>
|
<span>{formatRecipientText(recipient)}</span>
|
||||||
@@ -191,7 +192,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<DocumentLogsTable documentId={document.id} />
|
<DocumentLogsTable documentId={document.id} userId={userId} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default function TeamsSettingsPage() {
|
|||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
includeAuditLog,
|
||||||
signatureTypes,
|
signatureTypes,
|
||||||
|
delegateDocumentOwnership,
|
||||||
aiFeaturesEnabled,
|
aiFeaturesEnabled,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ export default function TeamsSettingsPage() {
|
|||||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
}),
|
}),
|
||||||
|
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function TeamEmailSettingsGeneral() {
|
|||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={t`Email Preferences`}
|
title={t`Email Preferences`}
|
||||||
subtitle={t`You can manage your email preferences here`}
|
subtitle={t`You can manage your email preferences here.`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link to={templateRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link to={templateRootPath} className="flex items-center text-documenso-700 hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Templates</Trans>
|
<Trans>Templates</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function TemplateEditPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
to={`${templateRootPath}/${template.envelopeId}`}
|
to={`${templateRootPath}/${template.envelopeId}`}
|
||||||
className="flex items-center text-[#7AC455] hover:opacity-80"
|
className="flex items-center text-documenso-700 hover:opacity-80"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Template</Trans>
|
<Trans>Template</Trans>
|
||||||
|
|||||||
@@ -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 { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||||
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
|
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 { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@@ -53,6 +54,8 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
throw redirect('/');
|
throw redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
|
||||||
|
|
||||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
|
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
|
||||||
|
|
||||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||||
@@ -81,6 +84,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
deletedAt: envelope.deletedAt,
|
deletedAt: envelope.deletedAt,
|
||||||
documentMeta: envelope.documentMeta,
|
documentMeta: envelope.documentMeta,
|
||||||
},
|
},
|
||||||
|
hidePoweredBy: organisationClaim.flags.hidePoweredBy,
|
||||||
documentLanguage,
|
documentLanguage,
|
||||||
messages,
|
messages,
|
||||||
};
|
};
|
||||||
@@ -95,7 +99,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||||
*/
|
*/
|
||||||
export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||||
const { auditLogs, document, documentLanguage, messages } = loaderData;
|
const { auditLogs, document, documentLanguage, hidePoweredBy, messages } = loaderData;
|
||||||
|
|
||||||
const { i18n, _ } = useLingui();
|
const { i18n, _ } = useLingui();
|
||||||
|
|
||||||
@@ -145,7 +149,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
|||||||
<span className="mt-1 block">
|
<span className="mt-1 block">
|
||||||
{DateTime.fromJSDate(document.createdAt)
|
{DateTime.fromJSDate(document.createdAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -155,7 +159,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
|||||||
<span className="mt-1 block">
|
<span className="mt-1 block">
|
||||||
{DateTime.fromJSDate(document.updatedAt)
|
{DateTime.fromJSDate(document.updatedAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -188,11 +192,13 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
|||||||
<InternalAuditLogTable logs={auditLogs} />
|
<InternalAuditLogTable logs={auditLogs} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-8 flex-row-reverse">
|
{!hidePoweredBy && (
|
||||||
<div className="flex items-end justify-end gap-x-4">
|
<div className="my-8 flex-row-reverse">
|
||||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
<div className="flex items-end justify-end gap-x-4">
|
||||||
|
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
(log) =>
|
(log) =>
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
|
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
|
||||||
),
|
),
|
||||||
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT]: auditLogs[
|
||||||
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT
|
||||||
|
].filter((log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT),
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
|
||||||
].filter(
|
].filter(
|
||||||
@@ -245,11 +248,11 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
||||||
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
||||||
<div className="break-all">{recipient.email}</div>
|
<div className="break-all">{recipient.email}</div>
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
||||||
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -273,13 +276,13 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{signature.signature?.typedSignature && (
|
{signature.signature?.typedSignature && (
|
||||||
<p className="font-signature text-center text-sm">
|
<p className="text-center font-signature text-sm">
|
||||||
{signature.signature?.typedSignature}
|
{signature.signature?.typedSignature}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
||||||
<span className="block font-mono uppercase">
|
<span className="block font-mono uppercase">
|
||||||
{signature.secondaryId}
|
{signature.secondaryId}
|
||||||
@@ -290,14 +293,14 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
<p className="text-muted-foreground">N/A</p>
|
<p className="text-muted-foreground">N/A</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm print:text-xs">
|
<p className="mt-1 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||||
@@ -307,18 +310,22 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
|
|
||||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.EMAIL_SENT[0]
|
{logs.EMAIL_SENT[0]
|
||||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
: _(msg`Unknown`)}
|
: logs.DOCUMENT_SENT[0]
|
||||||
|
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||||
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
|
: _(msg`Unknown`)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_OPENED[0]
|
{logs.DOCUMENT_OPENED[0]
|
||||||
@@ -330,7 +337,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||||
@@ -341,7 +348,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||||
@@ -355,7 +362,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{recipient.signingStatus === SigningStatus.REJECTED
|
{recipient.signingStatus === SigningStatus.REJECTED
|
||||||
|
|||||||
@@ -355,24 +355,25 @@ const SigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV1Loade
|
|||||||
</Trans>
|
</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
|
||||||
<Trans>This document has been cancelled by the owner.</Trans>
|
<Trans>This document has been cancelled by the owner.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link to="/" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
<Link to="/" className="mt-36 text-documenso-700 hover:text-documenso-600">
|
||||||
<Trans>Go Back Home</Trans>
|
<Trans>Go Back Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground/60 mt-36 text-sm">
|
<p className="mt-36 text-sm text-muted-foreground/60">
|
||||||
<Trans>
|
<Trans>
|
||||||
Want to send slick signing links like this one?{' '}
|
Want to send slick signing links like this one?{' '}
|
||||||
<Link
|
<Link
|
||||||
to="https://documenso.com"
|
to="https://documenso.com"
|
||||||
className="text-documenso-700 hover:text-documenso-600"
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
>
|
>
|
||||||
Check out Documenso.
|
Check out Documenso
|
||||||
</Link>
|
</Link>
|
||||||
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -454,24 +455,25 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
|||||||
</Trans>
|
</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="mt-2.5 max-w-[60ch] text-center text-sm font-medium text-muted-foreground/60 md:text-base">
|
||||||
<Trans>This document has been cancelled by the owner.</Trans>
|
<Trans>This document has been cancelled by the owner.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link to="/" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
<Link to="/" className="mt-36 text-documenso-700 hover:text-documenso-600">
|
||||||
<Trans>Go Back Home</Trans>
|
<Trans>Go Back Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground/60 mt-36 text-sm">
|
<p className="mt-36 text-sm text-muted-foreground/60">
|
||||||
<Trans>
|
<Trans>
|
||||||
Want to send slick signing links like this one?{' '}
|
Want to send slick signing links like this one?{' '}
|
||||||
<Link
|
<Link
|
||||||
to="https://documenso.com"
|
to="https://documenso.com"
|
||||||
className="text-documenso-700 hover:text-documenso-600"
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
>
|
>
|
||||||
Check out Documenso.
|
Check out Documenso
|
||||||
</Link>
|
</Link>
|
||||||
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -91,31 +91,33 @@ export default function RejectedSigningPage({ loaderData }: Route.ComponentProps
|
|||||||
|
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<XCircle className="text-destructive h-10 w-10" />
|
<XCircle className="h-10 w-10 text-destructive" />
|
||||||
|
|
||||||
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
<Trans>Document Rejected</Trans>
|
<Trans>Document Rejected</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-destructive mt-4 flex items-center text-center text-sm">
|
<div className="mt-4 flex items-center text-center text-sm text-destructive">
|
||||||
<Trans>You have rejected this document</Trans>
|
<Trans>You have rejected this document</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
|
<p className="mt-6 max-w-[60ch] text-center text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
The document owner has been notified of your decision. They may contact you with further
|
The document owner has been notified of your decision. They may contact you with further
|
||||||
instructions if necessary.
|
instructions if necessary.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
|
<p className="mt-2 max-w-[60ch] text-center text-sm text-muted-foreground">
|
||||||
<Trans>No further action is required from you at this time.</Trans>
|
<Trans>No further action is required from you at this time.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<Button className="mt-6" asChild>
|
<Button className="mt-6" asChild>
|
||||||
<Link to={`/`}>Return Home</Link>
|
<Link to={`/`}>
|
||||||
|
<Trans>Return Home</Trans>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ export default function WaitingForTurnToSignPage({ loaderData }: Route.Component
|
|||||||
<Trans>Waiting for Your Turn</Trans>
|
<Trans>Waiting for Your Turn</Trans>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
It's currently not your turn to sign. You will receive an email with instructions once
|
It's currently not your turn to sign. You will receive an email with instructions once
|
||||||
it's your turn to sign the document.
|
it's your turn to sign the document.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4 text-sm">
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
<Trans>Please check your email for updates.</Trans>
|
<Trans>Please check your email for updates.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -98,7 +98,9 @@ export default function WaitingForTurnToSignPage({ loaderData }: Route.Component
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="link" asChild>
|
<Button variant="link" asChild>
|
||||||
<Link to="/">Return Home</Link>
|
<Link to="/">
|
||||||
|
<Trans>Return Home</Trans>
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import satori from 'satori';
|
import satori from 'satori';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
||||||
|
import { svgToPng } from '@documenso/lib/utils/images/svg-to-png';
|
||||||
|
|
||||||
import type { Route } from './+types/share.$slug.opengraph';
|
import type { Route } from './+types/share.$slug.opengraph';
|
||||||
|
|
||||||
@@ -181,8 +181,7 @@ export const loader = async ({ params }: Route.LoaderArgs) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert SVG to PNG using sharp
|
const pngBuffer = await svgToPng(svg.toString());
|
||||||
const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
|
||||||
|
|
||||||
return new Response(pngBuffer, {
|
return new Response(pngBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||