mirror of
https://github.com/documenso/documenso.git
synced 2026-07-12 05:55:12 +10:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bac2bf11f4 | |||
| d93b2a70a7 | |||
| 5da915da38 | |||
| dcaecf1fc5 | |||
| f70b76d8b8 | |||
| 93137c6396 | |||
| d058b7c705 | |||
| b51f562224 | |||
| f80aa4bf72 | |||
| 9238f759a6 | |||
| 74ad6af47d | |||
| 18902ed59d | |||
| 3f70082146 | |||
| 31ba6d5f00 | |||
| c4f89a87a2 | |||
| 89d6dd5b0e |
@@ -1,14 +1,19 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main', 'feat/rr7']
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e_tests:
|
e2e_tests:
|
||||||
name: 'E2E Tests'
|
name: 'E2E Tests'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: warp-ubuntu-2204-x64-16x
|
runs-on: warp-ubuntu-2204-x64-8x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -28,9 +33,6 @@ jobs:
|
|||||||
- name: Seed the database
|
- name: Seed the database
|
||||||
run: npm run prisma:seed
|
run: npm run prisma:seed
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Install playwright browsers
|
- name: Install playwright browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: 'packages/app-tests/**/test-results/*'
|
path: 'packages/app-tests/**/test-results/*'
|
||||||
retention-days: 30
|
retention-days: 7
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ jobs:
|
|||||||
environment: Translations
|
environment: Translations
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -26,12 +27,54 @@ jobs:
|
|||||||
- name: Extract translations
|
- name: Extract translations
|
||||||
run: npm run translate:extract
|
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: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BRANCH="chore/extract-translations"
|
||||||
|
|
||||||
git config --global user.name 'github-actions'
|
git config --global user.name 'github-actions'
|
||||||
git config --global user.email 'github-actions@documenso.com'
|
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 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
|
- name: Compile translations
|
||||||
id: compile_translations
|
id: compile_translations
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export default {
|
|||||||
'how-to': 'How To',
|
'how-to': 'How To',
|
||||||
'setting-up-oauth-providers': 'Setting up OAuth Providers',
|
'setting-up-oauth-providers': 'Setting up OAuth Providers',
|
||||||
telemetry: 'Telemetry',
|
telemetry: 'Telemetry',
|
||||||
|
'ai-features': 'AI Recipient & Field Detection',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: AI Recipient & Field Detection (Self-hosting)
|
||||||
|
description: Configure Google Vertex AI so Documenso can detect recipients and fields automatically.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout, Steps } from 'nextra/components';
|
||||||
|
|
||||||
|
# AI Recipient & Field Detection (Self-hosting)
|
||||||
|
|
||||||
|
This guide covers how to enable the AI recipient and field detection features when you self-host Documenso.
|
||||||
|
|
||||||
|
## What this enables
|
||||||
|
|
||||||
|
- Detect recipients from uploaded PDFs (roles, names, emails when present).
|
||||||
|
- Detect and place fields (signature, initials, name, email, date, text, number, radio, checkbox) onto draft envelopes.
|
||||||
|
- Built-in rate limits (3 requests per minute per IP) to prevent abuse.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Google Cloud project with the **Vertex AI API** enabled and billing active.
|
||||||
|
- A **Vertex AI Express API key** with access to Gemini models (create via the [Vertex AI Express flow](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview) and manage keys in [API keys](https://cloud.google.com/vertex-ai/generative-ai/docs/start/api-keys)).
|
||||||
|
- Documenso version that includes the AI detection feature and the corresponding database migration.
|
||||||
|
|
||||||
|
## Configure environment variables
|
||||||
|
|
||||||
|
Add these variables to your deployment `.env` (or secret manager):
|
||||||
|
|
||||||
|
```
|
||||||
|
GOOGLE_VERTEX_PROJECT_ID="<your-gcp-project-id>"
|
||||||
|
GOOGLE_VERTEX_API_KEY="<your-vertex-api-key>"
|
||||||
|
# Optional, defaults to "global"
|
||||||
|
GOOGLE_VERTEX_LOCATION="global"
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Use a region close to your users if you need data residency considerations (e.g. `europe-west1`).
|
||||||
|
If you omit the location, Documenso uses `global`. Not all models are available in every region;
|
||||||
|
if a model is unavailable, switch to a supported region.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Deploy with the published container
|
||||||
|
|
||||||
|
- Use the official Documenso image (DockerHub or GHCR) and supply the Vertex env vars above.
|
||||||
|
- Ensure migrations run on startup (the container runs `prisma migrate deploy` in production mode).
|
||||||
|
- Restart the container after adding or changing Vertex env vars.
|
||||||
|
|
||||||
|
## Enable the feature in Documenso
|
||||||
|
|
||||||
|
Once the service is running with the Vertex env vars:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
### Organisation settings
|
||||||
|
|
||||||
|
Go to **Settings → Document Preferences → AI Features** and set to **Enabled**. Teams that inherit organisation defaults will pick this up.
|
||||||
|
|
||||||
|
### Team settings
|
||||||
|
|
||||||
|
If a team overrides organisation defaults, go to **Team Settings → Document Preferences → AI Features** and choose **Enabled** (or **Inherit** to follow the organisation).
|
||||||
|
|
||||||
|
### Verify in the editor
|
||||||
|
|
||||||
|
Open a draft envelope. In **Recipients**, you should see the sparkle button for AI detection. In **Fields**, you should see **Detect with AI** available.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Too many requests**: Wait a minute or two and retry (rate limit is 3/min per IP).
|
||||||
|
- **AI options hidden**: Ensure the env vars are set, the server was restarted after setting them, and `aiFeaturesEnabled` is enabled at organisation/team level.
|
||||||
|
- **Detection fails immediately**: Confirm the Vertex API key is valid and the project has Vertex AI enabled. Check server logs for status codes from Vertex.
|
||||||
|
|
||||||
|
If issues persist, recheck env vars, restart the service, and confirm the Prisma migration was applied.
|
||||||
@@ -119,6 +119,8 @@ NEXT_PRIVATE_SMTP_USERNAME="<your-username>"
|
|||||||
NEXT_PRIVATE_SMTP_PASSWORD="<your-password>"
|
NEXT_PRIVATE_SMTP_PASSWORD="<your-password>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For full AI setup details (including model availability notes), see the [AI Recipient & Field Detection (Self-hosting)](./ai-features) page.
|
||||||
|
|
||||||
### Set Up Your Signing Certificate
|
### Set Up Your Signing Certificate
|
||||||
|
|
||||||
<Callout type="warning">
|
<Callout type="warning">
|
||||||
@@ -267,58 +269,63 @@ You can access the Documenso application by visiting the URL you provided for th
|
|||||||
|
|
||||||
The environment variables listed above are a subset of those available for configuring Documenso. The table below provides a complete list of environment variables and their descriptions.
|
The environment variables listed above are a subset of those available for configuring Documenso. The table below provides a complete list of environment variables and their descriptions.
|
||||||
|
|
||||||
| Variable | Description |
|
For AI setup specifics, see the [AI Recipient & Field Detection (Self-hosting)](./ai-features) page.
|
||||||
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
||||||
| `PORT` | The port on which the Documenso application runs. It defaults to `3000`. |
|
| Variable | Description |
|
||||||
| `NEXTAUTH_SECRET` | The secret key used by NextAuth.js for encryption and signing. |
|
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `NEXT_PRIVATE_ENCRYPTION_KEY` | The primary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
| `PORT` | The port on which the Documenso application runs. It defaults to `3000`. |
|
||||||
| `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | The secondary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
| `NEXTAUTH_SECRET` | The secret key used by NextAuth.js for encryption and signing. |
|
||||||
| `NEXT_PRIVATE_GOOGLE_CLIENT_ID` | The Google client ID for Google authentication (optional). |
|
| `NEXT_PRIVATE_ENCRYPTION_KEY` | The primary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
||||||
| `NEXT_PRIVATE_GOOGLE_CLIENT_SECRET` | The Google client secret for Google authentication (optional). |
|
| `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | The secondary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
||||||
| `NEXT_PRIVATE_MICROSOFT_CLIENT_ID` | The Microsoft client ID for Microsoft authentication (optional). |
|
| `NEXT_PRIVATE_GOOGLE_CLIENT_ID` | The Google client ID for Google authentication (optional). |
|
||||||
| `NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET` | The Microsoft client secret for Microsoft authentication (optional). |
|
| `NEXT_PRIVATE_GOOGLE_CLIENT_SECRET` | The Google client secret for Google authentication (optional). |
|
||||||
| `NEXT_PRIVATE_OIDC_CLIENT_ID` | The OIDC client ID for OIDC authentication (optional). |
|
| `NEXT_PRIVATE_MICROSOFT_CLIENT_ID` | The Microsoft client ID for Microsoft authentication (optional). |
|
||||||
| `NEXT_PRIVATE_OIDC_CLIENT_SECRET` | The OIDC client secret for OIDC authentication (optional). |
|
| `NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET` | The Microsoft client secret for Microsoft authentication (optional). |
|
||||||
| `NEXT_PRIVATE_OIDC_WELL_KNOWN` | The well-known URL for the OIDC provider (optional). |
|
| `NEXT_PRIVATE_OIDC_CLIENT_ID` | The OIDC client ID for OIDC authentication (optional). |
|
||||||
| `NEXT_PRIVATE_OIDC_PROVIDER_LABEL` | The label to display for the OIDC provider button (optional). |
|
| `NEXT_PRIVATE_OIDC_CLIENT_SECRET` | The OIDC client secret for OIDC authentication (optional). |
|
||||||
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | Whether to skip email verification for OIDC accounts (optional, default `false`). |
|
| `NEXT_PRIVATE_OIDC_WELL_KNOWN` | The well-known URL for the OIDC provider (optional). |
|
||||||
| `NEXT_PUBLIC_WEBAPP_URL` | The URL for the web application. |
|
| `NEXT_PRIVATE_OIDC_PROVIDER_LABEL` | The label to display for the OIDC provider button (optional). |
|
||||||
| `NEXT_PUBLIC_SUPPORT_EMAIL` | The support email address displayed to users (default `support@documenso.com`). |
|
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | Whether to skip email verification for OIDC accounts (optional, default `false`). |
|
||||||
| `NEXT_PRIVATE_DATABASE_URL` | The URL for the primary database connection (with connection pooling). |
|
| `NEXT_PUBLIC_WEBAPP_URL` | The URL for the web application. |
|
||||||
| `NEXT_PRIVATE_DIRECT_DATABASE_URL` | The URL for the direct database connection (without connection pooling). |
|
| `NEXT_PUBLIC_SUPPORT_EMAIL` | The support email address displayed to users (default `support@documenso.com`). |
|
||||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | The signing transport to use. Available options: local (default) |
|
| `NEXT_PRIVATE_DATABASE_URL` | The URL for the primary database connection (with connection pooling). |
|
||||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | The passphrase for the key file. |
|
| `NEXT_PRIVATE_DIRECT_DATABASE_URL` | The URL for the direct database connection (without connection pooling). |
|
||||||
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | The base64-encoded contents of the key file will be used instead of the file path. |
|
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | The signing transport to use. Available options: local (default) |
|
||||||
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | The path to the key file, default `/opt/documenso/cert.p12`. |
|
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | The passphrase for the key file. |
|
||||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | The transport for file uploads (database or s3). |
|
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | The base64-encoded contents of the key file will be used instead of the file path. |
|
||||||
| `NEXT_PRIVATE_UPLOAD_ENDPOINT` | The endpoint for the S3 storage transport (for third-party S3-compatible providers). |
|
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | The path to the key file, default `/opt/documenso/cert.p12`. |
|
||||||
| `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE` | Whether to force path-style URLs for the S3 storage transport. |
|
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | The transport for file uploads (database or s3). |
|
||||||
| `NEXT_PRIVATE_UPLOAD_REGION` | The region for the S3 storage transport (defaults to us-east-1). |
|
| `NEXT_PRIVATE_UPLOAD_ENDPOINT` | The endpoint for the S3 storage transport (for third-party S3-compatible providers). |
|
||||||
| `NEXT_PRIVATE_UPLOAD_BUCKET` | The bucket to use for the S3 storage transport. |
|
| `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE` | Whether to force path-style URLs for the S3 storage transport. |
|
||||||
| `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | The access key ID for the S3 storage transport. |
|
| `NEXT_PRIVATE_UPLOAD_REGION` | The region for the S3 storage transport (defaults to us-east-1). |
|
||||||
| `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | The secret access key for the S3 storage transport. |
|
| `NEXT_PRIVATE_UPLOAD_BUCKET` | The bucket to use for the S3 storage transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_TRANSPORT` | The transport to send emails (smtp-auth, smtp-api, resend, or mailchannels). |
|
| `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | The access key ID for the S3 storage transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_HOST` | The host for the SMTP server for SMTP transports. |
|
| `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | The secret access key for the S3 storage transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_PORT` | The port for the SMTP server for SMTP transports. |
|
| `NEXT_PRIVATE_SMTP_TRANSPORT` | The transport to send emails (smtp-auth, smtp-api, resend, or mailchannels). |
|
||||||
| `NEXT_PRIVATE_SMTP_USERNAME` | The username for the SMTP server for the `smtp-auth` transport. |
|
| `NEXT_PRIVATE_SMTP_HOST` | The host for the SMTP server for SMTP transports. |
|
||||||
| `NEXT_PRIVATE_SMTP_PASSWORD` | The password for the SMTP server for the `smtp-auth` transport. |
|
| `NEXT_PRIVATE_SMTP_PORT` | The port for the SMTP server for SMTP transports. |
|
||||||
| `NEXT_PRIVATE_SMTP_APIKEY_USER` | The API key user for the SMTP server for the `smtp-api` transport. |
|
| `NEXT_PRIVATE_SMTP_USERNAME` | The username for the SMTP server for the `smtp-auth` transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_APIKEY` | The API key for the SMTP server for the `smtp-api` transport. |
|
| `NEXT_PRIVATE_SMTP_PASSWORD` | The password for the SMTP server for the `smtp-auth` transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_SECURE` | Whether to force the use of TLS for the SMTP server for SMTP transports. |
|
| `NEXT_PRIVATE_SMTP_APIKEY_USER` | The API key user for the SMTP server for the `smtp-api` transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS` | Whether to ignore TLS errors for the SMTP server (useful for self-signed certificates). |
|
| `NEXT_PRIVATE_SMTP_APIKEY` | The API key for the SMTP server for the `smtp-api` transport. |
|
||||||
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | The email address for the "from" address. |
|
| `NEXT_PRIVATE_SMTP_SECURE` | Whether to force the use of TLS for the SMTP server for SMTP transports. |
|
||||||
| `NEXT_PRIVATE_SMTP_FROM_NAME` | The sender name for the "from" address. |
|
| `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS` | Whether to ignore TLS errors for the SMTP server (useful for self-signed certificates). |
|
||||||
| `NEXT_PRIVATE_RESEND_API_KEY` | The API key for Resend.com for the `resend` transport. |
|
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | The email address for the "from" address. |
|
||||||
| `NEXT_PRIVATE_MAILCHANNELS_API_KEY` | The optional API key for MailChannels (if using a proxy) for the `mailchannels` transport. |
|
| `NEXT_PRIVATE_SMTP_FROM_NAME` | The sender name for the "from" address. |
|
||||||
| `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT` | The optional endpoint for the MailChannels API (if using a proxy) for the `mailchannels` transport. |
|
| `NEXT_PRIVATE_RESEND_API_KEY` | The API key for Resend.com for the `resend` transport. |
|
||||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN` | The domain for DKIM signing with MailChannels for the `mailchannels` transport. |
|
| `NEXT_PRIVATE_MAILCHANNELS_API_KEY` | The optional API key for MailChannels (if using a proxy) for the `mailchannels` transport. |
|
||||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR` | The selector for DKIM signing with MailChannels for the `mailchannels` transport. |
|
| `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT` | The optional endpoint for the MailChannels API (if using a proxy) for the `mailchannels` transport. |
|
||||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | The private key for DKIM signing with MailChannels for the `mailchannels` transport. |
|
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN` | The domain for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||||
| `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` | The maximum document upload limit displayed to the user (in MB). |
|
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR` | The selector for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||||
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
|
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | The private key for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
|
| `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` | The maximum document upload limit displayed to the user (in MB). |
|
||||||
| `NEXT_PRIVATE_BROWSERLESS_URL` | The URL for a Browserless.io instance to generate PDFs (optional). |
|
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
|
||||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Set to `true` to disable anonymous telemetry (see [Telemetry](#telemetry) section below). |
|
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
|
||||||
|
| `NEXT_PRIVATE_BROWSERLESS_URL` | The URL for a Browserless.io instance to generate PDFs (optional). |
|
||||||
|
| `DOCUMENSO_DISABLE_TELEMETRY` | Set to `true` to disable anonymous telemetry (see [Telemetry](#telemetry) section below). |
|
||||||
|
| `GOOGLE_VERTEX_PROJECT_ID` | Google Cloud project ID used for Vertex AI (required for AI detection). |
|
||||||
|
| `GOOGLE_VERTEX_API_KEY` | Vertex AI Express API key with access to Gemini models (required for AI detection). See [AI Recipient & Field Detectionfor](./ai-features) for details. |
|
||||||
|
| `GOOGLE_VERTEX_LOCATION` | Optional Vertex region, defaults to `global`. Not all models are available in every region. |
|
||||||
|
|
||||||
## Telemetry
|
## Telemetry
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export default {
|
|||||||
'document-visibility': 'Document Visibility',
|
'document-visibility': 'Document Visibility',
|
||||||
fields: 'Document Fields',
|
fields: 'Document Fields',
|
||||||
'email-preferences': 'Email Preferences',
|
'email-preferences': 'Email Preferences',
|
||||||
|
'ai-detection': 'AI Recipient & Field Detection',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: AI Recipient & Field Detection
|
||||||
|
description: Use Documenso’s AI helpers to detect recipients and fields in draft documents.
|
||||||
|
---
|
||||||
|
|
||||||
|
# AI Recipient & Field Detection
|
||||||
|
|
||||||
|
Documenso can suggest recipients and place fields automatically using Google Vertex AI (Gemini). The feature is optional and only available when your organisation or team has **AI Features** enabled. Documents are processed securely and providers do not retain your data for training.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- AI Features must be enabled in **Document Preferences** for your organisation or team.
|
||||||
|
- The envelope must be in **Draft** status.
|
||||||
|
- Helpful rate limits are in place (up to 3 detection requests per minute per IP) to prevent abuse. If you see a “too many requests” message, wait a minute or two and try again.
|
||||||
|
|
||||||
|
### Enable AI features
|
||||||
|
|
||||||
|
1. **Organisation settings**:
|
||||||
|
|
||||||
|
Settings → Document Preferences → **AI Features** → Enabled.
|
||||||
|
|
||||||
|
_This applies to teams that inherit organisation defaults._
|
||||||
|
|
||||||
|
2. **Team settings**:
|
||||||
|
|
||||||
|
Team Settings → Document Preferences → **AI Features** → choose Enabled, Disabled, or Inherit.
|
||||||
|
|
||||||
|
## Detect recipients
|
||||||
|
|
||||||
|
Use this to identify who needs to sign or approve.
|
||||||
|
|
||||||
|
1. Open a draft document/template and go to the **Recipients** panel.
|
||||||
|
2. Select the **sparkle** button to start detection. If AI is enabled, uploads launched from the dashboard will open the detector automatically.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Wait for progress to finish, then review the suggested recipients.
|
||||||
|
4. Remove any incorrect entries, then **Add recipients** to apply them. Existing recipients and duplicates are preserved.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Detection is unavailable once an envelope is completed.
|
||||||
|
- You can re-run detection if you update the document; each run counts toward the rate limit.
|
||||||
|
|
||||||
|
## Detect fields
|
||||||
|
|
||||||
|
Use this to auto-place fields on the pages of a draft.
|
||||||
|
|
||||||
|
1. Open the envelope editor and switch to the **Fields** tab.
|
||||||
|
2. Select **Detect with AI**. Provide optional context (e.g., “Alice is the tenant, Bob is the landlord”) to improve recipient assignment.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
3. Watch the progress indicators; they update per page and total fields found.
|
||||||
|
4. Review the summary and choose **Add fields** to place them in the editor.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Works only for draft envelopes and teams with AI features enabled.
|
||||||
|
- Existing fields are masked during detection to avoid duplicates.
|
||||||
|
- Fields are assigned to recipients based on nearby labels and your context message; you can edit them after adding.
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
- Keep labels near the intended fields (e.g., “Tenant signature”, “Buyer email”).
|
||||||
|
- Provide short context when roles are ambiguous.
|
||||||
|
- Always review suggestions before sending; AI assists but does not replace final checks.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 466 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
@@ -19,6 +19,7 @@ import * as z from 'zod';
|
|||||||
|
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
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 { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -129,18 +130,43 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
const distributionMethod = watch('meta.distributionMethod');
|
const distributionMethod = watch('meta.distributionMethod');
|
||||||
|
|
||||||
|
const recipientsWithIndex = useMemo(
|
||||||
|
() =>
|
||||||
|
envelope.recipients.map((recipient, index) => ({
|
||||||
|
...recipient,
|
||||||
|
index,
|
||||||
|
})),
|
||||||
|
[envelope.recipients],
|
||||||
|
);
|
||||||
|
|
||||||
const recipientsMissingSignatureFields = useMemo(
|
const recipientsMissingSignatureFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
envelope.recipients.filter(
|
recipientsWithIndex.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.SIGNER &&
|
recipient.role === RecipientRole.SIGNER &&
|
||||||
!envelope.fields.some(
|
!envelope.fields.some(
|
||||||
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
(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(() => {
|
const invalidEnvelopeCode = useMemo(() => {
|
||||||
if (recipientsMissingSignatureFields.length > 0) {
|
if (recipientsMissingSignatureFields.length > 0) {
|
||||||
return 'MISSING_SIGNATURES';
|
return 'MISSING_SIGNATURES';
|
||||||
@@ -150,8 +176,12 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
return 'MISSING_RECIPIENTS';
|
return 'MISSING_RECIPIENTS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipientsMissingRequiredEmail.length > 0) {
|
||||||
|
return 'MISSING_REQUIRED_EMAIL';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [envelope.recipients, envelope.fields, recipientsMissingSignatureFields]);
|
}, [envelope.recipients, recipientsMissingRequiredEmail, recipientsMissingSignatureFields]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
||||||
try {
|
try {
|
||||||
@@ -444,7 +474,22 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
<ul className="ml-2 mt-1 list-inside list-disc">
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
{recipientsMissingSignatureFields.map((recipient) => (
|
{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>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
const ZSignFieldEmailFormSchema = z.object({
|
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>;
|
type TSignFieldEmailFormSchema = z.infer<typeof ZSignFieldEmailFormSchema>;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
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 { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -65,7 +66,7 @@ const ZAddRecipientsForNewDocumentSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
email: z.string().email(),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
@@ -100,12 +101,29 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery(
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
{
|
||||||
defaultValues: {
|
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,
|
distributeDocument: false,
|
||||||
useCustomDocument: false,
|
useCustomDocument: false,
|
||||||
customDocumentData: [],
|
customDocumentData: envelopeItems.map((item) => ({
|
||||||
|
title: item.title,
|
||||||
|
data: undefined,
|
||||||
|
envelopeItemId: item.id,
|
||||||
|
})),
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@@ -124,7 +142,12 @@ export function TemplateUseDialog({
|
|||||||
signingOrder: recipient.signingOrder ?? undefined,
|
signingOrder: recipient.signingOrder ?? undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
|
defaultValues: generateDefaultFormValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { replace, fields: localCustomDocumentData } = useFieldArray({
|
const { replace, fields: localCustomDocumentData } = useFieldArray({
|
||||||
@@ -132,19 +155,6 @@ export function TemplateUseDialog({
|
|||||||
name: 'customDocumentData',
|
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 } =
|
const { mutateAsync: createDocumentFromTemplate } =
|
||||||
trpc.template.createDocumentFromTemplate.useMutation();
|
trpc.template.createDocumentFromTemplate.useMutation();
|
||||||
|
|
||||||
@@ -214,8 +224,8 @@ export function TemplateUseDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (open) {
|
||||||
form.reset();
|
form.reset(generateDefaultFormValues());
|
||||||
}
|
}
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
@@ -322,7 +332,7 @@ export function TemplateUseDialog({
|
|||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
aria-label="Name"
|
aria-label="Name"
|
||||||
placeholder={recipients[index].name || _(msg`Name`)}
|
placeholder={recipients[index].name || _(msg`Recipient ${index + 1}`)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -349,7 +359,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
<label
|
<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"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Send document</Trans>
|
<Trans>Send document</Trans>
|
||||||
@@ -358,7 +368,7 @@ export function TemplateUseDialog({
|
|||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</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>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
The document will be immediately sent to recipients if this
|
The document will be immediately sent to recipients if this
|
||||||
@@ -378,7 +388,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
<label
|
<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"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Create as pending</Trans>
|
<Trans>Create as pending</Trans>
|
||||||
@@ -386,7 +396,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</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>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Create the document as pending and ready to sign.
|
Create the document as pending and ready to sign.
|
||||||
@@ -432,7 +442,7 @@ export function TemplateUseDialog({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<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"
|
htmlFor="useCustomDocument"
|
||||||
>
|
>
|
||||||
<Trans>Upload custom document</Trans>
|
<Trans>Upload custom document</Trans>
|
||||||
@@ -440,7 +450,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</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>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Upload a custom document to use instead of the template's default
|
Upload a custom document to use instead of the template's default
|
||||||
@@ -470,19 +480,19 @@ export function TemplateUseDialog({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
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="flex-shrink-0">
|
||||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||||
<FileTextIcon className="text-primary h-5 w-5" />
|
<FileTextIcon className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<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}
|
{item.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
<div>
|
<div>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ZDocumentMetaDateFormatSchema,
|
ZDocumentMetaDateFormatSchema,
|
||||||
ZDocumentMetaLanguageSchema,
|
ZDocumentMetaLanguageSchema,
|
||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
||||||
|
|
||||||
// Define the schema for configuration
|
// Define the schema for configuration
|
||||||
@@ -55,7 +56,7 @@ export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.exten
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
formId: z.string(),
|
formId: z.string(),
|
||||||
name: 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']),
|
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="text-muted-foreground text-sm"
|
className="text-sm text-muted-foreground"
|
||||||
title={
|
title={
|
||||||
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
|
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{recipient.email}</p>
|
<p>{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-18
@@ -57,12 +57,13 @@ export type DocumentSigningCompleteDialogProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
directTemplatePayload?: {
|
recipientPayload?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
buttonSize?: 'sm' | 'lg';
|
buttonSize?: 'sm' | 'lg';
|
||||||
position?: 'start' | 'end' | 'center';
|
position?: 'start' | 'end' | 'center';
|
||||||
|
disableNameInput?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZNextSignerFormSchema = z.object({
|
const ZNextSignerFormSchema = z.object({
|
||||||
@@ -89,10 +90,11 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
recipient,
|
recipient,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
allowDictateNextSigner = false,
|
allowDictateNextSigner = false,
|
||||||
directTemplatePayload,
|
recipientPayload,
|
||||||
defaultNextSigner,
|
defaultNextSigner,
|
||||||
buttonSize = 'lg',
|
buttonSize = 'lg',
|
||||||
position,
|
position,
|
||||||
|
disableNameInput = false,
|
||||||
}: DocumentSigningCompleteDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@@ -113,11 +115,11 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const directRecipientForm = useForm<TDirectRecipientFormSchema>({
|
const recipientForm = useForm<TDirectRecipientFormSchema>({
|
||||||
resolver: zodResolver(ZDirectRecipientFormSchema),
|
resolver: zodResolver(ZDirectRecipientFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: directTemplatePayload?.name ?? '',
|
name: recipientPayload?.name ?? '',
|
||||||
email: directTemplatePayload?.email ?? '',
|
email: recipientPayload?.email ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,16 +147,16 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||||
try {
|
try {
|
||||||
let directRecipient: { name: string; email: string } | undefined;
|
let recipientOverridePayload: { name: string; email: string } | undefined;
|
||||||
|
|
||||||
if (directTemplatePayload && !directTemplatePayload.email) {
|
if (recipientPayload && !recipientPayload.email) {
|
||||||
const isFormValid = await directRecipientForm.trigger();
|
const isFormValid = await recipientForm.trigger();
|
||||||
|
|
||||||
if (!isFormValid) {
|
if (!isFormValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
directRecipient = directRecipientForm.getValues();
|
recipientOverridePayload = recipientForm.getValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if 2FA is required
|
// Check if 2FA is required
|
||||||
@@ -168,7 +170,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
? { name: data.name, email: data.email }
|
? { name: data.name, email: data.email }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, directRecipient);
|
await onSignatureComplete(nextSigner, data.accessAuthOptions, recipientOverridePayload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = AppError.parseError(error);
|
const err = AppError.parseError(error);
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
<Trans>Are you sure?</Trans>
|
<Trans>Are you sure?</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
<div className="max-w-[50ch] text-muted-foreground">
|
||||||
{match(recipient.role)
|
{match(recipient.role)
|
||||||
.with(RecipientRole.VIEWER, () => (
|
.with(RecipientRole.VIEWER, () => (
|
||||||
<span className="inline-flex flex-wrap">
|
<span className="inline-flex flex-wrap">
|
||||||
@@ -250,19 +252,19 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="border-border bg-muted/50 rounded-lg border p-4 text-center">
|
<div className="rounded-lg border border-border bg-muted/50 p-4 text-center">
|
||||||
<p className="text-muted-foreground text-sm font-medium">{documentTitle}</p>
|
<p className="text-sm font-medium text-muted-foreground">{documentTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showTwoFactorForm && (
|
{!showTwoFactorForm && (
|
||||||
<>
|
<>
|
||||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||||
{directTemplatePayload && !directTemplatePayload.email && (
|
{recipientPayload && !recipientPayload.email && (
|
||||||
<Form {...directRecipientForm}>
|
<Form {...recipientForm}>
|
||||||
<div className="mb-4 flex flex-col gap-4">
|
<div className="mb-4 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
@@ -274,7 +276,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
{...field}
|
{...field}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
placeholder={t`Enter your name`}
|
placeholder={t`Enter your name`}
|
||||||
disabled={isNameLocked}
|
disabled={isNameLocked || disableNameInput}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -284,7 +286,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
|
|||||||
+11
-29
@@ -8,7 +8,6 @@ import {
|
|||||||
type SensorAPI,
|
type SensorAPI,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
ZRecipientActionAuthTypesSchema,
|
ZRecipientActionAuthTypesSchema,
|
||||||
ZRecipientAuthOptionsSchema,
|
ZRecipientAuthOptionsSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -70,10 +70,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
formId: z.string().min(1),
|
formId: z.string().min(1),
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z
|
email: ZRecipientEmailSchema,
|
||||||
.string()
|
|
||||||
.email({ message: msg`Invalid email`.id })
|
|
||||||
.min(1),
|
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
@@ -228,12 +225,13 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
keyName: 'nativeId',
|
keyName: 'nativeId',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptySigners = useCallback(
|
const emptySignerIndex = watchedSigners.findIndex(
|
||||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
(signer) =>
|
||||||
[form],
|
!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(
|
const isUserAlreadyARecipient = watchedSigners.some(
|
||||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||||
);
|
);
|
||||||
@@ -558,21 +556,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formValueSigners = formValues.signers || [];
|
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!validatedFormValues.success) {
|
if (!validatedFormValues.success) {
|
||||||
return;
|
return;
|
||||||
@@ -736,9 +720,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -924,7 +906,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showAdvancedSettings && index === 0 && (
|
{!showAdvancedSettings && index === 0 && (
|
||||||
<FormLabel required>
|
<FormLabel>
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
@@ -978,7 +960,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RecipientAutoCompleteInput
|
<RecipientAutoCompleteInput
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t`Name`}
|
placeholder={t`Recipient ${index + 1}`}
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
snapshot.isDragging ||
|
snapshot.isDragging ||
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import type { I18n } from '@lingui/core';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
@@ -39,8 +41,15 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
fields,
|
fields,
|
||||||
align = 'start',
|
align = 'start',
|
||||||
}: EnvelopeRecipientSelectorProps) => {
|
}: EnvelopeRecipientSelectorProps) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -49,7 +58,7 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
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(
|
getRecipientColorStyles(
|
||||||
Math.max(
|
Math.max(
|
||||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||||
@@ -59,16 +68,12 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedRecipient?.email && (
|
{selectedRecipient && (
|
||||||
<span className="flex-1 truncate text-left">
|
<span className="flex-1 truncate text-left">
|
||||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
{getRecipientLabel(selectedRecipient)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedRecipient?.email && (
|
|
||||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -105,7 +110,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
fields,
|
fields,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: EnvelopeRecipientSelectorCommandProps) => {
|
}: EnvelopeRecipientSelectorCommandProps) => {
|
||||||
const { t } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
|
|
||||||
const recipientsByRole = useCallback(() => {
|
const recipientsByRole = useCallback(() => {
|
||||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||||
@@ -154,6 +159,11 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
[fields, recipients],
|
[fields, recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command
|
<Command
|
||||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||||
@@ -162,21 +172,21 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
<CommandInput placeholder={placeholder} />
|
<CommandInput placeholder={placeholder} />
|
||||||
|
|
||||||
<CommandEmpty>
|
<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>
|
<Trans>No recipient matching this description was found.</Trans>
|
||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||||
<CommandGroup key={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)}
|
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{roleRecipients.length === 0 && (
|
{roleRecipients.length === 0 && (
|
||||||
<div
|
<div
|
||||||
key={`${role}-empty`}
|
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>
|
<Trans>No recipients with this role</Trans>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,18 +215,12 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('truncate text-foreground/70', {
|
||||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||||
'opacity-50': isRecipientDisabled(recipient.id),
|
'opacity-50': isRecipientDisabled(recipient.id),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name && (
|
{getRecipientLabel(recipient)}
|
||||||
<span title={`${recipient.name} (${recipient.email})`}>
|
|
||||||
{recipient.name} ({recipient.email})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center justify-center">
|
<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" />
|
<Info className="z-50 ml-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
This document has already been sent to this recipient. You can no longer
|
This document has already been sent to this recipient. You can no longer
|
||||||
edit this recipient.
|
edit this recipient.
|
||||||
@@ -250,3 +254,22 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
</Command>
|
</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}`);
|
||||||
|
};
|
||||||
|
|||||||
+16
-4
@@ -80,12 +80,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const handleOnCompleteClick = async (
|
const handleOnCompleteClick = async (
|
||||||
nextSigner?: { name: string; email: string },
|
nextSigner?: { name: string; email: string },
|
||||||
accessAuthOptions?: TRecipientAccessAuth,
|
accessAuthOptions?: TRecipientAccessAuth,
|
||||||
|
recipientDetails?: { name: string; email: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await completeDocument({
|
await completeDocument({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
|
recipientOverride: recipientDetails,
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,21 +207,30 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const directTemplatePayload = useMemo(() => {
|
const recipientPayload = useMemo(() => {
|
||||||
if (!isDirectTemplate) {
|
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 {
|
return {
|
||||||
name: fullName,
|
name: fullName,
|
||||||
email: email,
|
email: email,
|
||||||
};
|
};
|
||||||
}, [email, fullName, isDirectTemplate]);
|
}, [email, fullName, isDirectTemplate, recipient.email, recipient.name, recipient.fields]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentSigningCompleteDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isPending}
|
isSubmitting={isPending}
|
||||||
directTemplatePayload={directTemplatePayload}
|
recipientPayload={recipientPayload}
|
||||||
onSignatureComplete={
|
onSignatureComplete={
|
||||||
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
||||||
}
|
}
|
||||||
@@ -230,6 +241,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
allowDictateNextSigner={Boolean(
|
allowDictateNextSigner={Boolean(
|
||||||
nextRecipient && envelope.documentMeta.allowDictateNextSigner,
|
nextRecipient && envelope.documentMeta.allowDictateNextSigner,
|
||||||
)}
|
)}
|
||||||
|
disableNameInput={!isDirectTemplate && recipient.name !== ''}
|
||||||
defaultNextSigner={
|
defaultNextSigner={
|
||||||
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,8 +107,8 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { HydratedRouter } from 'react-router/dom';
|
|||||||
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
|
|
||||||
|
import './utils/polyfills/promise-with-resolvers';
|
||||||
|
|
||||||
function PosthogInit() {
|
function PosthogInit() {
|
||||||
const postHogConfig = extractPostHogConfig();
|
const postHogConfig = extractPostHogConfig();
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -46,12 +46,16 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const { getTheme } = await themeSessionResolver(request);
|
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)) {
|
if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
|
||||||
lang = extractLocaleData({ headers: request.headers }).lang;
|
lang = extractLocaleData({ headers: request.headers }).lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableAnimations = cookieHeader.includes('__disable_animations=true');
|
||||||
|
|
||||||
let organisations = null;
|
let organisations = null;
|
||||||
|
|
||||||
if (session.isAuthenticated) {
|
if (session.isAuthenticated) {
|
||||||
@@ -62,6 +66,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
{
|
{
|
||||||
lang,
|
lang,
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
|
disableAnimations,
|
||||||
session: session.isAuthenticated
|
session: session.isAuthenticated
|
||||||
? {
|
? {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
@@ -92,7 +97,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutContent({ 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();
|
const [theme] = useTheme();
|
||||||
|
|
||||||
@@ -111,6 +117,14 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
|
|||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
<PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
|
<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 */}
|
{/* Fix: https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer */}
|
||||||
<script>0</script>
|
<script>0</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import DocumentPage, { meta } from '../../o.$orgUrl.settings.document';
|
import DocumentPage, { loader, meta } from '../../o.$orgUrl.settings.document';
|
||||||
|
|
||||||
export { meta };
|
export { meta, loader };
|
||||||
|
|
||||||
export default DocumentPage;
|
export default DocumentPage;
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export const loader = () => {
|
|||||||
export default function TeamsSettingsPage() {
|
export default function TeamsSettingsPage() {
|
||||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
console.log('isAiFeaturesConfigured', isAiFeaturesConfigured);
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
const token = url.searchParams.get('token') || '';
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
// We also know that the token is valid, but we need the userId + teamId
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
const result = await verifyEmbeddingPresignToken({ token, scope: `documentId:${id}` }).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
const token = url.searchParams.get('token') || '';
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
// We also know that the token is valid, but we need the userId + teamId
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
const result = await verifyEmbeddingPresignToken({ token, scope: `templateId:${id}` }).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Polyfill for Promise.withResolvers (ES2024)
|
||||||
|
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PromiseWithResolvers<T> = {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void;
|
||||||
|
reject: (reason?: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We're patching here
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const GlobalPromise = globalThis.Promise as any;
|
||||||
|
|
||||||
|
if (typeof GlobalPromise.withResolvers !== 'function') {
|
||||||
|
GlobalPromise.withResolvers = function <T>(): PromiseWithResolvers<T> {
|
||||||
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||||
|
let reject!: (reason?: unknown) => void;
|
||||||
|
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -108,5 +108,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "2.2.2"
|
"version": "2.2.5"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+277
-533
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.2",
|
"version": "2.2.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.2",
|
"version": "2.2.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "2.2.2",
|
"version": "2.2.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.3",
|
"@cantoo/pdf-lib": "^2.5.3",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -666,58 +666,6 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aws-sdk/client-ses": {
|
|
||||||
"version": "3.936.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.936.0.tgz",
|
|
||||||
"integrity": "sha512-2toHYwRkcYGasPHYGwOwaIAa2Api/uFhmL3px0Tyt4bne2ilqhSwq+6a/0UVMd8JYwWaLMJolTbWKFt2jUlmGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-crypto/sha256-browser": "5.2.0",
|
|
||||||
"@aws-crypto/sha256-js": "5.2.0",
|
|
||||||
"@aws-sdk/core": "3.936.0",
|
|
||||||
"@aws-sdk/credential-provider-node": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-host-header": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-logger": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-recursion-detection": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-user-agent": "3.936.0",
|
|
||||||
"@aws-sdk/region-config-resolver": "3.936.0",
|
|
||||||
"@aws-sdk/types": "3.936.0",
|
|
||||||
"@aws-sdk/util-endpoints": "3.936.0",
|
|
||||||
"@aws-sdk/util-user-agent-browser": "3.936.0",
|
|
||||||
"@aws-sdk/util-user-agent-node": "3.936.0",
|
|
||||||
"@smithy/config-resolver": "^4.4.3",
|
|
||||||
"@smithy/core": "^3.18.5",
|
|
||||||
"@smithy/fetch-http-handler": "^5.3.6",
|
|
||||||
"@smithy/hash-node": "^4.2.5",
|
|
||||||
"@smithy/invalid-dependency": "^4.2.5",
|
|
||||||
"@smithy/middleware-content-length": "^4.2.5",
|
|
||||||
"@smithy/middleware-endpoint": "^4.3.12",
|
|
||||||
"@smithy/middleware-retry": "^4.4.12",
|
|
||||||
"@smithy/middleware-serde": "^4.2.6",
|
|
||||||
"@smithy/middleware-stack": "^4.2.5",
|
|
||||||
"@smithy/node-config-provider": "^4.3.5",
|
|
||||||
"@smithy/node-http-handler": "^4.4.5",
|
|
||||||
"@smithy/protocol-http": "^5.3.5",
|
|
||||||
"@smithy/smithy-client": "^4.9.8",
|
|
||||||
"@smithy/types": "^4.9.0",
|
|
||||||
"@smithy/url-parser": "^4.2.5",
|
|
||||||
"@smithy/util-base64": "^4.3.0",
|
|
||||||
"@smithy/util-body-length-browser": "^4.2.0",
|
|
||||||
"@smithy/util-body-length-node": "^4.2.1",
|
|
||||||
"@smithy/util-defaults-mode-browser": "^4.3.11",
|
|
||||||
"@smithy/util-defaults-mode-node": "^4.2.14",
|
|
||||||
"@smithy/util-endpoints": "^3.2.5",
|
|
||||||
"@smithy/util-middleware": "^4.2.5",
|
|
||||||
"@smithy/util-retry": "^4.2.5",
|
|
||||||
"@smithy/util-utf8": "^4.2.0",
|
|
||||||
"@smithy/util-waiter": "^4.2.5",
|
|
||||||
"tslib": "^2.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@aws-sdk/client-sesv2": {
|
"node_modules/@aws-sdk/client-sesv2": {
|
||||||
"version": "3.936.0",
|
"version": "3.936.0",
|
||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.936.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.936.0.tgz",
|
||||||
@@ -5963,12 +5911,6 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@one-ini/wasm": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
@@ -12161,6 +12103,7 @@
|
|||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -14407,21 +14350,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/render": {
|
"node_modules/@react-email/render": {
|
||||||
"version": "0.0.17",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz",
|
||||||
"integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==",
|
"integrity": "sha512-rdjNj6iVzv8kRKDPFas+47nnoe6B40+nwukuXwY4FCwM7XBg6tmYr+chQryCuavUj2J65MMf6fztk1bxOUiSVA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"js-beautify": "^1.14.11",
|
"prettier": "^3.5.3"
|
||||||
"react-promise-suspense": "0.3.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/row": {
|
"node_modules/@react-email/row": {
|
||||||
@@ -17107,13 +17049,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/nodemailer": {
|
"node_modules/@types/nodemailer": {
|
||||||
"version": "6.4.21",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz",
|
||||||
"integrity": "sha512-Eix+sb/Nj28MNnWvO2X1OLrk5vuD4C9SMnb2Vf4itWnxphYeSceqkFX7IdmxTzn+dvmnNz7paMbg4Uc60wSfJg==",
|
"integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-ses": "^3.731.1",
|
"@aws-sdk/client-sesv2": "^3.839.0",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17787,15 +17729,6 @@
|
|||||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -19861,6 +19794,7 @@
|
|||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -20063,22 +19997,6 @@
|
|||||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/config-chain": {
|
|
||||||
"version": "1.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
|
||||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ini": "^1.3.4",
|
|
||||||
"proto-list": "~1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/config-chain/node_modules/ini": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
@@ -21571,48 +21489,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/editorconfig": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@one-ini/wasm": "0.1.1",
|
|
||||||
"commander": "^10.0.0",
|
|
||||||
"minimatch": "9.0.1",
|
|
||||||
"semver": "^7.5.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"editorconfig": "bin/editorconfig"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig/node_modules/minimatch": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -26314,117 +26190,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-beautify": {
|
|
||||||
"version": "1.15.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
|
|
||||||
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"config-chain": "^1.1.13",
|
|
||||||
"editorconfig": "^1.0.4",
|
|
||||||
"glob": "^10.4.2",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"nopt": "^7.2.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"css-beautify": "js/bin/css-beautify.js",
|
|
||||||
"html-beautify": "js/bin/html-beautify.js",
|
|
||||||
"js-beautify": "js/bin/js-beautify.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/glob": {
|
|
||||||
"version": "10.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
|
||||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"foreground-child": "^3.1.0",
|
|
||||||
"jackspeak": "^3.1.2",
|
|
||||||
"minimatch": "^9.0.4",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"package-json-from-dist": "^1.0.0",
|
|
||||||
"path-scurry": "^1.11.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"glob": "dist/esm/bin.mjs"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/jackspeak": {
|
|
||||||
"version": "3.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
|
||||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/cliui": "^8.0.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/lru-cache": {
|
|
||||||
"version": "10.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/minimatch": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/path-scurry": {
|
|
||||||
"version": "1.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
|
||||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^10.2.0",
|
|
||||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-sdsl": {
|
"node_modules/js-sdsl": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
@@ -29480,21 +29245,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nopt": {
|
|
||||||
"version": "7.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
|
|
||||||
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/normalize-package-data": {
|
"node_modules/normalize-package-data": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
|
||||||
@@ -31586,12 +31336,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proto-list": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
@@ -31927,263 +31671,6 @@
|
|||||||
"react": ">= 16.8 || 18.0.0"
|
"react": ">= 16.8 || 18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-email": {
|
|
||||||
"version": "5.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.4.tgz",
|
|
||||||
"integrity": "sha512-pI6b4ePMWC1AFtyd0gyVMsJ6Nhg4nnmDxVo1EQP+r011WQafU8UGAUfPUiE5Wh7x28+3yFw7qXCnD33y71guUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.27.0",
|
|
||||||
"@babel/traverse": "^7.27.0",
|
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"commander": "^13.0.0",
|
|
||||||
"conf": "^15.0.2",
|
|
||||||
"debounce": "^2.0.0",
|
|
||||||
"esbuild": "^0.25.0",
|
|
||||||
"glob": "^11.0.0",
|
|
||||||
"jiti": "2.4.2",
|
|
||||||
"log-symbols": "^7.0.0",
|
|
||||||
"mime-types": "^3.0.0",
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"nypm": "0.6.0",
|
|
||||||
"ora": "^8.0.0",
|
|
||||||
"prompts": "2.4.2",
|
|
||||||
"socket.io": "^4.8.1",
|
|
||||||
"tsconfig-paths": "4.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"email": "dist/index.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ansi-regex": {
|
|
||||||
"version": "6.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
|
||||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/chokidar": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.16.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/commander": {
|
|
||||||
"version": "13.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
|
||||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/emoji-regex": {
|
|
||||||
"version": "10.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
|
||||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/is-interactive": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/is-unicode-supported": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/jiti": {
|
|
||||||
"version": "2.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/log-symbols": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-unicode-supported": "^2.0.0",
|
|
||||||
"yoctocolors": "^2.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"cli-cursor": "^5.0.0",
|
|
||||||
"cli-spinners": "^2.9.2",
|
|
||||||
"is-interactive": "^2.0.0",
|
|
||||||
"is-unicode-supported": "^2.0.0",
|
|
||||||
"log-symbols": "^6.0.0",
|
|
||||||
"stdin-discarder": "^0.2.2",
|
|
||||||
"string-width": "^7.2.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora/node_modules/log-symbols": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"is-unicode-supported": "^1.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/readdirp": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/string-width": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"emoji-regex": "^10.3.0",
|
|
||||||
"get-east-asian-width": "^1.0.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/strip-ansi": {
|
|
||||||
"version": "7.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/tsconfig-paths": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json5": "^2.2.2",
|
|
||||||
"minimist": "^1.2.6",
|
|
||||||
"strip-bom": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.66.1",
|
"version": "7.66.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz",
|
||||||
@@ -37453,18 +36940,275 @@
|
|||||||
"@react-email/img": "0.0.11",
|
"@react-email/img": "0.0.11",
|
||||||
"@react-email/link": "0.0.12",
|
"@react-email/link": "0.0.12",
|
||||||
"@react-email/preview": "0.0.13",
|
"@react-email/preview": "0.0.13",
|
||||||
"@react-email/render": "0.0.17",
|
"@react-email/render": "2.0.0",
|
||||||
"@react-email/row": "0.0.12",
|
"@react-email/row": "0.0.12",
|
||||||
"@react-email/section": "0.0.16",
|
"@react-email/section": "0.0.16",
|
||||||
"@react-email/tailwind": "^2.0.1",
|
"@react-email/tailwind": "^2.0.1",
|
||||||
"@react-email/text": "0.1.5",
|
"@react-email/text": "0.1.5",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"react-email": "^5.0.4",
|
"react-email": "^5.0.6",
|
||||||
"resend": "^6.5.2"
|
"resend": "^6.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.21"
|
"@types/nodemailer": "^7.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/commander": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/is-interactive": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/is-unicode-supported": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/jiti": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/log-symbols": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"yoctocolors": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-cursor": "^5.0.0",
|
||||||
|
"cli-spinners": "^2.9.2",
|
||||||
|
"is-interactive": "^2.0.0",
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"log-symbols": "^6.0.0",
|
||||||
|
"stdin-discarder": "^0.2.2",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora/node_modules/log-symbols": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"is-unicode-supported": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/react-email": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.27.0",
|
||||||
|
"@babel/traverse": "^7.27.0",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"commander": "^13.0.0",
|
||||||
|
"conf": "^15.0.2",
|
||||||
|
"debounce": "^2.0.0",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"glob": "^11.0.0",
|
||||||
|
"jiti": "2.4.2",
|
||||||
|
"log-symbols": "^7.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"nypm": "0.6.0",
|
||||||
|
"ora": "^8.0.0",
|
||||||
|
"prompts": "2.4.2",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"tsconfig-paths": "4.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"email": "dist/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/tsconfig-paths": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json5": "^2.2.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"strip-bom": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/eslint-config": {
|
"packages/eslint-config": {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "2.2.2",
|
"version": "2.2.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import type { APIRequestContext } from 'playwright-core';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import type { CreateEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
|
||||||
|
import type { VerifyEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
@@ -17,18 +20,7 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token);
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -54,19 +46,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
expiresIn: 120, // 2 hours
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
expiresIn: 120, // 2 hours
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -92,19 +74,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
expiresIn: 0, // Immediate expiration
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
expiresIn: 0, // Immediate expiration
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
@@ -129,18 +101,7 @@ test.describe('Embedding Presign API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// First create a token
|
// First create a token
|
||||||
const createResponse = await request.post(
|
const createResponse = await createPresignToken(request, token);
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(createResponse.ok()).toBeTruthy();
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
const createResponseData = await createResponse.json();
|
const createResponseData = await createResponse.json();
|
||||||
@@ -150,18 +111,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
const presignToken = createResponseData.token;
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
// Then verify it
|
// Then verify it
|
||||||
const verifyResponse = await request.post(
|
const verifyResponse = await verifyPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
token: presignToken,
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
token: presignToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(verifyResponse.ok()).toBeTruthy();
|
expect(verifyResponse.ok()).toBeTruthy();
|
||||||
expect(verifyResponse.status()).toBe(200);
|
expect(verifyResponse.status()).toBe(200);
|
||||||
@@ -183,18 +135,87 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await verifyPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
token: 'invalid-token',
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
const responseData = await response.json();
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
console.log('Invalid token response:', responseData);
|
||||||
data: {
|
|
||||||
token: 'invalid-token',
|
expect(response.ok()).toBeTruthy();
|
||||||
},
|
expect(response.status()).toBe(200);
|
||||||
},
|
|
||||||
);
|
expect(responseData.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyEmbeddingPresignToken: should verify a valid scoped token', async ({ request }) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create a token
|
||||||
|
const createResponse = await createPresignToken(request, token, {
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
|
const createResponseData = await createResponse.json();
|
||||||
|
|
||||||
|
console.log('Create response:', createResponseData);
|
||||||
|
|
||||||
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
|
// Then verify it
|
||||||
|
const verifyResponse = await verifyPresignToken(request, token, {
|
||||||
|
token: presignToken,
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(verifyResponse.ok()).toBeTruthy();
|
||||||
|
expect(verifyResponse.status()).toBe(200);
|
||||||
|
|
||||||
|
const verifyResponseData = await verifyResponse.json();
|
||||||
|
|
||||||
|
console.log('Verify response:', verifyResponseData);
|
||||||
|
|
||||||
|
expect(verifyResponseData.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyEmbeddingPresignToken: should reject a scope mismatched token', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create a token
|
||||||
|
const createResponse = await createPresignToken(request, token, {
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
|
const createResponseData = await createResponse.json();
|
||||||
|
|
||||||
|
console.log('Create response:', createResponseData);
|
||||||
|
|
||||||
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
|
// Then verify it
|
||||||
|
const response = await verifyPresignToken(request, token, {
|
||||||
|
token: presignToken,
|
||||||
|
scope: 'documentId:2',
|
||||||
|
});
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -206,3 +227,40 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expect(responseData.success).toBe(false);
|
expect(responseData.success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createPresignToken = async (
|
||||||
|
request: APIRequestContext,
|
||||||
|
apiToken: string,
|
||||||
|
data?: Partial<CreateEmbeddingPresignTokenOptions>,
|
||||||
|
) => {
|
||||||
|
return await request.post(
|
||||||
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
apiToken,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyPresignToken = async (
|
||||||
|
request: APIRequestContext,
|
||||||
|
apiToken: string,
|
||||||
|
data: VerifyEmbeddingPresignTokenOptions,
|
||||||
|
) => {
|
||||||
|
return await request.post(
|
||||||
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { pick } from 'remeda';
|
|||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||||
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentDistributionMethod,
|
DocumentDistributionMethod,
|
||||||
@@ -23,7 +24,9 @@ import type {
|
|||||||
TCreateEnvelopePayload,
|
TCreateEnvelopePayload,
|
||||||
TCreateEnvelopeResponse,
|
TCreateEnvelopeResponse,
|
||||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||||
|
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
|
||||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||||
|
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
|
||||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||||
|
|
||||||
@@ -144,6 +147,9 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
externalId: 'externalId',
|
externalId: 'externalId',
|
||||||
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||||
globalAccessAuth: ['ACCOUNT'],
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
|
// Ignore this error in the test since it doesn't actually exist in the PDF:
|
||||||
|
// - Error setting value for field hello: PDFDocument has no form field with the name "hello"
|
||||||
|
// We want to check if the form value is set in the DB.
|
||||||
formValues: {
|
formValues: {
|
||||||
hello: 'world',
|
hello: 'world',
|
||||||
},
|
},
|
||||||
@@ -262,8 +268,6 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(userB.email);
|
|
||||||
|
|
||||||
expect(envelope.envelopeItems.length).toBe(2);
|
expect(envelope.envelopeItems.length).toBe(2);
|
||||||
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
|
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
|
||||||
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
|
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
|
||||||
@@ -557,4 +561,543 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
userEmail: userA.email,
|
userEmail: userA.email,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Empty recipient tests', () => {
|
||||||
|
test('Create template envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Template with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Test Recipient',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
expect(createRecipientsRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Test Recipient');
|
||||||
|
|
||||||
|
// Get envelope items to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient with empty email
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
expect(createFieldsRes.status()).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create document envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Document Recipient No Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Document Recipient No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update recipient to have empty email', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Update Recipient Email Test',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Test User',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Get the envelope to get recipient ID
|
||||||
|
const getRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getRes.json();
|
||||||
|
const recipientId = envelope.recipients[0].id;
|
||||||
|
|
||||||
|
// Update recipient to have empty email
|
||||||
|
const updateRequest: TUpdateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: recipientId,
|
||||||
|
email: '',
|
||||||
|
name: 'Updated Name No Email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRes = await request.post(`${baseUrl}/envelope/recipient/update-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: updateRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateRes.ok()).toBeTruthy();
|
||||||
|
const updateResponse = await updateRes.json();
|
||||||
|
const updatedRecipient = updateResponse.data[0];
|
||||||
|
|
||||||
|
expect(updatedRecipient.email).toBe('');
|
||||||
|
expect(updatedRecipient.name).toBe('Updated Name No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed recipients with and without emails', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Mixed Recipients Test',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create multiple recipients, some with email, some without
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Recipient With Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 1',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: userB.email,
|
||||||
|
name: 'Another With Email',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 2',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
expect(recipients.length).toBe(4);
|
||||||
|
expect(recipients[0].email).toBe(userA.email.toLowerCase());
|
||||||
|
expect(recipients[1].email).toBe('');
|
||||||
|
expect(recipients[2].email).toBe(userB.email.toLowerCase());
|
||||||
|
expect(recipients[3].email).toBe('');
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for all recipients including those without emails
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipients', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document for Distribution with Empty Email',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipients with empty emails
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient One',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Two',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for recipients
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Distribute the envelope
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
} satisfies TDistributeEnvelopeRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(distributeRes.ok()).toBeTruthy();
|
||||||
|
expect(distributeRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const distributeResponse = await distributeRes.json();
|
||||||
|
expect(distributeResponse.success).toBe(true);
|
||||||
|
expect(distributeResponse.id).toBe(createResponse.id);
|
||||||
|
expect(distributeResponse.recipients).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify recipients have empty emails and signing URLs
|
||||||
|
expect(distributeResponse.recipients[0].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[0].signingUrl).toBeTruthy();
|
||||||
|
expect(distributeResponse.recipients[1].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[1].signingUrl).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipient and auth requirements fails', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Auth Requirements',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email and TWO_FACTOR_AUTH action auth
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient With Auth',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [DocumentAccessAuth.TWO_FACTOR_AUTH],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Try to distribute the envelope - should fail
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expect distribution to fail
|
||||||
|
expect(distributeRes.ok()).toBeFalsy();
|
||||||
|
expect(distributeRes.status()).toBe(400);
|
||||||
|
|
||||||
|
const errorResponse = await distributeRes.json();
|
||||||
|
expect(errorResponse.message).toContain('requires an email');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3103,7 +3103,7 @@ test.describe('Document API V2', () => {
|
|||||||
expect(res.status()).toBe(404);
|
expect(res.status()).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow authorized access to envelope use endpoint', async ({ page, request }) => {
|
test('should allow authorized access to envelope use endpoint', async ({ request }) => {
|
||||||
const doc = await seedTemplate({
|
const doc = await seedTemplate({
|
||||||
title: 'Team template 1',
|
title: 'Team template 1',
|
||||||
userId: userA.id,
|
userId: userA.id,
|
||||||
@@ -4313,5 +4313,62 @@ test.describe('Document API V2', () => {
|
|||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Envelope audit logs endpoint', () => {
|
||||||
|
test('should block unauthorized access to envelope audit logs endpoint', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.get(
|
||||||
|
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/${doc.id}/audit-log`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.ok()).toBeFalsy();
|
||||||
|
expect(res.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to envelope audit logs endpoint', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
|
// Add a recipient which will trigger an audit log.
|
||||||
|
await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: doc.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'Test',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request.get(
|
||||||
|
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/${doc.id}/audit-log`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
expect(Array.isArray(data.data)).toBe(true);
|
||||||
|
expect(data.count).toEqual(1);
|
||||||
|
expect(data.data[0].type).toEqual('RECIPIENT_CREATED');
|
||||||
|
expect(data.currentPage).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(data.perPage).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(3);
|
expect(retrievedFields.length).toBe(3);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
expect(retrievedFields.filter((field) => field.type === 'SIGNATURE')).toHaveLength(2);
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
expect(retrievedFields.filter((field) => field.type === 'TEXT')).toHaveLength(1);
|
||||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
|
||||||
}).toPass();
|
}).toPass();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,10 +213,8 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(4);
|
expect(retrievedFields.length).toBe(4);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
expect(retrievedFields.filter((field) => field.type === 'SIGNATURE')).toHaveLength(3);
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
expect(retrievedFields.filter((field) => field.type === 'TEXT')).toHaveLength(1);
|
||||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
|
||||||
expect(retrievedFields[3].type).toBe('SIGNATURE');
|
|
||||||
}).toPass();
|
}).toPass();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,10 +256,16 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(2);
|
expect(retrievedFields.length).toBe(2);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
const textField = retrievedFields.find((field) => field.type === 'TEXT');
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
const signatureField = retrievedFields.find((field) => field.type === 'SIGNATURE');
|
||||||
|
|
||||||
|
expect(signatureField).toBeDefined();
|
||||||
|
expect(textField).toBeDefined();
|
||||||
|
|
||||||
|
if (!signatureField || !textField) {
|
||||||
|
throw new Error('No signature or text field');
|
||||||
|
}
|
||||||
|
|
||||||
const textField = retrievedFields[1];
|
|
||||||
expect(textField.fieldMeta).toBeDefined();
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { seedUser } from '@documenso/prisma/seed/users';
|
|||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||||
|
import { expectToastTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'serial' });
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
@@ -83,14 +84,13 @@ test('[DOCUMENTS]: deleting a completed document should not remove it from recip
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -128,14 +128,13 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients'
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -169,19 +168,17 @@ test('[DOCUMENTS]: deleting draft documents should permanently remove it', async
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Draft' })
|
.locator('tr', { hasText: 'Document 1 - Draft' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); // Required to reduce flakiness.
|
||||||
|
|
||||||
// delete document
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible();
|
await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.waitForTimeout(2500);
|
await expectToastTextToBeVisible(page, 'Document deleted');
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
||||||
|
|
||||||
@@ -203,14 +200,13 @@ test('[DOCUMENTS]: deleting pending documents should permanently remove it', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -239,14 +235,13 @@ test('[DOCUMENTS]: deleting completed documents as an owner should hide it from
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -292,36 +287,24 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await expect(async () => {
|
const completedDocActionBtn = page
|
||||||
await page
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.getByTestId('document-table-action-btn');
|
||||||
.getByTestId('document-table-action-btn')
|
await openDropdownMenu(page, completedDocActionBtn);
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
||||||
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
await expect(async () => {
|
const pendingDocActionBtn = page
|
||||||
await page
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.getByTestId('document-table-action-btn');
|
||||||
.getByTestId('document-table-action-btn')
|
await openDropdownMenu(page, pendingDocActionBtn);
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
||||||
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Locator } from '@playwright/test';
|
||||||
import { type Page, expect } from '@playwright/test';
|
import { type Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
export const expectTextToBeVisible = async (page: Page, text: string) => {
|
export const expectTextToBeVisible = async (page: Page, text: string) => {
|
||||||
@@ -7,3 +8,22 @@ export const expectTextToBeVisible = async (page: Page, text: string) => {
|
|||||||
export const expectTextToNotBeVisible = async (page: Page, text: string) => {
|
export const expectTextToNotBeVisible = async (page: Page, text: string) => {
|
||||||
await expect(page.getByText(text).first()).not.toBeVisible();
|
await expect(page.getByText(text).first()).not.toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const expectToastTextToBeVisible = async (page: Page, text: string) => {
|
||||||
|
await expect(page.locator('[role="status"]').getByText(text)).toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openDropdownMenu = async (page: Page, dropdownButton: Locator) => {
|
||||||
|
await page.waitForTimeout(500); // Initial timeout incase table remounts which will close the dropdown.
|
||||||
|
await dropdownButton.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await dropdownButton.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem').first()).toBeVisible();
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { seedTeamMember } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import { expectTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
@@ -117,7 +117,9 @@ test('[TEAMS]: can pin a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -142,7 +144,9 @@ test('[TEAMS]: can unpin a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Unpin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -166,7 +170,9 @@ test('[TEAMS]: can rename a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await page.getByLabel('Name').fill('Team Archive');
|
await page.getByLabel('Name').fill('Team Archive');
|
||||||
@@ -191,7 +197,9 @@ test('[TEAMS]: document folder visibility is visible to team member', async ({ p
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible();
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible();
|
||||||
@@ -220,7 +228,9 @@ test('[TEAMS]: document folder can be moved to another document folder', async (
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Team Clients' }).click();
|
await page.getByRole('button', { name: 'Team Clients' }).click();
|
||||||
@@ -271,7 +281,9 @@ test('[TEAMS]: document folder and its contents can be deleted', async ({ page }
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
||||||
@@ -280,12 +292,8 @@ test('[TEAMS]: document folder and its contents can be deleted', async ({ page }
|
|||||||
await page.goto(`/t/${team.url}/documents`);
|
await page.goto(`/t/${team.url}/documents`);
|
||||||
|
|
||||||
await expect(page.locator(`[data-folder-id="${folder.id}"]`)).not.toBeVisible();
|
await expect(page.locator(`[data-folder-id="${folder.id}"]`)).not.toBeVisible();
|
||||||
await expect(page.getByText(proposal.title)).not.toBeVisible();
|
await expect(page.getByText(proposal.title)).toBeVisible();
|
||||||
|
await expect(page.getByText(report.title)).toBeVisible();
|
||||||
await page.goto(`/t/${team.url}/documents/f/${folder.id}`);
|
|
||||||
|
|
||||||
await expect(page.getByText(report.title)).not.toBeVisible();
|
|
||||||
await expect(page.locator(`[data-folder-id="${reportsFolder.id}"]`)).not.toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: create folder button is visible on templates page', async ({ page }) => {
|
test('[TEAMS]: create folder button is visible on templates page', async ({ page }) => {
|
||||||
@@ -410,7 +418,9 @@ test('[TEAMS]: can pin a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -436,7 +446,9 @@ test('[TEAMS]: can unpin a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Unpin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -462,7 +474,9 @@ test('[TEAMS]: can rename a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await page.getByLabel('Name').fill('Updated Team Template Folder');
|
await page.getByLabel('Name').fill('Updated Team Template Folder');
|
||||||
@@ -488,7 +502,9 @@ test('[TEAMS]: template folder visibility is not visible to team member', async
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible();
|
||||||
@@ -519,7 +535,9 @@ test('[TEAMS]: template folder can be moved to another template folder', async (
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Team Client Templates' }).click();
|
await page.getByRole('button', { name: 'Team Client Templates' }).click();
|
||||||
@@ -572,7 +590,9 @@ test('[TEAMS]: template folder can be deleted', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
||||||
@@ -761,7 +781,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents/`);
|
await page.goto(`/t/${team.url}/documents/`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn1 = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn1);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only');
|
||||||
@@ -781,7 +803,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents`);
|
await page.goto(`/t/${team.url}/documents`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn2 = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn2);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above');
|
||||||
@@ -801,7 +825,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents/`);
|
await page.goto(`/t/${team.url}/documents/`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn3 = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn3);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone');
|
||||||
@@ -966,7 +992,9 @@ test('[TEAMS]: team member can move documents to everyone folder', async ({ page
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1018,7 +1046,9 @@ test('[TEAMS]: team manager can move manager document to manager folder', async
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1071,7 +1101,9 @@ test('[TEAMS]: team manager can move manager document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1124,7 +1156,9 @@ test('[TEAMS]: team manager can move everyone document to manager folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1177,7 +1211,9 @@ test('[TEAMS]: team admin can move admin document to admin folder', async ({ pag
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1228,7 +1264,9 @@ test('[TEAMS]: team admin can move admin document to manager folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1279,7 +1317,9 @@ test('[TEAMS]: team admin can move admin document to everyone folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1330,7 +1370,9 @@ test('[TEAMS]: team admin can move manager document to admin folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1381,7 +1423,9 @@ test('[TEAMS]: team admin can move manager document to manager folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1432,7 +1476,9 @@ test('[TEAMS]: team admin can move manager document to everyone folder', async (
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1483,7 +1529,9 @@ test('[TEAMS]: team admin can move everyone document to admin folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1534,7 +1582,9 @@ test('[TEAMS]: team admin can move everyone document to manager folder', async (
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1585,7 +1635,9 @@ test('[TEAMS]: team admin can move everyone document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1630,7 +1682,9 @@ test('[TEAMS]: team owner can move admin document to admin folder', async ({ pag
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1675,7 +1729,9 @@ test('[TEAMS]: team owner can move admin document to manager folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1720,7 +1776,9 @@ test('[TEAMS]: team owner can move admin document to everyone folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1765,7 +1823,9 @@ test('[TEAMS]: team owner can move manager document to admin folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1810,7 +1870,9 @@ test('[TEAMS]: team owner can move manager document to manager folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1855,7 +1917,9 @@ test('[TEAMS]: team owner can move manager document to everyone folder', async (
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1900,7 +1964,9 @@ test('[TEAMS]: team owner can move everyone document to admin folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1945,7 +2011,9 @@ test('[TEAMS]: team owner can move everyone document to manager folder', async (
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1990,7 +2058,9 @@ test('[TEAMS]: team owner can move everyone document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible, expectTextToNotBeVisible } from '../fixtures/generic';
|
import {
|
||||||
|
expectTextToBeVisible,
|
||||||
|
expectTextToNotBeVisible,
|
||||||
|
openDropdownMenu,
|
||||||
|
} from '../fixtures/generic';
|
||||||
|
|
||||||
test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
|
test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
|
||||||
const { user, organisation } = await seedUser({
|
const { user, organisation } = await seedUser({
|
||||||
@@ -399,7 +403,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
|
|||||||
await expect(page.getByText('Team members have been added').first()).toBeVisible();
|
await expect(page.getByText('Team members have been added').first()).toBeVisible();
|
||||||
|
|
||||||
// Update CUSTOM_GROUP_B
|
// Update CUSTOM_GROUP_B
|
||||||
await page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button').click();
|
const updateBtn = page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, updateBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Update role' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Update role' }).click();
|
await page.getByRole('menuitem', { name: 'Update role' }).click();
|
||||||
await page.getByRole('combobox').click();
|
await page.getByRole('combobox').click();
|
||||||
await page.getByRole('option', { name: 'Team Admin' }).click();
|
await page.getByRole('option', { name: 'Team Admin' }).click();
|
||||||
@@ -409,7 +415,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
|
|||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
// Delete CUSTOM_GROUP_B
|
// Delete CUSTOM_GROUP_B
|
||||||
await page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button').click();
|
const deleteBtn = page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, deleteBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expectTextToBeVisible(page, 'You have successfully removed this group from the team.');
|
await expectTextToBeVisible(page, 'You have successfully removed this group from the team.');
|
||||||
@@ -477,7 +485,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
await expect(page.getByText(user3.email)).toBeVisible();
|
await expect(page.getByText(user3.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user3.email }).getByRole('button').click();
|
const inviteActionBtn = page.getByRole('row', { name: user3.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, inviteActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await expect(page.getByText('Invitation has been deleted').first()).toBeVisible();
|
await expect(page.getByText('Invitation has been deleted').first()).toBeVisible();
|
||||||
await expect(page.getByText(user3.email)).not.toBeVisible();
|
await expect(page.getByText(user3.email)).not.toBeVisible();
|
||||||
@@ -508,7 +518,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user.email)).toBeVisible();
|
await expect(page.getByText(user.email)).toBeVisible();
|
||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user2.email }).getByRole('button').click();
|
const memberActionBtn = page.getByRole('row', { name: user2.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, memberActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Remove' }).click();
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
await expect(page.getByText('You have successfully removed').first()).toBeVisible();
|
await expect(page.getByText('You have successfully removed').first()).toBeVisible();
|
||||||
@@ -522,7 +534,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user.email)).toBeVisible();
|
await expect(page.getByText(user.email)).toBeVisible();
|
||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user2.email }).getByRole('button').click();
|
const orgMemberBtn = page.getByRole('row', { name: user2.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, orgMemberBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expect(page.getByText('You have successfully removed this user').first()).toBeVisible();
|
await expect(page.getByText('You have successfully removed this user').first()).toBeVisible();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
@@ -44,6 +46,9 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
|||||||
.fill('public-direct-template-description');
|
.fill('public-direct-template-description');
|
||||||
await page.getByRole('button', { name: 'Update' }).click();
|
await page.getByRole('button', { name: 'Update' }).click();
|
||||||
|
|
||||||
|
// Wait for toast
|
||||||
|
await expectToastTextToBeVisible(page, 'Template has been updated');
|
||||||
|
|
||||||
// Check that public profile is disabled.
|
// Check that public profile is disabled.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||||
await expect(page.locator('body')).toContainText('404 Profile not found');
|
await expect(page.locator('body')).toContainText('404 Profile not found');
|
||||||
@@ -51,7 +56,21 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
|||||||
// Go back to public profile page.
|
// Go back to public profile page.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
||||||
await page.getByRole('switch').click();
|
await page.getByRole('switch').click();
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
// Expect profile to be enabled via db.
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const profile = await prisma.teamProfile.findFirst({
|
||||||
|
where: { teamId: team.id },
|
||||||
|
});
|
||||||
|
return profile?.enabled;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 1000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
// Assert values.
|
// Assert values.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import { seedUser } from '@documenso/prisma/seed/users';
|
|||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import {
|
||||||
|
expectTextToBeVisible,
|
||||||
|
expectToastTextToBeVisible,
|
||||||
|
openDropdownMenu,
|
||||||
|
} from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEAMS]: check team documents count', async ({ page }) => {
|
test('[TEAMS]: check team documents count', async ({ page }) => {
|
||||||
const { team, teamOwner, teamMember2 } = await seedTeamDocuments();
|
const { team, teamOwner, teamMember2 } = await seedTeamDocuments();
|
||||||
@@ -239,21 +243,15 @@ test('[TEAMS]: resend pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeAttached();
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await page.waitForTimeout(1000);
|
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Resend' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem').filter({ hasText: 'Resend' }).click();
|
|
||||||
await page.getByLabel('test.documenso.com').first().click();
|
await page.getByLabel('test.documenso.com').first().click();
|
||||||
await page.getByRole('button', { name: 'Send reminder' }).click();
|
await page.getByRole('button', { name: 'Send reminder' }).click();
|
||||||
|
|
||||||
await expect(
|
await expectToastTextToBeVisible(page, 'Document re-sent');
|
||||||
page.getByRole('status').filter({ hasText: 'Document re-sent' }).first(),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
||||||
@@ -265,14 +263,12 @@ test('[TEAMS]: delete draft team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
@@ -309,14 +305,12 @@ test('[TEAMS]: delete pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
||||||
@@ -354,14 +348,12 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { seedTeamEmailVerification } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEAMS]: send team email request', async ({ page }) => {
|
test('[TEAMS]: send team email request', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
@@ -54,8 +55,13 @@ test('[TEAMS]: delete team email', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/settings`,
|
redirectPath: `/t/${team.url}/settings`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.locator('section div').filter({ hasText: 'Team email' }).getByRole('button').click();
|
const settingsBtn = page
|
||||||
|
.locator('section div')
|
||||||
|
.filter({ hasText: 'Team email' })
|
||||||
|
.getByRole('button');
|
||||||
|
await openDropdownMenu(page, settingsBtn);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Remove' }).click();
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,13 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
['SIGNATURE', 'TEXT'].toSorted(),
|
['SIGNATURE', 'TEXT'].toSorted(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const textField = fields[1];
|
const textField = fields.find((field) => field.type === 'TEXT');
|
||||||
|
expect(textField).toBeDefined();
|
||||||
|
|
||||||
|
if (!textField) {
|
||||||
|
throw new Error('No text field');
|
||||||
|
}
|
||||||
|
|
||||||
expect(textField.fieldMeta).toBeDefined();
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEMPLATES]: view templates', async ({ page }) => {
|
test('[TEMPLATES]: view templates', async ({ page }) => {
|
||||||
const { team, owner, organisation } = await seedTeam({
|
const { team, owner, organisation } = await seedTeam({
|
||||||
@@ -71,13 +72,14 @@ test('[TEMPLATES]: delete template', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const template of ['Team template 1', 'Team template 2']) {
|
for (const template of ['Team template 1', 'Team template 2']) {
|
||||||
await page
|
const actionBtn = page
|
||||||
.getByRole('row', { name: template })
|
.getByRole('row', { name: template })
|
||||||
.getByRole('cell', { name: 'Use Template' })
|
.getByRole('cell', { name: 'Use Template' })
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.nth(1)
|
.nth(1);
|
||||||
.click();
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expect(page.getByText('Template deleted').first()).toBeVisible();
|
await expect(page.getByText('Template deleted').first()).toBeVisible();
|
||||||
@@ -110,7 +112,9 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Duplicate team template.
|
// Duplicate team template.
|
||||||
await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
|
const actionBtn = page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1);
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Duplicate' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||||
await page.getByRole('button', { name: 'Duplicate' }).click();
|
await page.getByRole('button', { name: 'Duplicate' }).click();
|
||||||
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import { expectTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to seed a webhook directly in the database for testing.
|
* Helper function to seed a webhook directly in the database for testing.
|
||||||
@@ -147,9 +147,11 @@ test('[WEBHOOKS]: delete webhook', async ({ page }) => {
|
|||||||
|
|
||||||
// Find the row with the webhook and click the action dropdown
|
// Find the row with the webhook and click the action dropdown
|
||||||
const webhookRow = page.locator('tr', { hasText: webhookUrl });
|
const webhookRow = page.locator('tr', { hasText: webhookUrl });
|
||||||
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
const actionBtn = webhookRow.getByTestId('webhook-table-action-btn');
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
// Click Delete menu item
|
// Click Delete menu item
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
// Fill in confirmation field
|
// Fill in confirmation field
|
||||||
@@ -196,9 +198,11 @@ test('[WEBHOOKS]: update webhook', async ({ page }) => {
|
|||||||
|
|
||||||
// Find the row with the webhook and click the action dropdown
|
// Find the row with the webhook and click the action dropdown
|
||||||
const webhookRow = page.locator('tr', { hasText: originalWebhookUrl });
|
const webhookRow = page.locator('tr', { hasText: originalWebhookUrl });
|
||||||
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
const actionBtn = webhookRow.getByTestId('webhook-table-action-btn');
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
// Click Edit menu item
|
// Click Edit menu item
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Edit' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
|
||||||
// Wait for dialog to open
|
// Wait for dialog to open
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
||||||
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
||||||
"test:e2e": "NODE_OPTIONS=--experimental-require-module NODE_ENV=test start-server-and-test \"npm run start -w @documenso/remix\" http://localhost:3000 \"playwright test $E2E_TEST_PATH\""
|
"test:e2e": "NODE_OPTIONS=--experimental-require-module NODE_ENV=test NEXT_PRIVATE_LOGGER_FILE_PATH=./logs.json start-server-and-test \"npm run start -w @documenso/remix\" http://localhost:3000 \"playwright test $E2E_TEST_PATH\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
function calculateWorkers() {
|
||||||
|
const total = os.cpus().length;
|
||||||
|
|
||||||
|
// Reserve 2 cores for the system
|
||||||
|
const usable = Math.max(total - 2, 1);
|
||||||
|
|
||||||
|
// 1 worker per 2 cores, minimum 1
|
||||||
|
const workers = Math.max(Math.floor(usable / 2), 1);
|
||||||
|
|
||||||
|
// Max 6 workers
|
||||||
|
return Math.min(workers, 6);
|
||||||
|
}
|
||||||
|
|
||||||
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
|
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
|
||||||
|
|
||||||
ENV_FILES.forEach((file) => {
|
ENV_FILES.forEach((file) => {
|
||||||
@@ -15,9 +29,8 @@ ENV_FILES.forEach((file) => {
|
|||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
/* Run tests in files in parallel */
|
fullyParallel: true,
|
||||||
fullyParallel: false,
|
workers: 10, // See Projects where 10 is utilized for API tests. We're not running 10 workers for UI tests.
|
||||||
workers: 2,
|
|
||||||
maxFailures: process.env.CI ? 1 : undefined,
|
maxFailures: process.env.CI ? 1 : undefined,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
@@ -31,25 +44,54 @@ export default defineConfig({
|
|||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on',
|
trace: 'retain-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
video: 'on-first-retry',
|
|
||||||
|
|
||||||
/* Add explicit timeouts for actions */
|
/* Add explicit timeouts for actions */
|
||||||
actionTimeout: 15_000,
|
actionTimeout: 15_000,
|
||||||
navigationTimeout: 30_000,
|
navigationTimeout: 30_000,
|
||||||
|
|
||||||
|
contextOptions: {
|
||||||
|
reducedMotion: 'reduce',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Disable animations via cookie for more stable tests */
|
||||||
|
storageState: {
|
||||||
|
cookies: [
|
||||||
|
{
|
||||||
|
name: '__disable_animations',
|
||||||
|
value: 'true',
|
||||||
|
domain: 'localhost',
|
||||||
|
path: '/',
|
||||||
|
expires: -1,
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'Lax' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
origins: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
|
// API Tests e2e/api/**/*.spec.ts
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'api',
|
||||||
|
testMatch: /e2e\/api\/.*\.spec\.ts/,
|
||||||
|
workers: 10, // Limited by DB connections before it gets flakey.
|
||||||
|
},
|
||||||
|
// Run UI Tests
|
||||||
|
{
|
||||||
|
name: 'ui',
|
||||||
|
testMatch: /e2e\/(?!api\/).*\.spec\.ts/,
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices['Desktop Chrome'],
|
||||||
viewport: { width: 1920, height: 1200 },
|
viewport: { width: 1920, height: 1200 },
|
||||||
},
|
},
|
||||||
|
workers: calculateWorkers(),
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -32,17 +32,17 @@
|
|||||||
"@react-email/img": "0.0.11",
|
"@react-email/img": "0.0.11",
|
||||||
"@react-email/link": "0.0.12",
|
"@react-email/link": "0.0.12",
|
||||||
"@react-email/preview": "0.0.13",
|
"@react-email/preview": "0.0.13",
|
||||||
"@react-email/render": "0.0.17",
|
"@react-email/render": "2.0.0",
|
||||||
"@react-email/row": "0.0.12",
|
"@react-email/row": "0.0.12",
|
||||||
"@react-email/section": "0.0.16",
|
"@react-email/section": "0.0.16",
|
||||||
"@react-email/tailwind": "^2.0.1",
|
"@react-email/tailwind": "^2.0.1",
|
||||||
"@react-email/text": "0.1.5",
|
"@react-email/text": "0.1.5",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"react-email": "^5.0.4",
|
"react-email": "^5.0.6",
|
||||||
"resend": "^6.5.2"
|
"resend": "^6.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.21"
|
"@types/nodemailer": "^7.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type RenderOptions = ReactEmail.Options & {
|
|||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const colors = (config.theme?.extend?.colors || {}) as Record<string, string>;
|
const colors = (config.theme?.extend?.colors || {}) as Record<string, string>;
|
||||||
|
|
||||||
export const render = (element: React.ReactNode, options?: RenderOptions) => {
|
export const render = async (element: React.ReactNode, options?: RenderOptions) => {
|
||||||
const { branding, ...otherOptions } = options ?? {};
|
const { branding, ...otherOptions } = options ?? {};
|
||||||
|
|
||||||
return ReactEmail.render(
|
return ReactEmail.render(
|
||||||
@@ -36,7 +36,7 @@ export const render = (element: React.ReactNode, options?: RenderOptions) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderWithI18N = (element: React.ReactNode, options?: RenderOptions) => {
|
export const renderWithI18N = async (element: React.ReactNode, options?: RenderOptions) => {
|
||||||
const { branding, i18n, ...otherOptions } = options ?? {};
|
const { branding, i18n, ...otherOptions } = options ?? {};
|
||||||
|
|
||||||
if (!i18n) {
|
if (!i18n) {
|
||||||
@@ -62,24 +62,3 @@ export const renderWithI18N = (element: React.ReactNode, options?: RenderOptions
|
|||||||
otherOptions,
|
otherOptions,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderAsync = async (element: React.ReactNode, options?: RenderOptions) => {
|
|
||||||
const { branding, ...otherOptions } = options ?? {};
|
|
||||||
|
|
||||||
return await ReactEmail.renderAsync(
|
|
||||||
<BrandingProvider branding={branding}>
|
|
||||||
<Tailwind
|
|
||||||
config={{
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{element}
|
|
||||||
</Tailwind>
|
|
||||||
</BrandingProvider>,
|
|
||||||
otherOptions,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/cli
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -77,7 +78,8 @@ export const run = async ({
|
|||||||
const recipientsToNotify = envelope.recipients.filter(
|
const recipientsToNotify = envelope.recipients.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
);
|
);
|
||||||
|
|
||||||
await io.runTask('send-cancellation-emails', async () => {
|
await io.runTask('send-cancellation-emails', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
|||||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
import type { JobRunIO } from '../../client/_internal/job';
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||||
@@ -79,8 +80,8 @@ export const run = async ({
|
|||||||
|
|
||||||
const recipientReference = recipientName || recipientEmail;
|
const recipientReference = recipientName || recipientEmail;
|
||||||
|
|
||||||
// Don't send notification if the owner is the one who signed
|
// Don't send notification if the owner is the one who signed.
|
||||||
if (owner.email === recipientEmail) {
|
if (owner.email === recipientEmail || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -85,36 +86,38 @@ export const run = async ({
|
|||||||
const i18n = await getI18nInstance(emailLanguage);
|
const i18n = await getI18nInstance(emailLanguage);
|
||||||
|
|
||||||
// Send confirmation email to the recipient who rejected
|
// Send confirmation email to the recipient who rejected
|
||||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||||
recipientName: recipient.name,
|
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||||
documentName: envelope.title,
|
recipientName: recipient.name,
|
||||||
documentOwnerName: envelope.user.name || envelope.user.email,
|
documentName: envelope.title,
|
||||||
reason: recipient.rejectionReason || '',
|
documentOwnerName: envelope.user.name || envelope.user.email,
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
reason: recipient.rejectionReason || '',
|
||||||
});
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
});
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
||||||
renderEmailWithI18N(recipientTemplate, {
|
renderEmailWithI18N(recipientTemplate, {
|
||||||
lang: emailLanguage,
|
lang: emailLanguage,
|
||||||
branding,
|
branding,
|
||||||
plainText: true,
|
plainText: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Send notification email to document owner
|
// Send notification email to document owner
|
||||||
await io.runTask('send-owner-notification-email', async () => {
|
await io.runTask('send-owner-notification-email', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -177,31 +178,33 @@ export const run = async ({
|
|||||||
includeSenderDetails: settings.includeSenderDetails,
|
includeSenderDetails: settings.includeSenderDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
await io.runTask('send-signing-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const [html, text] = await Promise.all([
|
await io.runTask('send-signing-email', async () => {
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(template, {
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||||
lang: emailLanguage,
|
renderEmailWithI18N(template, {
|
||||||
branding,
|
lang: emailLanguage,
|
||||||
plainText: true,
|
branding,
|
||||||
}),
|
plainText: true,
|
||||||
]);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: renderCustomEmailTemplate(
|
subject: renderCustomEmailTemplate(
|
||||||
documentMeta?.subject || emailSubject,
|
documentMeta?.subject || emailSubject,
|
||||||
customEmailTemplate,
|
customEmailTemplate,
|
||||||
),
|
),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
await io.runTask('update-recipient', async () => {
|
await io.runTask('update-recipient', async () => {
|
||||||
await prisma.recipient.update({
|
await prisma.recipient.update({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType } from '@prisma/client';
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -69,6 +70,12 @@ export const send2FATokenEmail = async ({ token, envelopeId }: Send2FATokenEmail
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Recipient is missing email address',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
||||||
envelopeId,
|
envelopeId,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -64,14 +65,18 @@ export const adminSuperDeleteDocument = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).documentDeleted;
|
).documentDeleted;
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
// if the document is pending, send cancellation emails to all recipients
|
// if the document is pending, send cancellation emails to all recipients
|
||||||
if (
|
if (
|
||||||
status === DocumentStatus.PENDING &&
|
status === DocumentStatus.PENDING &&
|
||||||
envelope.recipients.length > 0 &&
|
recipientsToNotify.length > 0 &&
|
||||||
isDocumentDeletedEmailEnabled
|
isDocumentDeletedEmailEnabled
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,41 @@
|
|||||||
import { createCanvas } from '@napi-rs/canvas';
|
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||||
|
import { Canvas, Image, Path2D } from 'skia-canvas';
|
||||||
|
|
||||||
|
// @ts-expect-error napi-rs/canvas satisfies the requirements
|
||||||
|
globalThis.Path2D = Path2D;
|
||||||
|
// @ts-expect-error napi-rs/canvas satisfies the requirements
|
||||||
|
globalThis.Image = Image;
|
||||||
|
|
||||||
|
class SkiaCanvasFactory {
|
||||||
|
_createCanvas(width: number, height: number) {
|
||||||
|
return new Canvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(width: number, height: number) {
|
||||||
|
const canvas = this._createCanvas(width, height);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvas,
|
||||||
|
context: canvas.getContext('2d'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(canvasAndContext: { canvas: Canvas }, width: number, height: number) {
|
||||||
|
canvasAndContext.canvas.width = width;
|
||||||
|
canvasAndContext.canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(canvasAndContext: { canvas: Canvas | null; context: unknown }) {
|
||||||
|
if (canvasAndContext.canvas) {
|
||||||
|
canvasAndContext.canvas.width = 0;
|
||||||
|
canvasAndContext.canvas.height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasAndContext.canvas = null;
|
||||||
|
canvasAndContext.context = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type PdfToImagesOptions = {
|
export type PdfToImagesOptions = {
|
||||||
scale?: number;
|
scale?: number;
|
||||||
@@ -9,7 +44,10 @@ export type PdfToImagesOptions = {
|
|||||||
export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOptions = {}) => {
|
export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOptions = {}) => {
|
||||||
const { scale = 2 } = options;
|
const { scale = 2 } = options;
|
||||||
|
|
||||||
const pdf = await pdfjsLib.getDocument(pdfBytes).promise;
|
const pdf = await pdfjsLib.getDocument({
|
||||||
|
data: pdfBytes,
|
||||||
|
CanvasFactory: SkiaCanvasFactory,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
const images = await pMap(
|
const images = await pMap(
|
||||||
Array.from({ length: pdf.numPages }),
|
Array.from({ length: pdf.numPages }),
|
||||||
@@ -19,7 +57,7 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
|
|||||||
|
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
const canvas = createCanvas(viewport.width, viewport.height);
|
const canvas = new Canvas(viewport.width, viewport.height);
|
||||||
const canvasContext = canvas.getContext('2d');
|
const canvasContext = canvas.getContext('2d');
|
||||||
|
|
||||||
await page.render({
|
await page.render({
|
||||||
@@ -32,7 +70,7 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pageNumber,
|
pageNumber,
|
||||||
image: await canvas.encode('jpeg'),
|
image: await canvas.toBuffer('jpeg'),
|
||||||
width: Math.floor(viewport.width),
|
width: Math.floor(viewport.width),
|
||||||
height: Math.floor(viewport.height),
|
height: Math.floor(viewport.height),
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
EnvelopeType,
|
EnvelopeType,
|
||||||
|
FieldType,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
@@ -43,6 +44,14 @@ export type CompleteDocumentWithTokenOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Override the recipient information. This will only work if the recipient
|
||||||
|
* does not have a name or email set.
|
||||||
|
*/
|
||||||
|
recipientOverride?: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
@@ -52,6 +61,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
|
recipientOverride,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -116,6 +126,35 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let recipientName = recipient.name;
|
||||||
|
let recipientEmail = recipient.email;
|
||||||
|
|
||||||
|
// Only trim the name if it's been derived.
|
||||||
|
if (!recipientName) {
|
||||||
|
recipientName = (
|
||||||
|
recipientOverride?.name ||
|
||||||
|
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trim the email if it's been derived.
|
||||||
|
if (!recipient.email) {
|
||||||
|
recipientEmail = (
|
||||||
|
recipientOverride?.email ||
|
||||||
|
fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipientEmail) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Recipient email is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check ACCESS AUTH 2FA validation during document completion
|
// Check ACCESS AUTH 2FA validation during document completion
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
@@ -129,6 +168,12 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!recipient.email.trim()) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isValid = await isRecipientAuthorized({
|
const isValid = await isRecipientAuthorized({
|
||||||
type: 'ACCESS_2FA',
|
type: 'ACCESS_2FA',
|
||||||
documentAuthOptions: envelope.authOptions,
|
documentAuthOptions: envelope.authOptions,
|
||||||
@@ -176,9 +221,43 @@ export const completeDocumentWithToken = async ({
|
|||||||
data: {
|
data: {
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
signedAt: new Date(),
|
signedAt: new Date(),
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (recipientEmail !== recipient.email || recipientName !== recipient.name) {
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
user: {
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
|
},
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||||
|
from: recipient.name,
|
||||||
|
to: recipientName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||||
|
from: recipient.email,
|
||||||
|
to: recipientEmail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const authOptions = extractDocumentAuthMethods({
|
const authOptions = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
@@ -189,13 +268,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipientEmail,
|
||||||
recipientName: recipient.name,
|
recipientName: recipientName,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
actionAuth: authOptions.derivedRecipientActionAuth,
|
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||||
@@ -247,8 +326,8 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { ApiRequestMetadata } from '../../universal/extract-request-metadat
|
|||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getMemberRoles } from '../team/get-member-roles';
|
import { getMemberRoles } from '../team/get-member-roles';
|
||||||
@@ -209,7 +210,7 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
// Send cancellation emails to recipients.
|
// Send cancellation emails to recipients.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
envelope.recipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -118,7 +119,7 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
recipientsToRemind.map(async (recipient) => {
|
recipientsToRemind.map(async (recipient) => {
|
||||||
if (recipient.role === RecipientRole.CC) {
|
if (recipient.role === RecipientRole.CC || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { formatDocumentsPath } from '../../utils/teams';
|
import { formatDocumentsPath } from '../../utils/teams';
|
||||||
@@ -176,8 +177,12 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
'signer.name': recipient.name,
|
'signer.name': recipient.name,
|
||||||
'signer.email': recipient.email,
|
'signer.email': recipient.email,
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ import {
|
|||||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@@ -128,6 +130,24 @@ export const sendDocument = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that recipients with auth requirements have a valid email.
|
||||||
|
envelope.recipients.forEach((recipient) => {
|
||||||
|
const auth = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
recipient.role !== RecipientRole.CC &&
|
||||||
|
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) &&
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
||||||
// decide if we want to enforce this for API & templates.
|
// decide if we want to enforce this for API & templates.
|
||||||
// const fields = await getFieldsForDocument({
|
// const fields = await getFieldsForDocument({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -69,6 +70,11 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
|
|||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
// Skip sending email if recipient has no email address
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
const template = createElement(DocumentPendingEmailTemplate, {
|
const template = createElement(DocumentPendingEmailTemplate, {
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export type CreateEmbeddingPresignTokenOptions = {
|
|||||||
* In development mode, can be set to 0 to create a token that expires immediately (for testing)
|
* In development mode, can be set to 0 to create a token that expires immediately (for testing)
|
||||||
*/
|
*/
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
|
scope?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEmbeddingPresignToken = async ({
|
export const createEmbeddingPresignToken = async ({
|
||||||
apiToken,
|
apiToken,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
scope,
|
||||||
}: CreateEmbeddingPresignTokenOptions) => {
|
}: CreateEmbeddingPresignTokenOptions) => {
|
||||||
try {
|
try {
|
||||||
// Validate the API token
|
// Validate the API token
|
||||||
@@ -40,6 +42,7 @@ export const createEmbeddingPresignToken = async ({
|
|||||||
const token = await new SignJWT({
|
const token = await new SignJWT({
|
||||||
aud: String(validatedToken.teamId ?? validatedToken.userId),
|
aud: String(validatedToken.teamId ?? validatedToken.userId),
|
||||||
sub: String(validatedToken.id),
|
sub: String(validatedToken.id),
|
||||||
|
scope,
|
||||||
})
|
})
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
.setIssuedAt(now.toJSDate())
|
.setIssuedAt(now.toJSDate())
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
|||||||
|
|
||||||
export type VerifyEmbeddingPresignTokenOptions = {
|
export type VerifyEmbeddingPresignTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
|
scope?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyEmbeddingPresignToken = async ({
|
export const verifyEmbeddingPresignToken = async ({
|
||||||
token,
|
token,
|
||||||
|
scope,
|
||||||
}: VerifyEmbeddingPresignTokenOptions) => {
|
}: VerifyEmbeddingPresignTokenOptions) => {
|
||||||
// First decode the JWT to get the claims without verification
|
// First decode the JWT to get the claims without verification
|
||||||
let decodedToken: JWTPayload;
|
let decodedToken: JWTPayload;
|
||||||
@@ -81,6 +83,12 @@ export const verifyEmbeddingPresignToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decodedToken.scope && scope && decodedToken.scope !== scope) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'Presign token scope not matched',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Now verify the token with the actual secret
|
// Now verify the token with the actual secret
|
||||||
const secret = new TextEncoder().encode(apiToken.token);
|
const secret = new TextEncoder().encode(apiToken.token);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { PDFDocument } from '@cantoo/pdf-lib';
|
|||||||
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
|
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
|
||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import { getPageSize } from './get-page-size';
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +16,7 @@ export async function addRejectionStampToPdf(
|
|||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
pdfDoc.registerFontkit(fontkit);
|
pdfDoc.registerFontkit(fontkit);
|
||||||
|
|
||||||
const fontBytes = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
const fontBytes = await fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||||
async (res) => res.arrayBuffer(),
|
async (res) => res.arrayBuffer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@cantoo/pdf-lib';
|
} from '@cantoo/pdf-lib';
|
||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
|
|
||||||
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
||||||
const context = document.context;
|
const context = document.context;
|
||||||
@@ -29,7 +29,7 @@ export const flattenForm = async (document: PDFDocument) => {
|
|||||||
|
|
||||||
const form = document.getForm();
|
const form = document.getForm();
|
||||||
|
|
||||||
const fontNoto = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
const fontNoto = await fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||||
async (res) => res.arrayBuffer(),
|
async (res) => res.arrayBuffer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
|||||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
@@ -37,8 +37,12 @@ import { getPageSize } from './get-page-size';
|
|||||||
|
|
||||||
export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) =>
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
|||||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
@@ -30,8 +30,12 @@ import { getPageSize } from './get-page-size';
|
|||||||
|
|
||||||
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) =>
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
@@ -142,7 +142,8 @@ export const deleteEnvelopeRecipient = async ({
|
|||||||
if (
|
if (
|
||||||
recipientToDelete.sendStatus === SendStatus.SENT &&
|
recipientToDelete.sendStatus === SendStatus.SENT &&
|
||||||
isRecipientRemovedEmailEnabled &&
|
isRecipientRemovedEmailEnabled &&
|
||||||
envelope.type === EnvelopeType.DOCUMENT
|
envelope.type === EnvelopeType.DOCUMENT &&
|
||||||
|
isRecipientEmailValidForSending(recipientToDelete)
|
||||||
) {
|
) {
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -294,10 +294,15 @@ export const setDocumentRecipients = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).recipientRemoved;
|
).recipientRemoved;
|
||||||
|
|
||||||
// Send emails to deleted recipients.
|
// Send emails to deleted recipients who have emails.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
removedRecipients.map(async (recipient) => {
|
removedRecipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientRemovedEmailEnabled) {
|
if (
|
||||||
|
recipient.sendStatus !== SendStatus.SENT ||
|
||||||
|
recipient.role === RecipientRole.CC ||
|
||||||
|
!isRecipientRemovedEmailEnabled ||
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
||||||
@@ -110,3 +111,13 @@ export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
|
|||||||
documentId: true,
|
documentId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZRecipientEmailSchema = z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.email({ message: msg`Invalid email`.id })
|
||||||
|
.max(254),
|
||||||
|
]);
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
|||||||
|
|
||||||
return token
|
return token
|
||||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}${presignToken ? `?presignToken=${presignToken}` : ''}`
|
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}${presignToken ? `?presignToken=${presignToken}` : ''}`
|
||||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
|
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}${presignToken ? `?token=${presignToken}` : ''}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Envelope } from '@prisma/client';
|
import type { Envelope } from '@prisma/client';
|
||||||
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
import { extractLegacyIds } from '../universal/id';
|
import { extractLegacyIds } from '../universal/id';
|
||||||
@@ -58,3 +59,7 @@ export const mapRecipientToLegacyRecipient = (
|
|||||||
...legacyId,
|
...legacyId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isRecipientEmailValidForSending = (recipient: Pick<Recipient, 'email'>) => {
|
||||||
|
return z.string().email().safeParse(recipient.email).success;
|
||||||
|
};
|
||||||
|
|||||||
@@ -587,6 +587,9 @@ export const seedPendingDocumentWithFullFields = async ({
|
|||||||
where: {
|
where: {
|
||||||
envelopeId: document.id,
|
envelopeId: document.id,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
signingOrder: 'asc',
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
fields: true,
|
fields: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const createEmbeddingPresignTokenRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { expiresIn } = input;
|
const { expiresIn, scope } = input;
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED()) {
|
if (IS_BILLING_ENABLED()) {
|
||||||
const token = await getApiTokenByToken({ token: apiToken });
|
const token = await getApiTokenByToken({ token: apiToken });
|
||||||
@@ -54,6 +54,7 @@ export const createEmbeddingPresignTokenRoute = procedure
|
|||||||
const presignToken = await createEmbeddingPresignToken({
|
const presignToken = await createEmbeddingPresignToken({
|
||||||
apiToken,
|
apiToken,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
scope,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...presignToken };
|
return { ...presignToken };
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default(60)
|
.default(60)
|
||||||
.describe('Expiration time in minutes (default: 60, max: 10,080)'),
|
.describe('Expiration time in minutes (default: 60, max: 10,080)'),
|
||||||
|
scope: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Resource restriction. Example: documentId:1, templateId:2'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
documentDataId: z.string(),
|
documentDataId: z.string(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const updateEmbeddingDocumentRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
|
token: presignToken,
|
||||||
|
scope: `documentId:${input.documentId}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { documentId, title, externalId, recipients, meta } = input;
|
const { documentId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export const updateEmbeddingTemplateRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
|
token: presignToken,
|
||||||
|
scope: `templateId:${input.templateId}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { templateId, title, externalId, recipients, meta } = input;
|
const { templateId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
@@ -21,22 +21,11 @@ import {
|
|||||||
ZFieldPageYSchema,
|
ZFieldPageYSchema,
|
||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
const ZFieldSchema = z.object({
|
|
||||||
id: z.number().optional(),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
pageNumber: ZFieldPageNumberSchema,
|
|
||||||
pageX: ZFieldPageXSchema,
|
|
||||||
pageY: ZFieldPageYSchema,
|
|
||||||
width: ZFieldWidthSchema,
|
|
||||||
height: ZFieldHeightSchema,
|
|
||||||
fieldMeta: ZFieldMetaSchema.optional(),
|
|
||||||
envelopeItemId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
@@ -44,7 +33,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export const verifyEmbeddingPresignTokenRoute = procedure
|
|||||||
.output(ZVerifyEmbeddingPresignTokenResponseSchema)
|
.output(ZVerifyEmbeddingPresignTokenResponseSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const { token } = input;
|
const { token, scope } = input;
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
token,
|
token,
|
||||||
|
scope,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
return { success: !!apiToken };
|
return { success: !!apiToken };
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const ZVerifyEmbeddingPresignTokenRequestSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Token is required' })
|
.min(1, { message: 'Token is required' })
|
||||||
.describe('The presign token to verify'),
|
.describe('The presign token to verify'),
|
||||||
|
scope: z.string().optional().describe('The scope to verify'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZVerifyEmbeddingPresignTokenResponseSchema = z.object({
|
export const ZVerifyEmbeddingPresignTokenResponseSchema = z.object({
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
ZDocumentTitleSchema,
|
ZDocumentTitleSchema,
|
||||||
ZDocumentVisibilitySchema,
|
ZDocumentVisibilitySchema,
|
||||||
} from '../document-router/schema';
|
} from '../document-router/schema';
|
||||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../trpc';
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
|
import { ZCreateEnvelopeRecipientSchema } from './envelope-recipients/create-envelope-recipients.types';
|
||||||
|
|
||||||
export const createEnvelopeMeta: TrpcRouteMeta = {
|
export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||||
openapi: {
|
openapi: {
|
||||||
@@ -54,7 +54,7 @@ export const ZCreateEnvelopePayloadSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
ZCreateRecipientSchema.extend({
|
ZCreateEnvelopeRecipientSchema.extend({
|
||||||
fields: ZEnvelopeFieldAndMetaSchema.and(
|
fields: ZEnvelopeFieldAndMetaSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
identifier: z
|
identifier: z
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,15 @@
|
|||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZEnvelopeRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import {
|
||||||
|
ZRecipientAccessAuthTypesSchema,
|
||||||
|
ZRecipientActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import {
|
||||||
|
ZEnvelopeRecipientLiteSchema,
|
||||||
|
ZRecipientEmailSchema,
|
||||||
|
} from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZCreateRecipientSchema } from '../../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../../trpc';
|
import type { TrpcRouteMeta } from '../../trpc';
|
||||||
|
|
||||||
export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
||||||
@@ -15,9 +22,18 @@ export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ZCreateEnvelopeRecipientSchema = z.object({
|
||||||
|
email: ZRecipientEmailSchema,
|
||||||
|
name: z.string().max(255),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
|
||||||
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZCreateEnvelopeRecipientsRequestSchema = z.object({
|
export const ZCreateEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
data: ZCreateRecipientSchema.array(),
|
data: ZCreateEnvelopeRecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateEnvelopeRecipientsResponseSchema = z.object({
|
export const ZCreateEnvelopeRecipientsResponseSchema = z.object({
|
||||||
|
|||||||
+17
-3
@@ -1,8 +1,12 @@
|
|||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import {
|
||||||
|
ZRecipientAccessAuthTypesSchema,
|
||||||
|
ZRecipientActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZUpdateRecipientSchema } from '../../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../../trpc';
|
import type { TrpcRouteMeta } from '../../trpc';
|
||||||
|
|
||||||
export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
||||||
@@ -15,9 +19,19 @@ export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ZUpdateEnvelopeRecipientSchema = z.object({
|
||||||
|
id: z.number().describe('The ID of the recipient to update.'),
|
||||||
|
email: ZRecipientEmailSchema.optional(),
|
||||||
|
name: z.string().max(255).optional(),
|
||||||
|
role: z.nativeEnum(RecipientRole).optional(),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
|
||||||
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRecipientsRequestSchema = z.object({
|
export const ZUpdateEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
data: ZUpdateRecipientSchema.array(),
|
data: ZUpdateEnvelopeRecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRecipientsResponseSchema = z.object({
|
export const ZUpdateEnvelopeRecipientsResponseSchema = z.object({
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||||
|
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||||
|
import { parseDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { authenticatedProcedure } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZFindEnvelopeAuditLogsRequestSchema,
|
||||||
|
ZFindEnvelopeAuditLogsResponseSchema,
|
||||||
|
findEnvelopeAuditLogsMeta,
|
||||||
|
} from './find-envelope-audit-logs.types';
|
||||||
|
|
||||||
|
export const findEnvelopeAuditLogsRoute = authenticatedProcedure
|
||||||
|
.meta(findEnvelopeAuditLogsMeta)
|
||||||
|
.input(ZFindEnvelopeAuditLogsRequestSchema)
|
||||||
|
.output(ZFindEnvelopeAuditLogsResponseSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const {
|
||||||
|
envelopeId,
|
||||||
|
page = 1,
|
||||||
|
perPage = 50,
|
||||||
|
orderByColumn = 'createdAt',
|
||||||
|
orderByDirection = 'desc',
|
||||||
|
} = input;
|
||||||
|
|
||||||
|
ctx.logger.info({
|
||||||
|
input: {
|
||||||
|
envelopeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||||
|
id: {
|
||||||
|
type: 'envelopeId',
|
||||||
|
id: envelopeId,
|
||||||
|
},
|
||||||
|
type: null,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
teamId: ctx.teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findUnique({
|
||||||
|
where: envelopeWhereInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelope) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only documents have audit logs.
|
||||||
|
if (envelope.type !== EnvelopeType.DOCUMENT) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Templates do not have audit logs.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, count] = await Promise.all([
|
||||||
|
prisma.documentAuditLog.findMany({
|
||||||
|
where: { envelopeId: envelope.id },
|
||||||
|
skip: Math.max(page - 1, 0) * perPage,
|
||||||
|
take: perPage,
|
||||||
|
orderBy: {
|
||||||
|
[orderByColumn]: orderByDirection,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.documentAuditLog.count({
|
||||||
|
where: { envelopeId: envelope.id },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: parsedData,
|
||||||
|
count,
|
||||||
|
currentPage: Math.max(page, 1),
|
||||||
|
perPage,
|
||||||
|
totalPages: Math.ceil(count / perPage),
|
||||||
|
} satisfies FindResultResponse<typeof parsedData>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
|
||||||
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
|
|
||||||
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
|
|
||||||
|
export const findEnvelopeAuditLogsMeta: TrpcRouteMeta = {
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/envelope/{envelopeId}/audit-log',
|
||||||
|
summary: 'Get envelope audit logs',
|
||||||
|
description: 'Find audit logs based on a search criteria',
|
||||||
|
tags: ['Envelope'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZFindEnvelopeAuditLogsRequestSchema = ZFindSearchParamsSchema.omit({
|
||||||
|
query: true,
|
||||||
|
}).extend({
|
||||||
|
envelopeId: z.string().describe('Envelope ID'),
|
||||||
|
orderByColumn: z.enum(['createdAt']).optional(),
|
||||||
|
orderByDirection: z.enum(['asc', 'desc']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZFindEnvelopeAuditLogsResponseSchema = ZFindResultResponse.extend({
|
||||||
|
data: ZDocumentAuditLogSchema.array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFindEnvelopeAuditLogsRequest = z.infer<typeof ZFindEnvelopeAuditLogsRequestSchema>;
|
||||||
|
export type TFindEnvelopeAuditLogsResponse = z.infer<typeof ZFindEnvelopeAuditLogsResponseSchema>;
|
||||||
@@ -18,6 +18,7 @@ import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-enve
|
|||||||
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
|
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
|
||||||
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
|
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
|
||||||
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
||||||
|
import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs';
|
||||||
import { getEnvelopeRoute } from './get-envelope';
|
import { getEnvelopeRoute } from './get-envelope';
|
||||||
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
||||||
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
||||||
@@ -65,6 +66,9 @@ export const envelopeRouter = router({
|
|||||||
set: setEnvelopeFieldsRoute,
|
set: setEnvelopeFieldsRoute,
|
||||||
sign: signEnvelopeFieldRoute,
|
sign: signEnvelopeFieldRoute,
|
||||||
},
|
},
|
||||||
|
auditLog: {
|
||||||
|
find: findEnvelopeAuditLogsRoute,
|
||||||
|
},
|
||||||
get: getEnvelopeRoute,
|
get: getEnvelopeRoute,
|
||||||
create: createEnvelopeRoute,
|
create: createEnvelopeRoute,
|
||||||
use: useEnvelopeRoute,
|
use: useEnvelopeRoute,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EnvelopeType, RecipientRole } from '@prisma/client';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
@@ -10,7 +10,7 @@ export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z.string().toLowerCase().email().min(1).max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255),
|
name: z.string().max(255),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { zodFormData } from '../../utils/zod-form-data';
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
import type { TrpcRouteMeta } from '../trpc';
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
@@ -40,7 +41,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().describe('The ID of the recipient in the template.'),
|
id: z.number().describe('The ID of the recipient in the template.'),
|
||||||
email: z.string().email().max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255).optional(),
|
name: z.string().max(255).optional(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ export const recipientRouter = router({
|
|||||||
completeDocumentWithToken: procedure
|
completeDocumentWithToken: procedure
|
||||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { token, documentId, accessAuthOptions, nextSigner } = input;
|
const { token, documentId, accessAuthOptions, nextSigner, recipientOverride } = input;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
@@ -577,6 +577,7 @@ export const recipientRouter = router({
|
|||||||
},
|
},
|
||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
|
recipientOverride,
|
||||||
userId: ctx.user?.id,
|
userId: ctx.user?.id,
|
||||||
requestMetadata: ctx.metadata.requestMetadata,
|
requestMetadata: ctx.metadata.requestMetadata,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -171,6 +171,12 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
|||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
recipientOverride: z
|
||||||
|
.object({
|
||||||
|
email: z.string().trim().toLowerCase().email().max(254).optional(),
|
||||||
|
name: z.string().max(255).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import {
|
import {
|
||||||
ZTemplateLiteSchema,
|
ZTemplateLiteSchema,
|
||||||
@@ -102,7 +103,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().describe('The ID of the recipient in the template.'),
|
id: z.number().describe('The ID of the recipient in the template.'),
|
||||||
email: z.string().email().max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255).optional(),
|
name: z.string().max(255).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user