Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| bac2bf11f4 | |||
| d93b2a70a7 | |||
| 5da915da38 | |||
| dcaecf1fc5 | |||
| f70b76d8b8 | |||
| 93137c6396 | |||
| d058b7c705 | |||
| b51f562224 | |||
| f80aa4bf72 |
@@ -1,14 +1,19 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'feat/rr7']
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e_tests:
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: warp-ubuntu-2204-x64-16x
|
||||
runs-on: warp-ubuntu-2204-x64-8x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -28,9 +33,6 @@ jobs:
|
||||
- name: Seed the database
|
||||
run: npm run prisma:seed
|
||||
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
|
||||
- name: Install playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
@@ -45,7 +47,7 @@ jobs:
|
||||
with:
|
||||
name: test-results
|
||||
path: 'packages/app-tests/**/test-results/*'
|
||||
retention-days: 30
|
||||
retention-days: 7
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
@@ -65,6 +65,47 @@ jobs:
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
|
||||
- name: Build the chromium docker image
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile.chromium \
|
||||
--progress=plain \
|
||||
--build-arg TAG="$GIT_SHA" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:latest-chromium" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:latest-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium" \
|
||||
-t "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
.
|
||||
|
||||
- name: Push the chromium docker image to DockerHub
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:latest-chromium"
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium"
|
||||
docker push "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium" \
|
||||
|
||||
- name: Push the chromium docker image to GitHub Container Registry
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:latest-chromium"
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$GIT_SHA-chromium"
|
||||
docker push "ghcr.io/documenso/documenso-$BUILD_PLATFORM:$APP_VERSION-chromium"
|
||||
|
||||
create_and_publish_manifest:
|
||||
name: Create and publish manifest
|
||||
runs-on: ubuntu-latest
|
||||
@@ -125,6 +166,43 @@ jobs:
|
||||
docker manifest push documenso/documenso:$GIT_SHA
|
||||
docker manifest push documenso/documenso:$APP_VERSION
|
||||
|
||||
- name: Create and push DockerHub chromium manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:latest-chromium \
|
||||
--amend documenso/documenso-amd64:latest-chromium \
|
||||
--amend documenso/documenso-arm64:latest-chromium
|
||||
|
||||
docker manifest push documenso/documenso:latest-chromium
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:rc-chromium \
|
||||
--amend documenso/documenso-amd64:rc-chromium \
|
||||
--amend documenso/documenso-arm64:rc-chromium
|
||||
|
||||
docker manifest push documenso/documenso:rc-chromium
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$GIT_SHA-chromium \
|
||||
--amend documenso/documenso-amd64:$GIT_SHA-chromium \
|
||||
--amend documenso/documenso-arm64:$GIT_SHA-chromium
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$APP_VERSION-chromium \
|
||||
--amend documenso/documenso-amd64:$APP_VERSION-chromium \
|
||||
--amend documenso/documenso-arm64:$APP_VERSION-chromium
|
||||
|
||||
docker manifest push documenso/documenso:$GIT_SHA-chromium
|
||||
docker manifest push documenso/documenso:$APP_VERSION-chromium
|
||||
|
||||
- name: Create and push Github Container Registry manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
@@ -161,3 +239,40 @@ jobs:
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA
|
||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION
|
||||
|
||||
- name: Create and push Github Container Registry chromium manifest
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:latest-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:latest-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:latest-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:latest-chromium
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:rc-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:rc-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:rc-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:rc-chromium
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$GIT_SHA-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$GIT_SHA-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$GIT_SHA-chromium
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$APP_VERSION-chromium \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$APP_VERSION-chromium \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$APP_VERSION-chromium
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA-chromium
|
||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION-chromium
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
environment: Translations
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -26,12 +27,54 @@ jobs:
|
||||
- name: Extract translations
|
||||
run: npm run translate:extract
|
||||
|
||||
- name: Check and commit any files created
|
||||
- name: Commit changes and push to reserved branch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH="chore/extract-translations"
|
||||
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@documenso.com'
|
||||
|
||||
git fetch origin
|
||||
|
||||
# Create branch locally (always reset to main)
|
||||
git checkout -B "$BRANCH" origin/main
|
||||
|
||||
# Stage translation output
|
||||
git add packages/lib/translations
|
||||
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
|
||||
|
||||
# If no changes, exit early
|
||||
if git diff --staged --quiet; then
|
||||
echo "No translation changes found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit fresh snapshot
|
||||
git commit -m "chore: extract translations"
|
||||
|
||||
# Force push reserved branch
|
||||
git push origin "$BRANCH" --force
|
||||
|
||||
# Does a PR already exist?
|
||||
EXISTING_PR=$(gh pr list \
|
||||
--state open \
|
||||
--head "$BRANCH" \
|
||||
--json number \
|
||||
--jq '.[0].number // empty')
|
||||
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
echo "No existing PR — creating new one."
|
||||
gh pr create \
|
||||
--title "chore: extract translations" \
|
||||
--body "Automated translation extraction" \
|
||||
--base main \
|
||||
--head "$BRANCH"
|
||||
else
|
||||
echo "PR #$EXISTING_PR already exists — not creating a new one."
|
||||
fi
|
||||
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "^15.5.7",
|
||||
"next": "15.5.9",
|
||||
"next-plausible": "^3.12.5",
|
||||
"nextra": "^3",
|
||||
"nextra-theme-docs": "^3",
|
||||
@@ -29,4 +29,4 @@
|
||||
"pagefind": "^1.2.0",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,6 @@ You can access the following services:
|
||||
- Main application - http://localhost:3000
|
||||
- Incoming Mail Access - http://localhost:9000
|
||||
- Database Connection Details:
|
||||
- Port: 54320
|
||||
- Connection: Use your favourite database client to connect to the database.
|
||||
- Port: 54320
|
||||
- Connection: Use your favorite database client to connect to the database.
|
||||
- S3 Storage Dashboard - http://localhost:9001
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Rate Limits
|
||||
description: Learn about the rate limits for the Documenso Public API.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Rate Limits
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Telemetry
|
||||
description: Learn about the telemetry data that Documenso collects from self-hosted instances.
|
||||
---
|
||||
|
||||
# Telemetry
|
||||
|
||||
Documenso collects anonymous telemetry data from self-hosted instances to help us understand how the software is being used and make improvements to the product. This telemetry is enabled by default, but you can easily disable it if you prefer.
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Learn how to use webhooks to receive real-time notifications about
|
||||
|
||||
# Webhooks
|
||||
|
||||
Webhooks are HTTP callbacks triggered by specific events. When the user subscribes to a specific event, and that event occurs, the webhook makes an HTTP request to the URL provided by the user. The request can be a simple notification or carry a payload with more information about the event.
|
||||
Webhooks are HTTP callbacks triggered by specific events. When you subscribe to a specific event and that event occurs, the webhook makes an HTTP request to the URL you provide. The request can be a simple notification or carry a payload with more information about the event.
|
||||
|
||||
Some of the common use cases for webhooks include:
|
||||
|
||||
@@ -25,13 +25,13 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
|
||||
|
||||
## Create a webhook subscription
|
||||
|
||||
You can create a webhook subscription from the user settings page. Click on your avatar in the top right corner of the dashboard and select "**[User settings](https://app.documenso.com/settings)**" from the dropdown menu.
|
||||
You can create a webhook subscription from the team settings page. Click your avatar in the top right corner of the dashboard and select "Team settings" from the dropdown menu.
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

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

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

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

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

|
||||

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

|
||||

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

|
||||
|
||||
This will send the webhook event to the webhook URL again.
|
||||
|
||||
## Availability
|
||||
|
||||
Webhooks are available to individual users and teams.
|
||||
Webhooks are available to teams only.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Signature Levels
|
||||
description: Learn about the different signature levels for Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Signature Levels
|
||||
@@ -26,20 +31,20 @@ ensures the legal validity and enforceability of electronic signatures and recor
|
||||
|
||||
### Main Requirements
|
||||
|
||||
- [x] Intent to Sign: "Parties must demonstrate their intent to sign [..]"
|
||||
- [x] Consent: "The ESIGN Act requires that all parties involved in a transaction consent to the use of electronic signatures and records [..]"
|
||||
- [x] Consumer Disclosures: Before obtaining their consent, financial institutions must provide the consumer a clear and conspicuous statement informing the consumer [..]
|
||||
- [x] Record Retention: Electronic Records must be maintained for later access by signers.
|
||||
- [x] Security: The ESIGN Act does not mandate specific security measures, but it does require that parties take reasonable steps to ensure the security and integrity of electronic signatures and records. This may include implementing encryption, access controls, and authentication measures.
|
||||
- [x] **Intent to Sign**: "Parties must demonstrate their intent to sign [..]"
|
||||
- [x] **Consent**: "The ESIGN Act requires that all parties involved in a transaction consent to the use of electronic signatures and records [..]"
|
||||
- [x] **Consumer Disclosures**: Before obtaining their consent, financial institutions must provide the consumer a clear and conspicuous statement informing the consumer [..]
|
||||
- [x] **Record Retention**: Electronic Records must be maintained for later access by signers.
|
||||
- [x] **Security**: The ESIGN Act does not mandate specific security measures, but it does require that parties take reasonable steps to ensure the security and integrity of electronic signatures and records. This may include implementing encryption, access controls, and authentication measures.
|
||||
|
||||
## UETA (Uniform Electronic Transactions Act)
|
||||
|
||||
<Callout type="info" emoji="✅">
|
||||
Status: Compliant
|
||||
</Callout>
|
||||
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of electronic
|
||||
signatures and records in electronic transactions, ensuring they have the same validity and enforceability
|
||||
as paper documents and handwritten signatures.
|
||||
The Uniform Electronic Transactions Act is a law that provides a legal framework for the use of
|
||||
electronic signatures and records in electronic transactions, ensuring they have the same validity
|
||||
and enforceability as paper documents and handwritten signatures.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
@@ -50,9 +55,9 @@ _See [ESIGN](/users/compliance/signature-levels#-esign-electronic-signatures-in-
|
||||
<Callout type="info" emoji="✅">
|
||||
Status: Compliant for Level 1 - SES (Simple Electronic Signatures)
|
||||
</Callout>
|
||||
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that standardizes
|
||||
electronic identification and trust services for secure and seamless electronic transactions across European
|
||||
member states.
|
||||
eIDAS (Electronic Identification, Authentication and Trust Services) is an EU regulation that
|
||||
standardizes electronic identification and trust services for secure and seamless electronic
|
||||
transactions across European member states.
|
||||
|
||||
### Level 1 - SES (Simple Electronic Signatures)
|
||||
|
||||
@@ -69,8 +74,8 @@ eIDAS SES (Simple Electronic Signature) is a basic electronic signature with min
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/9) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique identification
|
||||
of the signer and data integrity.
|
||||
eIDAS AES (Advanced Electronic Signature) provides a higher level of security with unique
|
||||
identification of the signer and data integrity.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
@@ -85,8 +90,8 @@ of the signer and data integrity.
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/32) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a handwritten
|
||||
signature within the EU.
|
||||
eIDAS QES (Qualified Electronic Signature) is the highest security level, legally equivalent to a
|
||||
handwritten signature within the EU.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Standards and Regulations
|
||||
description: Learn about the different standards and regulations for Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
## 21 CFR Part 11
|
||||
|
||||
@@ -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.
|
||||
|
||||
{/*  */}
|
||||

|
||||
|
||||
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';
|
||||
|
||||
# Email Domains
|
||||
|
||||
@@ -7,28 +7,28 @@ import { Callout } from 'nextra/components';
|
||||
|
||||
# Fair Use Policy
|
||||
|
||||
### Why
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using.
|
||||
|
||||
We offer our plans without any limits on volume because we want our users and customers to make the most of their accounts. Estimating volume is incredibly hard, especially for shorter intervals like a quarter. We are not interested in selling volume packages our customers end up not using. This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
This is why the individual plan and the team plan do not include a limit on signing or API volume. If you are a customer of these [plans](https://documen.so/pricing), we ask you to abide by this fair use policy:
|
||||
|
||||
### Spirit of the Plan
|
||||
|
||||
> Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
Use the limitless accounts as much as you like (they are meant to offer a lot) while respecting the spirit and intended scope of the account.
|
||||
|
||||
<Callout type="info">
|
||||
What happens if I violate this policy? We will ask you to upgrade to a fitting plan or custom
|
||||
pricing. We won’t block your account without reaching out. [Message
|
||||
us](mailto:support@documenso.com) for questions. It's probably fine, though.
|
||||
pricing. We won’t block your account without reaching out. You can [message
|
||||
us](mailto:support@documenso.com) for questions.
|
||||
</Callout>
|
||||
|
||||
### DO
|
||||
|
||||
- Sign as many documents with the individual plan for your single business or organization you are part of
|
||||
- Use the API and Zapier to automate all your signing to sign as much as possible
|
||||
- Experiment with the plans and integrations, testing what you want to build: When in doubt, do it. Especially if you are just starting.
|
||||
- Sign as many documents as you need with the individual plan for your single business or organization you are part of
|
||||
- Use the API and automation tools to automate all your signing workflows
|
||||
- Experiment with the plans and integrations, testing what you want to build
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use the individual account's API to power a platform
|
||||
- Run a huge company, signing thousands of documents per day on a two-user team plan using the API
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win, and it's probably fine
|
||||
- Let this policy make you overthink. If you are a paying customer, we want you to win
|
||||
|
||||
@@ -10,7 +10,12 @@ import { Callout, Steps } from 'nextra/components';
|
||||
<Steps>
|
||||
### Pick a Plan
|
||||
|
||||
The first step to start using Documenso is to pick a plan and create an account. At the moment of writing this guide, we have 3 plans available: Free, Individual, Teams and Platform.
|
||||
The first step to start using Documenso is to pick a plan and create an account. At the moment of writing this guide, we have 4 plans available:
|
||||
|
||||
- Free
|
||||
- Individual
|
||||
- Teams
|
||||
- Platform
|
||||
|
||||
Explore each plan's features and choose the one that best suits your needs. The [pricing page](https://documen.so/pricing) has more information about the plans.
|
||||
|
||||
@@ -24,7 +29,7 @@ To create a free account, navigate to the [registration page](https://documen.so
|
||||
|
||||
### Optional: Claim a Premium Username
|
||||
|
||||
You can claim a premium username by upgrading to a paid plan. After upgrading to a paid plan, you can update your [public profile](https://app.documenso.com/settings/public-profile).
|
||||
You can claim a premium username by upgrading to a paid plan. After upgrading to a paid plan, you can update your [public profile](/users/profile).
|
||||
|
||||
### Optional: Create a Team
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Community Edition
|
||||
description: Learn about the Community Edition of Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Community Edition
|
||||
@@ -32,10 +37,10 @@ Documenso and the Community Edition are licensed under [AGPL3](https://github.co
|
||||
|
||||
### Conditions
|
||||
|
||||
ℹ️ License and copyright notice
|
||||
ℹ️ State changes
|
||||
ℹ️ Disclose source
|
||||
ℹ️ Network use is distribution
|
||||
- License and copyright notice
|
||||
- State changes
|
||||
- Disclose source
|
||||
- Network use is distribution
|
||||
|
||||
<Callout type="warning">
|
||||
It's important to remember that you must keep the AGPL3 license for your modified or non-modified
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Enterprise Edition
|
||||
description: Learn about the Enterprise Edition of Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Enterprise Edition
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Licenses
|
||||
description: Learn about the different licenses for self-hosting Documenso.
|
||||
---
|
||||
|
||||
# Self-Hosting Licenses
|
||||
|
||||
Documenso comes in two versions for self-hosting:
|
||||
|
||||
@@ -15,7 +15,7 @@ Documenso allows you to create a public profile to share your templates for anyo
|
||||
|
||||
### Navigate to Your Profile Settings
|
||||
|
||||
Click on your profile picture in the top right corner and select "Settings" or "Team Settings". Then, navigate to the "Public Profile" tab to configure your profile.
|
||||
Click on your profile picture in the top right corner and select "Team Settings". Then, navigate to the "Public Profile" tab to configure your profile.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -9,30 +9,30 @@ description: Learn what types of support we offer.
|
||||
|
||||
If you are a developer or free user, you can reach out to the community or raise an issue:
|
||||
|
||||
### [Create Github Issues](https://github.com/documenso/documenso/issues)
|
||||
**[Create Github Issues](https://github.com/documenso/documenso/issues)**
|
||||
|
||||
The community and the core team address GitHub issues. Be sure to check if a similar issue already exists. Please note that while we want to address everything immediately, we must prioritize.
|
||||
|
||||
### [Join our Discord](https://documen.so/discord)
|
||||
**[Join our Discord](https://documen.so/discord)**
|
||||
|
||||
You can ask for help in the [community help channel](https://discord.com/channels/1132216843537485854/1133419426524430376).
|
||||
|
||||
## Paid Account Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
**Email: support@documenso.com**
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Private Discord channel
|
||||
**Private Discord channel**
|
||||
|
||||
If you prefer Discord, we can invite you to a private channel. Message support to make this happen.
|
||||
|
||||
## Enterprise Support
|
||||
|
||||
### Email: support@documenso.com
|
||||
**Email: support@documenso.com**
|
||||
|
||||
If you are paying customers facing issues, email our customer support, especially in urgent cases.
|
||||
|
||||
### Slack
|
||||
**Slack**
|
||||
|
||||
If your team is on Slack, we can create a private workspace to support you more closely.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Templates
|
||||
description: Learn how to create and use templates in Documenso.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Document Templates
|
||||
|
||||
|
After Width: | Height: | Size: 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": {
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.7.2",
|
||||
"next": "^15.5.7"
|
||||
"next": "15.5.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "18.3.27",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@ export const AdminOrganisationMemberUpdateDialog = ({
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
You are currently updating{' '}
|
||||
<span className="font-bold">{organisationMemberName}.</span>
|
||||
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||
.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</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 { msg } from '@lingui/core/macro';
|
||||
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 type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
||||
@@ -232,10 +232,19 @@ export const AiFieldDetectionDialog = ({
|
||||
|
||||
{progress && (
|
||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
||||
{progress.fieldsDetected} field(s) found
|
||||
</Trans>
|
||||
<Plural
|
||||
value={progress.fieldsDetected}
|
||||
one={
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} - # field found
|
||||
</Trans>
|
||||
}
|
||||
other={
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} - # fields found
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -279,7 +288,11 @@ export const AiFieldDetectionDialog = ({
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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 { 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 { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
@@ -190,10 +190,19 @@ export const AiRecipientDetectionDialog = ({
|
||||
|
||||
{progress && (
|
||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
||||
{progress.recipientsDetected} recipient(s) found
|
||||
</Trans>
|
||||
<Plural
|
||||
value={progress.recipientsDetected}
|
||||
one={
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} - # recipient found
|
||||
</Trans>
|
||||
}
|
||||
other={
|
||||
<Trans>
|
||||
Page {progress.pagesProcessed} of {progress.totalPages} - # recipients found
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -237,9 +246,11 @@ export const AiRecipientDetectionDialog = ({
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
We found {detectedRecipients.length} recipient(s) in your document.
|
||||
</Trans>
|
||||
<Plural
|
||||
value={detectedRecipients.length}
|
||||
one="We found # recipient in your document."
|
||||
other="We found # recipients in your document."
|
||||
/>
|
||||
</p>
|
||||
|
||||
<ul className="mt-4 divide-y rounded-lg border">
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as z from 'zod';
|
||||
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -129,18 +130,43 @@ export const EnvelopeDistributeDialog = ({
|
||||
|
||||
const distributionMethod = watch('meta.distributionMethod');
|
||||
|
||||
const recipientsWithIndex = useMemo(
|
||||
() =>
|
||||
envelope.recipients.map((recipient, index) => ({
|
||||
...recipient,
|
||||
index,
|
||||
})),
|
||||
[envelope.recipients],
|
||||
);
|
||||
|
||||
const recipientsMissingSignatureFields = useMemo(
|
||||
() =>
|
||||
envelope.recipients.filter(
|
||||
recipientsWithIndex.filter(
|
||||
(recipient) =>
|
||||
recipient.role === RecipientRole.SIGNER &&
|
||||
!envelope.fields.some(
|
||||
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
||||
),
|
||||
),
|
||||
[envelope.recipients, envelope.fields],
|
||||
[recipientsWithIndex, envelope.fields],
|
||||
);
|
||||
|
||||
/**
|
||||
* List of recipients who must have an email due to having auth enabled.
|
||||
*/
|
||||
const recipientsMissingRequiredEmail = useMemo(() => {
|
||||
return recipientsWithIndex.filter((recipient) => {
|
||||
const auth = extractDocumentAuthMethods({
|
||||
documentAuth: envelope.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email
|
||||
);
|
||||
});
|
||||
}, [recipientsWithIndex, envelope.authOptions]);
|
||||
|
||||
const invalidEnvelopeCode = useMemo(() => {
|
||||
if (recipientsMissingSignatureFields.length > 0) {
|
||||
return 'MISSING_SIGNATURES';
|
||||
@@ -150,8 +176,12 @@ export const EnvelopeDistributeDialog = ({
|
||||
return 'MISSING_RECIPIENTS';
|
||||
}
|
||||
|
||||
if (recipientsMissingRequiredEmail.length > 0) {
|
||||
return 'MISSING_REQUIRED_EMAIL';
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [envelope.recipients, envelope.fields, recipientsMissingSignatureFields]);
|
||||
}, [envelope.recipients, recipientsMissingRequiredEmail, recipientsMissingSignatureFields]);
|
||||
|
||||
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
||||
try {
|
||||
@@ -323,8 +353,10 @@ export const EnvelopeDistributeDialog = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Email</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Trans>
|
||||
Reply To Email{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -342,8 +374,10 @@ export const EnvelopeDistributeDialog = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Subject</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Trans>
|
||||
Subject{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -360,8 +394,10 @@ export const EnvelopeDistributeDialog = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Trans>
|
||||
Message{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
@@ -444,7 +480,22 @@ export const EnvelopeDistributeDialog = ({
|
||||
|
||||
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||
{recipientsMissingSignatureFields.map((recipient) => (
|
||||
<li key={recipient.id}>{recipient.email}</li>
|
||||
<li key={recipient.id}>
|
||||
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
))
|
||||
.with('MISSING_REQUIRED_EMAIL', () => (
|
||||
<AlertDescription>
|
||||
<Trans>The following recipients require an email address:</Trans>
|
||||
|
||||
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||
{recipientsMissingRequiredEmail.map((recipient) => (
|
||||
<li key={recipient.id}>
|
||||
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
|
||||
@@ -117,7 +117,7 @@ export const EnvelopeItemDeleteDialog = ({
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<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>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
|
||||
<Trans>
|
||||
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"
|
||||
domains button
|
||||
domains button.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -148,8 +148,8 @@ export const OrganisationMemberUpdateDialog = ({
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
You are currently updating{' '}
|
||||
<span className="font-bold">{organisationMemberName}.</span>
|
||||
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||
.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
const ZSignFieldEmailFormSchema = z.object({
|
||||
email: z.string().min(1, { message: msg`Email is required`.id }),
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.min(1, { message: msg`Email is required`.id }),
|
||||
});
|
||||
|
||||
type TSignFieldEmailFormSchema = z.infer<typeof ZSignFieldEmailFormSchema>;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { DocumentSigningDisclosure } from '../general/document-signing/document-
|
||||
|
||||
export type SignFieldSignatureDialogProps = {
|
||||
initialSignature?: string;
|
||||
fullName?: string;
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
@@ -28,6 +29,7 @@ export const SignFieldSignatureDialog = createCallable<
|
||||
>(
|
||||
({
|
||||
call,
|
||||
fullName,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
@@ -46,6 +48,7 @@ export const SignFieldSignatureDialog = createCallable<
|
||||
</DialogHeader>
|
||||
|
||||
<SignaturePad
|
||||
fullName={fullName}
|
||||
value={localSignature ?? ''}
|
||||
onChange={({ value }) => setLocalSignature(value)}
|
||||
typedSignatureEnabled={typedSignatureEnabled}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -30,6 +31,13 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@@ -53,26 +61,34 @@ export const TeamDeleteDialog = ({
|
||||
const { toast } = useToast();
|
||||
const { refreshSession } = useSession();
|
||||
|
||||
const currentOrganisation = useCurrentOrganisation();
|
||||
|
||||
const deleteMessage = _(msg`delete ${teamName}`);
|
||||
|
||||
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
|
||||
|
||||
const ZDeleteTeamFormSchema = z.object({
|
||||
teamName: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||
}),
|
||||
transferTeamId: z.string().optional(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ZDeleteTeamFormSchema),
|
||||
defaultValues: {
|
||||
teamName: '',
|
||||
transferTeamId: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
|
||||
|
||||
const onFormSubmit = async () => {
|
||||
const onFormSubmit = async (data: z.infer<typeof ZDeleteTeamFormSchema>) => {
|
||||
try {
|
||||
await deleteTeam({ teamId });
|
||||
const transferTeamId = data.transferTeamId ? parseInt(data.transferTeamId, 10) : undefined;
|
||||
|
||||
await deleteTeam({ teamId, transferTeamId: transferTeamId || undefined });
|
||||
|
||||
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>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const TeamMemberUpdateDialog = ({
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
You are currently updating <span className="font-bold">{memberName}.</span>
|
||||
You are currently updating <span className="font-bold">{memberName}</span>.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -137,12 +137,12 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
||||
<div className="bg-muted/70 rounded-lg border p-4">
|
||||
<div className="rounded-lg border bg-muted/70 p-4">
|
||||
<h3 className="text-sm font-medium">
|
||||
<Trans>CSV Structure</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
For each recipient, provide their email (required) and name (optional) in separate
|
||||
columns. Download the template CSV below for the correct format.
|
||||
@@ -153,7 +153,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Current recipients:</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="text-muted-foreground mt-2 list-inside list-disc text-sm">
|
||||
<ul className="mt-2 list-inside list-disc text-sm text-muted-foreground">
|
||||
{recipients.map((recipient, index) => (
|
||||
<li key={index}>
|
||||
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
||||
@@ -167,7 +167,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Download Template CSV</Trans>
|
||||
</Button>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>Pre-formatted CSV template with example data.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -200,14 +200,14 @@ export const TemplateBulkSendDialog = ({
|
||||
) : (
|
||||
<div className="flex h-10 items-center rounded-md border px-3">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<FileIcon className="text-muted-foreground h-4 w-4" />
|
||||
<FileIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-sm">{value.name}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-destructive hover:text-destructive p-0 text-xs"
|
||||
className="p-0 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => onChange(null)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
@@ -220,9 +220,9 @@ export const TemplateBulkSendDialog = ({
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{error && <p className="text-destructive text-sm">{error.message}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>
|
||||
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
||||
template defaults.
|
||||
@@ -247,7 +247,7 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<label
|
||||
htmlFor="send-immediately"
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
>
|
||||
<Trans>Send documents to recipients immediately</Trans>
|
||||
</label>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -65,7 +66,7 @@ const ZAddRecipientsForNewDocumentSchema = z.object({
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string(),
|
||||
signingOrder: z.number().optional(),
|
||||
}),
|
||||
@@ -100,12 +101,29 @@ export function TemplateUseDialog({
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||
defaultValues: {
|
||||
const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery(
|
||||
{
|
||||
envelopeId,
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
enabled: open,
|
||||
},
|
||||
);
|
||||
|
||||
const envelopeItems = response?.data ?? [];
|
||||
|
||||
const generateDefaultFormValues = () => {
|
||||
return {
|
||||
distributeDocument: false,
|
||||
useCustomDocument: false,
|
||||
customDocumentData: [],
|
||||
customDocumentData: envelopeItems.map((item) => ({
|
||||
title: item.title,
|
||||
data: undefined,
|
||||
envelopeItemId: item.id,
|
||||
})),
|
||||
recipients: recipients
|
||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||
.map((recipient) => {
|
||||
@@ -124,7 +142,12 @@ export function TemplateUseDialog({
|
||||
signingOrder: recipient.signingOrder ?? undefined,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||
defaultValues: generateDefaultFormValues(),
|
||||
});
|
||||
|
||||
const { replace, fields: localCustomDocumentData } = useFieldArray({
|
||||
@@ -132,19 +155,6 @@ export function TemplateUseDialog({
|
||||
name: 'customDocumentData',
|
||||
});
|
||||
|
||||
const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery(
|
||||
{
|
||||
envelopeId,
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
},
|
||||
);
|
||||
|
||||
const envelopeItems = response?.data ?? [];
|
||||
|
||||
const { mutateAsync: createDocumentFromTemplate } =
|
||||
trpc.template.createDocumentFromTemplate.useMutation();
|
||||
|
||||
@@ -214,8 +224,8 @@ export function TemplateUseDialog({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset();
|
||||
if (open) {
|
||||
form.reset(generateDefaultFormValues());
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
@@ -322,7 +332,7 @@ export function TemplateUseDialog({
|
||||
<Input
|
||||
{...field}
|
||||
aria-label="Name"
|
||||
placeholder={recipients[index].name || _(msg`Name`)}
|
||||
placeholder={recipients[index].name || _(msg`Recipient ${index + 1}`)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -349,7 +359,7 @@ export function TemplateUseDialog({
|
||||
|
||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
htmlFor="distributeDocument"
|
||||
>
|
||||
<Trans>Send document</Trans>
|
||||
@@ -358,7 +368,7 @@ export function TemplateUseDialog({
|
||||
<InfoIcon className="mx-1 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
The document will be immediately sent to recipients if this
|
||||
@@ -378,7 +388,7 @@ export function TemplateUseDialog({
|
||||
|
||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
htmlFor="distributeDocument"
|
||||
>
|
||||
<Trans>Create as pending</Trans>
|
||||
@@ -386,7 +396,7 @@ export function TemplateUseDialog({
|
||||
<TooltipTrigger type="button">
|
||||
<InfoIcon className="mx-1 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
Create the document as pending and ready to sign.
|
||||
@@ -432,7 +442,7 @@ export function TemplateUseDialog({
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
htmlFor="useCustomDocument"
|
||||
>
|
||||
<Trans>Upload custom document</Trans>
|
||||
@@ -440,7 +450,7 @@ export function TemplateUseDialog({
|
||||
<TooltipTrigger type="button">
|
||||
<InfoIcon className="mx-1 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
Upload a custom document to use instead of the template's default
|
||||
@@ -470,19 +480,19 @@ export function TemplateUseDialog({
|
||||
<FormControl>
|
||||
<div
|
||||
key={item.id}
|
||||
className="border-border bg-card hover:bg-accent/10 flex items-center gap-4 rounded-lg border p-4 transition-colors"
|
||||
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/10"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<FileTextIcon className="text-primary h-5 w-5" />
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<FileTextIcon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="text-foreground truncate text-sm font-medium">
|
||||
<h4 className="truncate text-sm font-medium text-foreground">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{field.value ? (
|
||||
<div>
|
||||
<Trans>
|
||||
|
||||
@@ -219,9 +219,8 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
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>
|
||||
.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaLanguageSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
||||
|
||||
// Define the schema for configuration
|
||||
@@ -55,7 +56,7 @@ export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.exten
|
||||
nativeId: z.number().optional(),
|
||||
formId: z.string(),
|
||||
name: z.string(),
|
||||
email: z.union([z.string().length(0), z.string().email('Invalid email address')]),
|
||||
email: ZRecipientEmailSchema,
|
||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||
signingOrder: z.number().optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
|
||||
@@ -438,6 +438,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
className="mt-2"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
disableAnimation
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||
|
||||
@@ -455,6 +455,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
className="mt-2"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
disableAnimation
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||
|
||||
@@ -319,6 +319,7 @@ export const MultiSignDocumentSigningView = ({
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
disableAnimation
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={
|
||||
|
||||
@@ -58,6 +58,7 @@ export type TDocumentPreferencesFormSchema = {
|
||||
includeSigningCertificate: boolean | null;
|
||||
includeAuditLog: boolean | null;
|
||||
signatureTypes: DocumentSignatureType[];
|
||||
delegateDocumentOwnership: boolean | null;
|
||||
aiFeaturesEnabled: boolean | null;
|
||||
};
|
||||
|
||||
@@ -73,6 +74,7 @@ type SettingsSubset = Pick<
|
||||
| 'typedSignatureEnabled'
|
||||
| 'uploadSignatureEnabled'
|
||||
| 'drawSignatureEnabled'
|
||||
| 'delegateDocumentOwnership'
|
||||
| 'aiFeaturesEnabled'
|
||||
>;
|
||||
|
||||
@@ -109,6 +111,7 @@ export const DocumentPreferencesForm = ({
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
}),
|
||||
delegateDocumentOwnership: z.boolean().nullable(),
|
||||
aiFeaturesEnabled: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
@@ -125,6 +128,7 @@ export const DocumentPreferencesForm = ({
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
},
|
||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
||||
@@ -515,6 +519,52 @@ export const DocumentPreferencesForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="delegateDocumentOwnership"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Delegate Document Ownership</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Enable team API tokens to delegate document ownership to another team member.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAiFeaturesConfigured && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -110,7 +110,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
||||
<Label htmlFor="email" className="text-muted-foreground">
|
||||
<Trans>Email</Trans>
|
||||
</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>
|
||||
|
||||
<FormField
|
||||
@@ -124,6 +124,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
||||
<FormControl>
|
||||
<SignaturePadDialog
|
||||
disabled={isSubmitting}
|
||||
fullName={user.name ?? ''}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
/>
|
||||
|
||||
@@ -244,11 +244,11 @@ export const SignInForm = ({
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
AuthenticationErrorCode.InvalidCredentials,
|
||||
() => msg`The email or password provided is incorrect`,
|
||||
() => msg`The email or password provided is incorrect.`,
|
||||
)
|
||||
.with(
|
||||
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||
() => msg`The two-factor authentication code provided is incorrect`,
|
||||
() => msg`The two-factor authentication code provided is incorrect.`,
|
||||
)
|
||||
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||
|
||||
@@ -368,7 +368,7 @@ export const SignInForm = ({
|
||||
<p className="mt-2 text-right">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
className="text-sm text-muted-foreground duration-200 hover:opacity-70"
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
</Link>
|
||||
@@ -390,11 +390,11 @@ export const SignInForm = ({
|
||||
<>
|
||||
{hasSocialAuthEnabled && (
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="bg-transparent text-muted-foreground">
|
||||
<Trans>Or continue with</Trans>
|
||||
</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -403,7 +403,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
@@ -417,7 +417,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithMicrosoftClick}
|
||||
>
|
||||
@@ -435,7 +435,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
@@ -452,7 +452,7 @@ export const SignInForm = ({
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
loading={isPasskeyLoading}
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
onClick={onSignInWithPasskey}
|
||||
>
|
||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||
|
||||
@@ -131,7 +131,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
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.`);
|
||||
|
||||
|
||||
@@ -56,13 +56,13 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
||||
/>
|
||||
|
||||
<div
|
||||
className="text-muted-foreground text-sm"
|
||||
className="text-sm text-muted-foreground"
|
||||
title={
|
||||
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
|
||||
}
|
||||
>
|
||||
<p>{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
<p>{recipient.email || recipient.name}</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -417,6 +417,7 @@ export const DirectTemplateSigningForm = ({
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(value) => setSignature(value)}
|
||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||
@@ -433,7 +434,7 @@ export const DirectTemplateSigningForm = ({
|
||||
|
||||
<div className="mt-4 flex gap-x-4">
|
||||
<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"
|
||||
variant="secondary"
|
||||
disabled={isSubmitting}
|
||||
|
||||
@@ -57,12 +57,13 @@ export type DocumentSigningCompleteDialogProps = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
directTemplatePayload?: {
|
||||
recipientPayload?: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
buttonSize?: 'sm' | 'lg';
|
||||
position?: 'start' | 'end' | 'center';
|
||||
disableNameInput?: boolean;
|
||||
};
|
||||
|
||||
const ZNextSignerFormSchema = z.object({
|
||||
@@ -89,10 +90,11 @@ export const DocumentSigningCompleteDialog = ({
|
||||
recipient,
|
||||
disabled = false,
|
||||
allowDictateNextSigner = false,
|
||||
directTemplatePayload,
|
||||
recipientPayload,
|
||||
defaultNextSigner,
|
||||
buttonSize = 'lg',
|
||||
position,
|
||||
disableNameInput = false,
|
||||
}: DocumentSigningCompleteDialogProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -113,11 +115,11 @@ export const DocumentSigningCompleteDialog = ({
|
||||
},
|
||||
});
|
||||
|
||||
const directRecipientForm = useForm<TDirectRecipientFormSchema>({
|
||||
const recipientForm = useForm<TDirectRecipientFormSchema>({
|
||||
resolver: zodResolver(ZDirectRecipientFormSchema),
|
||||
defaultValues: {
|
||||
name: directTemplatePayload?.name ?? '',
|
||||
email: directTemplatePayload?.email ?? '',
|
||||
name: recipientPayload?.name ?? '',
|
||||
email: recipientPayload?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,16 +147,16 @@ export const DocumentSigningCompleteDialog = ({
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
try {
|
||||
let directRecipient: { name: string; email: string } | undefined;
|
||||
let recipientOverridePayload: { name: string; email: string } | undefined;
|
||||
|
||||
if (directTemplatePayload && !directTemplatePayload.email) {
|
||||
const isFormValid = await directRecipientForm.trigger();
|
||||
if (recipientPayload && !recipientPayload.email) {
|
||||
const isFormValid = await recipientForm.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
directRecipient = directRecipientForm.getValues();
|
||||
recipientOverridePayload = recipientForm.getValues();
|
||||
}
|
||||
|
||||
// Check if 2FA is required
|
||||
@@ -168,7 +170,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
? { name: data.name, email: data.email }
|
||||
: undefined;
|
||||
|
||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, directRecipient);
|
||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, recipientOverridePayload);
|
||||
} catch (error) {
|
||||
const err = AppError.parseError(error);
|
||||
|
||||
@@ -222,7 +224,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
<Trans>Are you sure?</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
<div className="max-w-[50ch] text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<span className="inline-flex flex-wrap">
|
||||
@@ -250,19 +252,19 @@ export const DocumentSigningCompleteDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="border-border bg-muted/50 rounded-lg border p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm font-medium">{documentTitle}</p>
|
||||
<div className="rounded-lg border border-border bg-muted/50 p-4 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">{documentTitle}</p>
|
||||
</div>
|
||||
|
||||
{!showTwoFactorForm && (
|
||||
<>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||
{directTemplatePayload && !directTemplatePayload.email && (
|
||||
<Form {...directRecipientForm}>
|
||||
{recipientPayload && !recipientPayload.email && (
|
||||
<Form {...recipientForm}>
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={directRecipientForm.control}
|
||||
control={recipientForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
@@ -274,7 +276,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder={t`Enter your name`}
|
||||
disabled={isNameLocked}
|
||||
disabled={isNameLocked || disableNameInput}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -284,7 +286,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={directRecipientForm.control}
|
||||
control={recipientForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
|
||||
@@ -280,6 +280,7 @@ export const DocumentSigningForm = ({
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
|
||||
@@ -106,7 +106,7 @@ export const DocumentSigningMobileWidget = () => {
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-signing-mobile-widget-progress-bar"
|
||||
className="bg-documenso absolute inset-y-0 left-0"
|
||||
className="bg-primary absolute inset-y-0 left-0"
|
||||
style={{
|
||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||
}}
|
||||
|
||||
@@ -109,7 +109,7 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="absolute inset-y-0 left-0 bg-documenso"
|
||||
className="absolute inset-y-0 left-0 bg-primary"
|
||||
style={{
|
||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||
}}
|
||||
|
||||
@@ -56,8 +56,11 @@ export const DocumentSigningSignatureField = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [fontSize, setFontSize] = useState(2);
|
||||
|
||||
const { signature: providedSignature, setSignature: setProvidedSignature } =
|
||||
useRequiredDocumentSigningContext();
|
||||
const {
|
||||
fullName,
|
||||
signature: providedSignature,
|
||||
setSignature: setProvidedSignature,
|
||||
} = useRequiredDocumentSigningContext();
|
||||
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
@@ -236,13 +239,13 @@ export const DocumentSigningSignatureField = ({
|
||||
type="Signature"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-background">
|
||||
<Loader className="h-5 w-5 animate-spin text-primary md:h-8 md:w-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</p>
|
||||
)}
|
||||
@@ -259,7 +262,7 @@ export const DocumentSigningSignatureField = ({
|
||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
|
||||
<p
|
||||
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` }}
|
||||
>
|
||||
{signature?.typedSignature}
|
||||
@@ -272,12 +275,13 @@ export const DocumentSigningSignatureField = ({
|
||||
<DialogTitle>
|
||||
<Trans>
|
||||
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>
|
||||
</DialogTitle>
|
||||
|
||||
<SignaturePad
|
||||
className="mt-2"
|
||||
fullName={fullName}
|
||||
value={localSignature ?? ''}
|
||||
onChange={({ value }) => setLocalSignature(value)}
|
||||
typedSignatureEnabled={typedSignatureEnabled}
|
||||
|
||||
@@ -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 { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
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 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 { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
||||
import {
|
||||
@@ -31,9 +32,13 @@ import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentUploadButtonLegacyProps = {
|
||||
className?: string;
|
||||
type: EnvelopeType;
|
||||
};
|
||||
|
||||
export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => {
|
||||
export const DocumentUploadButtonLegacy = ({
|
||||
className,
|
||||
type,
|
||||
}: DocumentUploadButtonLegacyProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
@@ -54,8 +59,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||
|
||||
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) {
|
||||
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.`;
|
||||
}
|
||||
|
||||
if (!user.emailVerified) {
|
||||
return msg`Verify your email to upload documents.`;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [remaining.documents, user.emailVerified, team]);
|
||||
}, [remaining.documents, user.emailVerified, team, type]);
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
try {
|
||||
@@ -80,44 +92,62 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
||||
meta: {
|
||||
timezone: userTimezone,
|
||||
},
|
||||
} satisfies TCreateDocumentPayloadSchema;
|
||||
} satisfies TCreateDocumentPayloadSchema | TCreateTemplatePayloadSchema;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
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({
|
||||
title: _(msg`Document uploaded`),
|
||||
description: _(msg`Your document has been uploaded successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
toast({
|
||||
title: _(msg`Document uploaded`),
|
||||
description: _(msg`Your document has been uploaded successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
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) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
console.error(err);
|
||||
|
||||
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(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'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.`);
|
||||
|
||||
@@ -149,17 +179,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
||||
<div>
|
||||
<DocumentUploadButtonPrimitive
|
||||
loading={isLoading}
|
||||
disabled={remaining.documents === 0 || !user.emailVerified}
|
||||
disabled={disabledMessage !== undefined}
|
||||
disabledMessage={disabledMessage}
|
||||
onDrop={async (files) => onFileDrop(files[0])}
|
||||
onDropRejected={onFileDropRejected}
|
||||
type={EnvelopeType.DOCUMENT}
|
||||
type={type}
|
||||
internalVersion="1"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{team?.id === undefined &&
|
||||
type === EnvelopeType.DOCUMENT &&
|
||||
remaining.documents > 0 &&
|
||||
Number.isFinite(remaining.documents) && (
|
||||
<TooltipContent>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { Link, useRevalidator, useSearchParams } from 'react-router';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
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 { 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 { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
||||
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
||||
@@ -81,6 +82,8 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
||||
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const selectedField = useMemo(
|
||||
() => structuredClone(editorFields.selectedField),
|
||||
@@ -135,6 +138,22 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
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 (
|
||||
<div className="relative flex h-full">
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
@@ -230,34 +249,36 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
||||
/>
|
||||
|
||||
{team.preferences.aiFeaturesEnabled && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-4 w-full"
|
||||
onClick={() => setIsAiFieldDialogOpen(true)}
|
||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||
title={
|
||||
envelope.status !== DocumentStatus.DRAFT
|
||||
? _(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>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-4 w-full"
|
||||
onClick={onDetectClick}
|
||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||
title={
|
||||
envelope.status !== DocumentStatus.DRAFT
|
||||
? _(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>
|
||||
</Button>
|
||||
|
||||
<AiFieldDetectionDialog
|
||||
open={isAiFieldDialogOpen}
|
||||
onOpenChange={setIsAiFieldDialogOpen}
|
||||
onComplete={onFieldDetectionComplete}
|
||||
envelopeId={envelope.id}
|
||||
teamId={envelope.teamId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AiFieldDetectionDialog
|
||||
open={isAiFieldDialogOpen}
|
||||
onOpenChange={setIsAiFieldDialogOpen}
|
||||
onComplete={onFieldDetectionComplete}
|
||||
envelopeId={envelope.id}
|
||||
teamId={envelope.teamId}
|
||||
/>
|
||||
|
||||
<AiFeaturesEnableDialog
|
||||
open={isAiEnableDialogOpen}
|
||||
onOpenChange={setIsAiEnableDialogOpen}
|
||||
onEnabled={onAiFeaturesEnabled}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Field details section. */}
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
type SensorAPI,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
||||
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 { z } from 'zod';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
ZRecipientAuthOptionsSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -62,6 +63,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
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 { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
@@ -70,10 +72,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
id: z.number().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: msg`Invalid email`.id })
|
||||
.min(1),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -99,11 +98,19 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
||||
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||
|
||||
// AI recipient detection dialog state
|
||||
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const onAiDialogOpenChange = (open: boolean) => {
|
||||
if (open && !team.preferences.aiFeaturesEnabled) {
|
||||
setIsAiEnableDialogOpen(true);
|
||||
setIsAiDialogOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAiDialogOpen(open);
|
||||
|
||||
if (!open && searchParams.get('ai') === 'true') {
|
||||
@@ -120,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 initialId = useId();
|
||||
@@ -228,12 +251,13 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
keyName: 'nativeId',
|
||||
});
|
||||
|
||||
const emptySigners = useCallback(
|
||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
||||
[form],
|
||||
const emptySignerIndex = watchedSigners.findIndex(
|
||||
(signer) =>
|
||||
!signer.name &&
|
||||
!signer.email &&
|
||||
envelope.fields.filter((field) => field.recipientId === signer.id).length === 0,
|
||||
);
|
||||
|
||||
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
|
||||
const isUserAlreadyARecipient = watchedSigners.some(
|
||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||
);
|
||||
@@ -331,8 +355,14 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Recipients added`,
|
||||
description: t`${detectedRecipients.length} recipient(s) have been added from AI detection.`,
|
||||
title: plural(detectedRecipients.length, {
|
||||
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.`,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -558,21 +588,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValueSigners = formValues.signers || [];
|
||||
|
||||
// Remove the last signer if it's empty.
|
||||
const nonEmptyRecipients = formValueSigners.filter((signer, i) => {
|
||||
if (i === formValueSigners.length - 1 && signer.email === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
|
||||
...formValues,
|
||||
signers: nonEmptyRecipients,
|
||||
});
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||
|
||||
if (!validatedFormValues.success) {
|
||||
return;
|
||||
@@ -641,25 +657,27 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
{team.preferences.aiFeaturesEnabled && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => setIsAiDialogOpen(true)}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
onClick={onDetectRecipientsClick}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<TooltipContent>
|
||||
{team.preferences.aiFeaturesEnabled ? (
|
||||
<Trans>Detect recipients with AI</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
) : (
|
||||
<Trans>Enable AI detection</Trans>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -736,9 +754,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
||||
}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -924,7 +940,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
})}
|
||||
>
|
||||
{!showAdvancedSettings && index === 0 && (
|
||||
<FormLabel required>
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
)}
|
||||
@@ -978,7 +994,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
<FormControl>
|
||||
<RecipientAutoCompleteInput
|
||||
type="text"
|
||||
placeholder={t`Name`}
|
||||
placeholder={t`Recipient ${index + 1}`}
|
||||
{...field}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
@@ -1118,6 +1134,12 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
envelopeId={envelope.id}
|
||||
teamId={envelope.teamId}
|
||||
/>
|
||||
|
||||
<AiFeaturesEnableDialog
|
||||
open={isAiEnableDialogOpen}
|
||||
onOpenChange={setIsAiEnableDialogOpen}
|
||||
onEnabled={onAiFeaturesEnabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -687,8 +687,10 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Email</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Trans>
|
||||
Reply To Email{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -726,8 +728,9 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Trans>
|
||||
Message <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function EnvelopeEditor() {
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="absolute inset-y-0 left-0 bg-documenso"
|
||||
className="absolute inset-y-0 left-0 bg-primary"
|
||||
style={{
|
||||
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { I18n } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
@@ -39,8 +41,15 @@ export const EnvelopeRecipientSelector = ({
|
||||
fields,
|
||||
align = 'start',
|
||||
}: EnvelopeRecipientSelectorProps) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
const getRecipientLabel = useCallback(
|
||||
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||
[recipients],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -49,7 +58,7 @@ export const EnvelopeRecipientSelector = ({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
||||
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
@@ -59,16 +68,12 @@ export const EnvelopeRecipientSelector = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedRecipient?.email && (
|
||||
{selectedRecipient && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
||||
{getRecipientLabel(selectedRecipient)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!selectedRecipient?.email && (
|
||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -105,7 +110,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
fields,
|
||||
placeholder,
|
||||
}: EnvelopeRecipientSelectorCommandProps) => {
|
||||
const { t } = useLingui();
|
||||
const { t, i18n } = useLingui();
|
||||
|
||||
const recipientsByRole = useCallback(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
@@ -154,6 +159,11 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
[fields, recipients],
|
||||
);
|
||||
|
||||
const getRecipientLabel = useCallback(
|
||||
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||
[recipients],
|
||||
);
|
||||
|
||||
return (
|
||||
<Command
|
||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||
@@ -162,21 +172,21 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
<CommandInput placeholder={placeholder} />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<span className="inline-block px-4 text-muted-foreground">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
@@ -205,18 +215,12 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
className={cn('truncate text-foreground/70', {
|
||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||
'opacity-50': isRecipientDisabled(recipient.id),
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
||||
{getRecipientLabel(recipient)}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
@@ -234,7 +238,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
<Info className="z-50 ml-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||
<Trans>
|
||||
This document has already been sent to this recipient. You can no longer
|
||||
edit this recipient.
|
||||
@@ -250,3 +254,22 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
|
||||
const extractRecipientLabel = (recipient: Recipient, recipients: Recipient[], i18n: I18n) => {
|
||||
if (recipient.name && recipient.email) {
|
||||
return `${recipient.name} (${recipient.email})`;
|
||||
}
|
||||
|
||||
if (recipient.name) {
|
||||
return recipient.name;
|
||||
}
|
||||
|
||||
if (recipient.email) {
|
||||
return recipient.email;
|
||||
}
|
||||
|
||||
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
|
||||
const index = recipients.indexOf(recipient);
|
||||
|
||||
return i18n._(msg`Recipient ${index + 1}`);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function EnvelopeSignerForm() {
|
||||
|
||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||
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
|
||||
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
||||
value={selectedAssistantRecipient?.id?.toString()}
|
||||
@@ -54,7 +54,7 @@ export default function EnvelopeSignerForm() {
|
||||
.map((r) => (
|
||||
<div
|
||||
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 gap-3">
|
||||
@@ -69,15 +69,15 @@ export default function EnvelopeSignerForm() {
|
||||
{r.name}
|
||||
|
||||
{r.id === recipient.id && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
<Trans>(You)</Trans>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{r.email}</p>
|
||||
<p className="text-xs text-muted-foreground">{r.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs leading-[inherit]">
|
||||
<div className="text-xs leading-[inherit] text-muted-foreground">
|
||||
<Plural
|
||||
value={assistantFields.filter((field) => field.recipientId === r.id).length}
|
||||
one="# field"
|
||||
@@ -103,7 +103,7 @@ export default function EnvelopeSignerForm() {
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
className="mt-2 bg-background"
|
||||
value={fullName}
|
||||
disabled={isNameLocked}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
||||
@@ -119,6 +119,7 @@ export default function EnvelopeSignerForm() {
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
fullName={fullName}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
|
||||
|
||||
@@ -374,6 +374,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
.with({ type: FieldType.SIGNATURE }, (field) => {
|
||||
handleSignatureFieldClick({
|
||||
field,
|
||||
fullName,
|
||||
signature,
|
||||
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
|
||||
|
||||
@@ -80,12 +80,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
const handleOnCompleteClick = async (
|
||||
nextSigner?: { name: string; email: string },
|
||||
accessAuthOptions?: TRecipientAccessAuth,
|
||||
recipientDetails?: { name: string; email: string },
|
||||
) => {
|
||||
try {
|
||||
await completeDocument({
|
||||
token: recipient.token,
|
||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||
accessAuthOptions,
|
||||
recipientOverride: recipientDetails,
|
||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||
});
|
||||
|
||||
@@ -205,21 +207,30 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const directTemplatePayload = useMemo(() => {
|
||||
const recipientPayload = useMemo(() => {
|
||||
if (!isDirectTemplate) {
|
||||
return;
|
||||
return {
|
||||
name:
|
||||
recipient.name ||
|
||||
recipient.fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||
'',
|
||||
email:
|
||||
recipient.email ||
|
||||
recipient.fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||
'',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: fullName,
|
||||
email: email,
|
||||
};
|
||||
}, [email, fullName, isDirectTemplate]);
|
||||
}, [email, fullName, isDirectTemplate, recipient.email, recipient.name, recipient.fields]);
|
||||
|
||||
return (
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isPending}
|
||||
directTemplatePayload={directTemplatePayload}
|
||||
recipientPayload={recipientPayload}
|
||||
onSignatureComplete={
|
||||
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
||||
}
|
||||
@@ -230,6 +241,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
allowDictateNextSigner={Boolean(
|
||||
nextRecipient && envelope.documentMeta.allowDictateNextSigner,
|
||||
)}
|
||||
disableNameInput={!isDirectTemplate && recipient.name !== ''}
|
||||
defaultNextSigner={
|
||||
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
||||
}
|
||||
|
||||
@@ -123,14 +123,14 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
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(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'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.`);
|
||||
|
||||
|
||||
@@ -126,14 +126,14 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
console.error(err);
|
||||
|
||||
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(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'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.`);
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
||||
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
||||
import { FolderMoveDialog } from '~/components/dialogs/folder-move-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 { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
@@ -70,7 +69,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
||||
|
||||
{type === FolderType.DOCUMENT ? (
|
||||
<DocumentUploadButtonLegacy /> // If you delete this, delete the component as well.
|
||||
) : (
|
||||
<TemplateCreateDialog folderId={parentId ?? undefined} /> // If you delete this, delete the component as well.
|
||||
{/* If you delete this, delete the component as well. */}
|
||||
{organisation.organisationClaim.flags.allowLegacyEnvelopes && (
|
||||
<DocumentUploadButtonLegacy type={type} />
|
||||
)}
|
||||
|
||||
<FolderCreateDialog type={type} />
|
||||
@@ -113,7 +111,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
||||
{isPending ? (
|
||||
<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) => (
|
||||
<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">
|
||||
<Skeleton className="h-8 w-8 rounded" />
|
||||
<div className="flex w-full items-center justify-between">
|
||||
@@ -194,7 +192,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
||||
{foldersData.folders.length > 12 && (
|
||||
<div className="mt-2 flex items-center justify-center">
|
||||
<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()}
|
||||
>
|
||||
View all folders
|
||||
|
||||
@@ -83,8 +83,8 @@ export const StackAvatarsWithTooltip = ({
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -107,8 +107,8 @@ export const StackAvatarsWithTooltip = ({
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Role, Subscription } from '@prisma/client';
|
||||
import { Edit, Loader } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
@@ -82,7 +83,7 @@ export const AdminDashboardUsersTable = ({
|
||||
<Button className="w-24" asChild>
|
||||
<Link to={`/admin/users/${row.original.id}`}>
|
||||
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
||||
Edit
|
||||
<Trans>Edit</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
export type DocumentLogsTableProps = {
|
||||
documentId: number;
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
@@ -26,7 +27,7 @@ const dateFormat: DateTimeFormatOptions = {
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -93,7 +94,9 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
{
|
||||
header: _(msg`Action`),
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDocumentAuditLogAction(_, row.original, userId).description}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`IP Address`),
|
||||
|
||||
@@ -82,7 +82,9 @@ export const OrganisationGroupsDataTable = () => {
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end space-x-2">
|
||||
<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>
|
||||
|
||||
<OrganisationGroupDeleteDialog
|
||||
|
||||
@@ -46,12 +46,16 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
|
||||
const { getTheme } = await themeSessionResolver(request);
|
||||
|
||||
let lang: SupportedLanguageCodes = await langCookie.parse(request.headers.get('cookie') ?? '');
|
||||
const cookieHeader = request.headers.get('cookie') ?? '';
|
||||
|
||||
let lang: SupportedLanguageCodes = await langCookie.parse(cookieHeader);
|
||||
|
||||
if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
|
||||
lang = extractLocaleData({ headers: request.headers }).lang;
|
||||
}
|
||||
|
||||
const disableAnimations = cookieHeader.includes('__disable_animations=true');
|
||||
|
||||
let organisations = null;
|
||||
|
||||
if (session.isAuthenticated) {
|
||||
@@ -62,6 +66,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
{
|
||||
lang,
|
||||
theme: getTheme(),
|
||||
disableAnimations,
|
||||
session: session.isAuthenticated
|
||||
? {
|
||||
user: session.user,
|
||||
@@ -92,7 +97,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
export function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
const { publicEnv, session, lang, ...data } = useLoaderData<typeof loader>() || {};
|
||||
const { publicEnv, session, lang, disableAnimations, ...data } =
|
||||
useLoaderData<typeof loader>() || {};
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
@@ -111,6 +117,14 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
<meta name="google" content="notranslate" />
|
||||
<PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
|
||||
|
||||
{disableAnimations && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `*, *::before, *::after { animation: none !important; transition: none !important; }`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fix: https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer */}
|
||||
<script>0</script>
|
||||
</head>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link, redirect } from 'react-router';
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground mt-4 text-sm">
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
||||
</div>
|
||||
@@ -112,7 +112,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
||||
disabled={envelope.recipients.some(
|
||||
(recipient) =>
|
||||
recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
||||
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||
recipient.role !== RecipientRole.CC,
|
||||
)}
|
||||
onClick={() => resealDocument({ id: envelope.id })}
|
||||
>
|
||||
|
||||
@@ -120,7 +120,9 @@ export default function OrganisationSettingsTeamsPage() {
|
||||
</div>
|
||||
|
||||
<Button asChild>
|
||||
<Link to={`/o/${organisation.url}/settings`}>Manage Organisation</Link>
|
||||
<Link to={`/o/${organisation.url}/settings`}>
|
||||
<Trans>Manage Organisation</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +180,9 @@ const TeamDropdownMenu = ({ team }: { team: TGetOrganisationSessionResponse[0]['
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<MoreVerticalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
<span className="sr-only">
|
||||
<Trans>Open menu</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -87,9 +87,9 @@ export default function OrganisationSettingsBrandingPage() {
|
||||
const settingsHeaderText = t`Branding Preferences`;
|
||||
|
||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||
? t`Here you can set your general branding preferences`
|
||||
? t`Here you can set your general branding preferences.`
|
||||
: 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.`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -85,6 +86,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
},
|
||||
});
|
||||
@@ -112,7 +114,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
|
||||
const settingsHeaderText = t`Document Preferences`;
|
||||
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.`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function OrganisationSettingsGeneral() {
|
||||
<div className="max-w-2xl">
|
||||
<SettingsHeader
|
||||
title={t`Email Preferences`}
|
||||
subtitle={t`You can manage your email preferences here`}
|
||||
subtitle={t`You can manage your email preferences here.`}
|
||||
/>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -75,11 +75,12 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
documentRootPath,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { document, recipients, documentRootPath } = loaderData;
|
||||
const { document, recipients, documentRootPath, userId } = loaderData;
|
||||
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
@@ -171,15 +172,15 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
<section className="mt-6">
|
||||
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
|
||||
{documentInformation.map((info, i) => (
|
||||
<div className="text-foreground text-sm" key={i}>
|
||||
<div className="text-sm text-foreground" key={i}>
|
||||
<h3 className="font-semibold">{_(info.description)}</h3>
|
||||
<p className="text-muted-foreground truncate">{info.value}</p>
|
||||
<p className="truncate text-muted-foreground">{info.value}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-foreground text-sm">
|
||||
<div className="text-sm text-foreground">
|
||||
<h3 className="font-semibold">Recipients</h3>
|
||||
<ul className="text-muted-foreground list-inside list-disc">
|
||||
<ul className="list-inside list-disc text-muted-foreground">
|
||||
{recipients.map((recipient) => (
|
||||
<li key={`recipient-${recipient.id}`}>
|
||||
<span>{formatRecipientText(recipient)}</span>
|
||||
@@ -191,7 +192,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<DocumentLogsTable documentId={document.id} />
|
||||
<DocumentLogsTable documentId={document.id} userId={userId} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -28,8 +28,6 @@ export const loader = () => {
|
||||
export default function TeamsSettingsPage() {
|
||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
||||
|
||||
console.log('isAiFeaturesConfigured', isAiFeaturesConfigured);
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { t } = useLingui();
|
||||
@@ -52,6 +50,7 @@ export default function TeamsSettingsPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -77,6 +76,7 @@ export default function TeamsSettingsPage() {
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
}),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function TeamEmailSettingsGeneral() {
|
||||
<div className="max-w-2xl">
|
||||
<SettingsHeader
|
||||
title={t`Email Preferences`}
|
||||
subtitle={t`You can manage your email preferences here`}
|
||||
subtitle={t`You can manage your email preferences here.`}
|
||||
/>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
||||
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -53,6 +54,8 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
throw redirect('/');
|
||||
}
|
||||
|
||||
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
|
||||
|
||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
||||
@@ -81,6 +84,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
deletedAt: envelope.deletedAt,
|
||||
documentMeta: envelope.documentMeta,
|
||||
},
|
||||
hidePoweredBy: organisationClaim.flags.hidePoweredBy,
|
||||
documentLanguage,
|
||||
messages,
|
||||
};
|
||||
@@ -95,7 +99,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||
*/
|
||||
export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
const { auditLogs, document, documentLanguage, messages } = loaderData;
|
||||
const { auditLogs, document, documentLanguage, hidePoweredBy, messages } = loaderData;
|
||||
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
@@ -188,11 +192,13 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
</div>
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
{!hidePoweredBy && (
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,6 +185,9 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
(log) =>
|
||||
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
|
||||
].filter(
|
||||
@@ -245,11 +248,11 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
<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="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)}
|
||||
</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="block">{getAuthenticationLevel(recipient.id)}</span>
|
||||
</p>
|
||||
@@ -273,13 +276,13 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
)}
|
||||
|
||||
{signature.signature?.typedSignature && (
|
||||
<p className="font-signature text-center text-sm">
|
||||
<p className="text-center font-signature text-sm">
|
||||
{signature.signature?.typedSignature}
|
||||
</p>
|
||||
)}
|
||||
</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="block font-mono uppercase">
|
||||
{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 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="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</span>
|
||||
</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="inline-block">
|
||||
{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">
|
||||
<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="inline-block">
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.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>
|
||||
</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="inline-block">
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
@@ -330,7 +337,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</p>
|
||||
|
||||
{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="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||
@@ -341,7 +348,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</span>
|
||||
</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="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
@@ -355,7 +362,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</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="inline-block">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
|
||||
@@ -371,8 +371,9 @@ const SigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV1Loade
|
||||
to="https://documenso.com"
|
||||
className="text-documenso-700 hover:text-documenso-600"
|
||||
>
|
||||
Check out Documenso.
|
||||
Check out Documenso
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
@@ -470,8 +471,9 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
||||
to="https://documenso.com"
|
||||
className="text-documenso-700 hover:text-documenso-600"
|
||||
>
|
||||
Check out Documenso.
|
||||
Check out Documenso
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -115,7 +115,9 @@ export default function RejectedSigningPage({ loaderData }: Route.ComponentProps
|
||||
|
||||
{user && (
|
||||
<Button className="mt-6" asChild>
|
||||
<Link to={`/`}>Return Home</Link>
|
||||
<Link to={`/`}>
|
||||
<Trans>Return Home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,9 @@ export default function WaitingForTurnToSignPage({ loaderData }: Route.Component
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="link" asChild>
|
||||
<Link to="/">Return Home</Link>
|
||||
<Link to="/">
|
||||
<Trans>Return Home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import satori from 'satori';
|
||||
import sharp from 'sharp';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
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 { svgToPng } from '@documenso/lib/utils/images/svg-to-png';
|
||||
|
||||
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 sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
||||
const pngBuffer = await svgToPng(svg.toString());
|
||||
|
||||
return new Response(pngBuffer, {
|
||||
headers: {
|
||||
|
||||
@@ -193,11 +193,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current User Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<Trans>Your Account</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(user.avatar)}
|
||||
avatarFallback={extractInitials(user.name || user.email)}
|
||||
@@ -215,11 +215,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Organisation Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<Trans>Requesting Organisation</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(organisation.avatar)}
|
||||
avatarFallback={extractInitials(organisation.name)}
|
||||
@@ -237,7 +237,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Warnings Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<Trans>Important: What This Means</Trans>
|
||||
</h3>
|
||||
@@ -253,7 +253,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Eye className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Full account access:
|
||||
</span>{' '}
|
||||
View all your profile information, settings, and activity
|
||||
@@ -264,7 +264,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Settings className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Account management:
|
||||
</span>{' '}
|
||||
Modify your account settings, permissions, and preferences
|
||||
@@ -275,7 +275,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Database className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">Data access:</span>{' '}
|
||||
<span className="font-semibold text-muted-foreground">Data access:</span>{' '}
|
||||
Access all data associated with your account
|
||||
</Trans>
|
||||
</span>
|
||||
@@ -304,7 +304,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={`accept-conditions`}
|
||||
>
|
||||
<Trans>I agree to link my account with this organization</Trans>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
||||
@@ -63,16 +62,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const img = await sharp(file)
|
||||
.toFormat('png', {
|
||||
quality: 80,
|
||||
})
|
||||
.toBuffer();
|
||||
const { content, contentType } = await loadLogo(file);
|
||||
|
||||
return new Response(Buffer.from(img), {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': img.length.toString(),
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': content.length.toString(),
|
||||
// Stale while revalidate for 1 hours to 24 hours
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||
|
||||
import type { Route } from './+types/branding.logo.team.$teamId';
|
||||
|
||||
@@ -56,16 +55,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const img = await sharp(file)
|
||||
.toFormat('png', {
|
||||
quality: 80,
|
||||
})
|
||||
.toBuffer();
|
||||
const { content, contentType } = await loadLogo(file);
|
||||
|
||||
return new Response(img, {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': img.length.toString(),
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': content.length.toString(),
|
||||
// Stale while revalidate for 1 hours to 24 hours
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
|
||||