mirror of
https://github.com/documenso/documenso.git
synced 2026-07-11 13:35:20 +10:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5c87e3fd1 | |||
| 24a74c7b57 | |||
| f0a5a7e816 | |||
| 8462cd13fd | |||
| 576846de32 | |||
| 06071ea035 | |||
| b45a2691ba | |||
| f31cc575d0 | |||
| 05d7015ef0 | |||
| 2ca5d6cfaa | |||
| 04814ca14e | |||
| dd1dccdb6a | |||
| df4316ac5c | |||
| 02f1264eea | |||
| 928edb8645 | |||
| 54b0e4964e | |||
| 68e6ccdd19 | |||
| 09ab7e9a09 | |||
| 3bb0777914 | |||
| 4d6389e901 | |||
| 51e3d5030d | |||
| 0cebdec637 | |||
| 43486d8448 | |||
| 4d3d1b8d14 | |||
| 0387f3c20a | |||
| c5032d0c43 | |||
| 3bd34964cd | |||
| fe93b11a2c | |||
| 7638faf27b | |||
| 8fca029d96 | |||
| bac2bf11f4 | |||
| d93b2a70a7 | |||
| 5da915da38 | |||
| dcaecf1fc5 | |||
| f70b76d8b8 | |||
| 93137c6396 | |||
| d058b7c705 | |||
| b51f562224 | |||
| f80aa4bf72 | |||
| 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
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "^15.5.7",
|
"next": "15.5.9",
|
||||||
"next-plausible": "^3.12.5",
|
"next-plausible": "^3.12.5",
|
||||||
"nextra": "^3",
|
"nextra": "^3",
|
||||||
"nextra-theme-docs": "^3",
|
"nextra-theme-docs": "^3",
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
"pagefind": "^1.2.0",
|
"pagefind": "^1.2.0",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -178,7 +178,7 @@ The dropdown/select field collects a single choice from a list of options.
|
|||||||
|
|
||||||
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
||||||
|
|
||||||
{/*  */}
|

|
||||||
|
|
||||||
The dropdown/select field settings include:
|
The dropdown/select field settings include:
|
||||||
|
|
||||||
|
|||||||
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 |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -12,11 +12,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"next": "^15.5.7"
|
"next": "15.5.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "18.3.27",
|
"@types/react": "18.3.27",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ export const AdminOrganisationMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating{' '}
|
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||||
<span className="font-bold">{organisationMemberName}.</span>
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
type AiFeaturesEnableDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onEnabled: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiFeaturesEnableDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onEnabled,
|
||||||
|
}: AiFeaturesEnableDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
|
const isTeamAdmin = team.currentTeamRole === TeamMemberRole.ADMIN;
|
||||||
|
const isOrganisationAdmin = organisation.currentOrganisationRole === OrganisationMemberRole.ADMIN;
|
||||||
|
const canEnableAiFeatures = isTeamAdmin || isOrganisationAdmin;
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { mutateAsync: updateTeamSettings, isPending: isUpdatingTeamSettings } =
|
||||||
|
trpc.team.settings.update.useMutation();
|
||||||
|
const { mutateAsync: updateOrganisationSettings, isPending: isUpdatingOrganisationSettings } =
|
||||||
|
trpc.organisation.settings.update.useMutation();
|
||||||
|
|
||||||
|
const isSubmitting = isUpdatingTeamSettings || isUpdatingOrganisationSettings;
|
||||||
|
|
||||||
|
const onEnableClick = async () => {
|
||||||
|
if (!canEnableAiFeatures) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTeamAdmin) {
|
||||||
|
await updateTeamSettings({
|
||||||
|
teamId: team.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateOrganisationSettings({
|
||||||
|
organisationId: organisation.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnabled();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to enable AI features', err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: t`We couldn't enable AI features right now. Please try again.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
Turn on AI detection to automatically find recipients and fields in your documents. AI
|
||||||
|
providers do not retain your data for training.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Alert variant="neutral">
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
Your document content will be sent securely to our AI provider solely for detection
|
||||||
|
and will not be stored or used for training.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
You're an admin. You can enable AI features for this team right away. Everyone on
|
||||||
|
the team will see AI detection once enabled.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
AI features are disabled for your team. Please ask your team owner or organisation
|
||||||
|
owner to enable them.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<Button type="button" onClick={() => void onEnableClick()} loading={isSubmitting}>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
||||||
@@ -232,10 +232,19 @@ export const AiFieldDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.fieldsDetected}
|
||||||
{progress.fieldsDetected} field(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # field found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # fields found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -279,7 +288,11 @@ export const AiFieldDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>We found {detectedFields.length} field(s) in your document.</Trans>
|
<Plural
|
||||||
|
value={detectedFields.length}
|
||||||
|
one="We found # field in your document."
|
||||||
|
other="We found # fields in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
@@ -190,10 +190,19 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.recipientsDetected}
|
||||||
{progress.recipientsDetected} recipient(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipient found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipients found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -237,9 +246,11 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Plural
|
||||||
We found {detectedRecipients.length} recipient(s) in your document.
|
value={detectedRecipients.length}
|
||||||
</Trans>
|
one="We found # recipient in your document."
|
||||||
|
other="We found # recipients in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -323,8 +353,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Reply To Email</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Reply To Email{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -342,8 +374,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Subject</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Subject{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -360,8 +394,10 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex flex-row items-center">
|
<FormLabel className="flex flex-row items-center">
|
||||||
<Trans>Message</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Message{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
@@ -444,7 +480,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>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const EnvelopeItemDeleteDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You cannot delete this item because the document has been sent to recipients
|
You cannot delete this item because the document has been sent to recipients.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
|
|||||||
<Trans>
|
<Trans>
|
||||||
Once you update your DNS records, it may take up to 48 hours for it to be propogated.
|
Once you update your DNS records, it may take up to 48 hours for it to be propogated.
|
||||||
Once the DNS propagation is complete you will need to come back and press the "Sync"
|
Once the DNS propagation is complete you will need to come back and press the "Sync"
|
||||||
domains button
|
domains button.
|
||||||
</Trans>
|
</Trans>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export const OrganisationMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription className="mt-4">
|
<DialogDescription className="mt-4">
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating{' '}
|
You are currently updating <span className="font-bold">{organisationMemberName}</span>
|
||||||
<span className="font-bold">{organisationMemberName}.</span>
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { DocumentSigningDisclosure } from '../general/document-signing/document-
|
|||||||
|
|
||||||
export type SignFieldSignatureDialogProps = {
|
export type SignFieldSignatureDialogProps = {
|
||||||
initialSignature?: string;
|
initialSignature?: string;
|
||||||
|
fullName?: string;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
uploadSignatureEnabled?: boolean;
|
uploadSignatureEnabled?: boolean;
|
||||||
drawSignatureEnabled?: boolean;
|
drawSignatureEnabled?: boolean;
|
||||||
@@ -28,6 +29,7 @@ export const SignFieldSignatureDialog = createCallable<
|
|||||||
>(
|
>(
|
||||||
({
|
({
|
||||||
call,
|
call,
|
||||||
|
fullName,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
@@ -46,6 +48,7 @@ export const SignFieldSignatureDialog = createCallable<
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
|
fullName={fullName}
|
||||||
value={localSignature ?? ''}
|
value={localSignature ?? ''}
|
||||||
onChange={({ value }) => setLocalSignature(value)}
|
onChange={({ value }) => setLocalSignature(value)}
|
||||||
typedSignatureEnabled={typedSignatureEnabled}
|
typedSignatureEnabled={typedSignatureEnabled}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -30,6 +31,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -53,26 +61,34 @@ export const TeamDeleteDialog = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { refreshSession } = useSession();
|
const { refreshSession } = useSession();
|
||||||
|
|
||||||
|
const currentOrganisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const deleteMessage = _(msg`delete ${teamName}`);
|
const deleteMessage = _(msg`delete ${teamName}`);
|
||||||
|
|
||||||
|
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
|
||||||
|
|
||||||
const ZDeleteTeamFormSchema = z.object({
|
const ZDeleteTeamFormSchema = z.object({
|
||||||
teamName: z.literal(deleteMessage, {
|
teamName: z.literal(deleteMessage, {
|
||||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||||
}),
|
}),
|
||||||
|
transferTeamId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ZDeleteTeamFormSchema),
|
resolver: zodResolver(ZDeleteTeamFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
teamName: '',
|
teamName: '',
|
||||||
|
transferTeamId: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
|
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async (data: z.infer<typeof ZDeleteTeamFormSchema>) => {
|
||||||
try {
|
try {
|
||||||
await deleteTeam({ teamId });
|
const transferTeamId = data.transferTeamId ? parseInt(data.transferTeamId, 10) : undefined;
|
||||||
|
|
||||||
|
await deleteTeam({ teamId, transferTeamId: transferTeamId || undefined });
|
||||||
|
|
||||||
await refreshSession();
|
await refreshSession();
|
||||||
|
|
||||||
@@ -168,6 +184,43 @@ export const TeamDeleteDialog = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{filteredTeams.length > 0 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="transferTeamId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Transfer documents to a different team</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select {...field} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={_(msg`Don't transfer (Delete all documents)`)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="-1">
|
||||||
|
<Trans>Don't transfer (Delete all documents)</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
{filteredTeams.map((team) => (
|
||||||
|
<SelectItem key={team.id} value={team.id.toString()}>
|
||||||
|
{team.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const TeamMemberUpdateDialog = ({
|
|||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
You are currently updating <span className="font-bold">{memberName}.</span>
|
You are currently updating <span className="font-bold">{memberName}</span>.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import { FilePlus, Loader } from 'lucide-react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
type TemplateCreateDialogProps = {
|
|
||||||
folderId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { user } = useSession();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
|
||||||
|
|
||||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
|
||||||
|
|
||||||
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
|
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
||||||
|
|
||||||
const onFileDrop = async (files: File[]) => {
|
|
||||||
const file = files[0];
|
|
||||||
|
|
||||||
if (isUploadingFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUploadingFile(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
title: file.name,
|
|
||||||
folderId: folderId,
|
|
||||||
} satisfies TCreateTemplatePayloadSchema;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('payload', JSON.stringify(payload));
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const { envelopeId: id } = await createTemplate(formData);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Template document uploaded`),
|
|
||||||
description: _(
|
|
||||||
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
|
|
||||||
),
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowTemplateCreateDialog(false);
|
|
||||||
|
|
||||||
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`Please try again later.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsUploadingFile(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={showTemplateCreateDialog}
|
|
||||||
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="cursor-pointer" disabled={!user.emailVerified}>
|
|
||||||
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
<Trans>Template (Legacy)</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent className="w-full max-w-xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans>New Template</Trans>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<Trans>
|
|
||||||
Templates allow you to quickly generate documents with pre-filled recipients and
|
|
||||||
fields.
|
|
||||||
</Trans>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
|
|
||||||
|
|
||||||
{isUploadingFile && (
|
|
||||||
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
|
|
||||||
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button type="button" variant="secondary" disabled={isUploadingFile}>
|
|
||||||
<Trans>Close</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
SKIP_QUERY_BATCH_META,
|
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>
|
||||||
|
|||||||
@@ -219,9 +219,8 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
|
|||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
A secret that will be sent to your URL so you can verify that the request
|
A secret that will be sent to your URL so you can verify that the request
|
||||||
has been sent by Documenso
|
has been sent by Documenso.
|
||||||
</Trans>
|
</Trans>
|
||||||
.
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
disableAnimation
|
disableAnimation
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={
|
typedSignatureEnabled={
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
|||||||
<Label htmlFor="email" className="text-muted-foreground">
|
<Label htmlFor="email" className="text-muted-foreground">
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
|
<Input id="email" type="email" className="mt-2 bg-muted" value={user.email} disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -124,6 +124,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={user.name ?? ''}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -244,11 +244,11 @@ export const SignInForm = ({
|
|||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AuthenticationErrorCode.InvalidCredentials,
|
AuthenticationErrorCode.InvalidCredentials,
|
||||||
() => msg`The email or password provided is incorrect`,
|
() => msg`The email or password provided is incorrect.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
AuthenticationErrorCode.InvalidTwoFactorCode,
|
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||||
() => msg`The two-factor authentication code provided is incorrect`,
|
() => msg`The two-factor authentication code provided is incorrect.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => handleFallbackErrorMessages(error.code));
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
|||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.UNAUTHORIZED,
|
AppErrorCode.UNAUTHORIZED,
|
||||||
() => msg`You do not have permission to create a token for this team`,
|
() => msg`You do not have permission to create a token for this team.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(value) => setSignature(value)}
|
onChange={(value) => setSignature(value)}
|
||||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||||
@@ -433,7 +434,7 @@ export const DirectTemplateSigningForm = ({
|
|||||||
|
|
||||||
<div className="mt-4 flex gap-x-4">
|
<div className="mt-4 flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="w-full bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
|||||||
+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">
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ export const DocumentSigningForm = ({
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-signing-mobile-widget-progress-bar"
|
layoutId="document-signing-mobile-widget-progress-bar"
|
||||||
className="bg-documenso absolute inset-y-0 left-0"
|
className="bg-primary absolute inset-y-0 left-0"
|
||||||
style={{
|
style={{
|
||||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-flow-container-step"
|
layoutId="document-flow-container-step"
|
||||||
className="absolute inset-y-0 left-0 bg-documenso"
|
className="absolute inset-y-0 left-0 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
+11
-7
@@ -56,8 +56,11 @@ export const DocumentSigningSignatureField = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [fontSize, setFontSize] = useState(2);
|
const [fontSize, setFontSize] = useState(2);
|
||||||
|
|
||||||
const { signature: providedSignature, setSignature: setProvidedSignature } =
|
const {
|
||||||
useRequiredDocumentSigningContext();
|
fullName,
|
||||||
|
signature: providedSignature,
|
||||||
|
setSignature: setProvidedSignature,
|
||||||
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
@@ -236,13 +239,13 @@ export const DocumentSigningSignatureField = ({
|
|||||||
type="Signature"
|
type="Signature"
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-background">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="h-5 w-5 animate-spin text-primary md:h-8 md:w-8" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state === 'empty' && (
|
{state === 'empty' && (
|
||||||
<p className="group-hover:text-primary font-signature text-muted-foreground group-hover:text-recipient-green text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200">
|
<p className="font-signature text-[clamp(0.575rem,25cqw,1.2rem)] text-xl text-muted-foreground duration-200 group-hover:text-primary group-hover:text-recipient-green">
|
||||||
<Trans>Signature</Trans>
|
<Trans>Signature</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -259,7 +262,7 @@ export const DocumentSigningSignatureField = ({
|
|||||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
|
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
|
||||||
<p
|
<p
|
||||||
ref={signatureRef}
|
ref={signatureRef}
|
||||||
className="font-signature text-muted-foreground w-full overflow-hidden break-all text-center leading-tight duration-200"
|
className="w-full overflow-hidden break-all text-center font-signature leading-tight text-muted-foreground duration-200"
|
||||||
style={{ fontSize: `${fontSize}rem` }}
|
style={{ fontSize: `${fontSize}rem` }}
|
||||||
>
|
>
|
||||||
{signature?.typedSignature}
|
{signature?.typedSignature}
|
||||||
@@ -272,12 +275,13 @@ export const DocumentSigningSignatureField = ({
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans>
|
<Trans>
|
||||||
Sign as {recipient.name}{' '}
|
Sign as {recipient.name}{' '}
|
||||||
<div className="text-muted-foreground h-5">({recipient.email})</div>
|
<div className="h-5 text-muted-foreground">({recipient.email})</div>
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
fullName={fullName}
|
||||||
value={localSignature ?? ''}
|
value={localSignature ?? ''}
|
||||||
onChange={({ value }) => setLocalSignature(value)}
|
onChange={({ value }) => setLocalSignature(value)}
|
||||||
typedSignatureEnabled={typedSignatureEnabled}
|
typedSignatureEnabled={typedSignatureEnabled}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||||
|
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
||||||
import {
|
import {
|
||||||
@@ -31,9 +32,13 @@ import { useCurrentTeam } from '~/providers/team';
|
|||||||
|
|
||||||
export type DocumentUploadButtonLegacyProps = {
|
export type DocumentUploadButtonLegacyProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
type: EnvelopeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => {
|
export const DocumentUploadButtonLegacy = ({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
}: DocumentUploadButtonLegacyProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
@@ -54,8 +59,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||||
|
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||||
|
|
||||||
const disabledMessage = useMemo(() => {
|
const disabledMessage = useMemo(() => {
|
||||||
|
if (!user.emailVerified) {
|
||||||
|
return msg`Verify your email to upload documents.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors for templates.
|
||||||
|
if (type === EnvelopeType.TEMPLATE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (organisation.subscription && remaining.documents === 0) {
|
if (organisation.subscription && remaining.documents === 0) {
|
||||||
return msg`Document upload disabled due to unpaid invoices`;
|
return msg`Document upload disabled due to unpaid invoices`;
|
||||||
}
|
}
|
||||||
@@ -64,11 +79,8 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
return msg`You have reached your document limit.`;
|
return msg`You have reached your document limit.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.emailVerified) {
|
|
||||||
return msg`Verify your email to upload documents.`;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [remaining.documents, user.emailVerified, team]);
|
}, [remaining.documents, user.emailVerified, team, type]);
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
@@ -80,44 +92,62 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
meta: {
|
meta: {
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
},
|
},
|
||||||
} satisfies TCreateDocumentPayloadSchema;
|
} satisfies TCreateDocumentPayloadSchema | TCreateTemplatePayloadSchema;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('payload', JSON.stringify(payload));
|
formData.append('payload', JSON.stringify(payload));
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const { envelopeId: id } = await createDocument(formData);
|
// Handle legacy document creation.
|
||||||
|
if (type === EnvelopeType.DOCUMENT) {
|
||||||
|
const { envelopeId: id } = await createDocument(formData);
|
||||||
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
|
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document uploaded`),
|
title: _(msg`Document uploaded`),
|
||||||
description: _(msg`Your document has been uploaded successfully.`),
|
description: _(msg`Your document has been uploaded successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.capture('App: Document Uploaded', {
|
analytics.capture('App: Document Uploaded', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
documentId: id,
|
documentId: id,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle legacy template creation.
|
||||||
|
if (type === EnvelopeType.TEMPLATE) {
|
||||||
|
const { envelopeId: id } = await createTemplate(formData);
|
||||||
|
|
||||||
|
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Template document uploaded`),
|
||||||
|
description: _(
|
||||||
|
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
|
||||||
|
),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => msg`You have reached the limit of the number of files per envelope`,
|
() => msg`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => msg`An error occurred while uploading your document.`);
|
.otherwise(() => msg`An error occurred while uploading your document.`);
|
||||||
|
|
||||||
@@ -149,17 +179,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
|
|||||||
<div>
|
<div>
|
||||||
<DocumentUploadButtonPrimitive
|
<DocumentUploadButtonPrimitive
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={remaining.documents === 0 || !user.emailVerified}
|
disabled={disabledMessage !== undefined}
|
||||||
disabledMessage={disabledMessage}
|
disabledMessage={disabledMessage}
|
||||||
onDrop={async (files) => onFileDrop(files[0])}
|
onDrop={async (files) => onFileDrop(files[0])}
|
||||||
onDropRejected={onFileDropRejected}
|
onDropRejected={onFileDropRejected}
|
||||||
type={EnvelopeType.DOCUMENT}
|
type={type}
|
||||||
internalVersion="1"
|
internalVersion="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
{team?.id === undefined &&
|
{team?.id === undefined &&
|
||||||
|
type === EnvelopeType.DOCUMENT &&
|
||||||
remaining.documents > 0 &&
|
remaining.documents > 0 &&
|
||||||
Number.isFinite(remaining.documents) && (
|
Number.isFinite(remaining.documents) && (
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
||||||
import { Link, useSearchParams } from 'react-router';
|
import { Link, useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual } from 'remeda';
|
import { isDeepEqual } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/al
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
||||||
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
||||||
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
||||||
@@ -81,6 +82,8 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const selectedField = useMemo(
|
const selectedField = useMemo(
|
||||||
() => structuredClone(editorFields.selectedField),
|
() => structuredClone(editorFields.selectedField),
|
||||||
@@ -135,6 +138,22 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDetectClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full">
|
<div className="relative flex h-full">
|
||||||
<div className="flex w-full flex-col overflow-y-auto">
|
<div className="flex w-full flex-col overflow-y-auto">
|
||||||
@@ -230,34 +249,36 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Button
|
||||||
<>
|
type="button"
|
||||||
<Button
|
variant="outline"
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
className="mt-4 w-full"
|
||||||
size="sm"
|
onClick={onDetectClick}
|
||||||
className="mt-4 w-full"
|
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||||
onClick={() => setIsAiFieldDialogOpen(true)}
|
title={
|
||||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
envelope.status !== DocumentStatus.DRAFT
|
||||||
title={
|
? _(msg`You can only detect fields in draft envelopes`)
|
||||||
envelope.status !== DocumentStatus.DRAFT
|
: undefined
|
||||||
? _(msg`You can only detect fields in draft envelopes`)
|
}
|
||||||
: undefined
|
>
|
||||||
}
|
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||||
>
|
<Trans>Detect with AI</Trans>
|
||||||
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
</Button>
|
||||||
<Trans>Detect with AI</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<AiFieldDetectionDialog
|
<AiFieldDetectionDialog
|
||||||
open={isAiFieldDialogOpen}
|
open={isAiFieldDialogOpen}
|
||||||
onOpenChange={setIsAiFieldDialogOpen}
|
onOpenChange={setIsAiFieldDialogOpen}
|
||||||
onComplete={onFieldDetectionComplete}
|
onComplete={onFieldDetectionComplete}
|
||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Field details section. */}
|
{/* Field details section. */}
|
||||||
|
|||||||
+71
-49
@@ -8,13 +8,13 @@ import {
|
|||||||
type SensorAPI,
|
type SensorAPI,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { plural } from '@lingui/core/macro';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual, prop, sortBy } from 'remeda';
|
import { isDeepEqual, prop, sortBy } from 'remeda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -28,6 +28,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';
|
||||||
@@ -62,6 +63,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
@@ -70,10 +72,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(),
|
||||||
@@ -99,11 +98,19 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
|
||||||
// AI recipient detection dialog state
|
// AI recipient detection dialog state
|
||||||
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const onAiDialogOpenChange = (open: boolean) => {
|
const onAiDialogOpenChange = (open: boolean) => {
|
||||||
|
if (open && !team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
setIsAiDialogOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsAiDialogOpen(open);
|
setIsAiDialogOpen(open);
|
||||||
|
|
||||||
if (!open && searchParams.get('ai') === 'true') {
|
if (!open && searchParams.get('ai') === 'true') {
|
||||||
@@ -120,6 +127,22 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDetectRecipientsClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
@@ -228,12 +251,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(),
|
||||||
);
|
);
|
||||||
@@ -331,8 +355,14 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Recipients added`,
|
title: plural(detectedRecipients.length, {
|
||||||
description: t`${detectedRecipients.length} recipient(s) have been added from AI detection.`,
|
one: `Recipient added`,
|
||||||
|
other: `Recipients added`,
|
||||||
|
}),
|
||||||
|
description: plural(detectedRecipients.length, {
|
||||||
|
one: `# recipient have been added from AI detection.`,
|
||||||
|
other: `# recipients have been added from AI detection.`,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -558,21 +588,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;
|
||||||
@@ -641,25 +657,27 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
type="button"
|
||||||
type="button"
|
size="sm"
|
||||||
size="sm"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
onClick={onDetectRecipientsClick}
|
||||||
onClick={() => setIsAiDialogOpen(true)}
|
>
|
||||||
>
|
<SparklesIcon className="h-4 w-4" />
|
||||||
<SparklesIcon className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
{team.preferences.aiFeaturesEnabled ? (
|
||||||
<Trans>Detect recipients with AI</Trans>
|
<Trans>Detect recipients with AI</Trans>
|
||||||
</TooltipContent>
|
) : (
|
||||||
</Tooltip>
|
<Trans>Enable AI detection</Trans>
|
||||||
)}
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -736,9 +754,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -924,7 +940,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showAdvancedSettings && index === 0 && (
|
{!showAdvancedSettings && index === 0 && (
|
||||||
<FormLabel required>
|
<FormLabel>
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
@@ -978,7 +994,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 ||
|
||||||
@@ -1118,6 +1134,12 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-4
@@ -687,8 +687,10 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>Reply To Email</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Reply To Email{' '}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -726,8 +728,9 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex flex-row items-center">
|
<FormLabel className="flex flex-row items-center">
|
||||||
<Trans>Message</Trans>{' '}
|
<Trans>
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
Message <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function EnvelopeEditor() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="size"
|
layout="size"
|
||||||
layoutId="document-flow-container-step"
|
layoutId="document-flow-container-step"
|
||||||
className="absolute inset-y-0 left-0 bg-documenso"
|
className="absolute inset-y-0 left-0 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
|
|
||||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||||
return (
|
return (
|
||||||
<fieldset className="embed--DocumentWidgetForm dark:bg-background border-border rounded-2xl sm:border sm:p-3">
|
<fieldset className="embed--DocumentWidgetForm rounded-2xl border-border sm:border sm:p-3 dark:bg-background">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
||||||
value={selectedAssistantRecipient?.id?.toString()}
|
value={selectedAssistantRecipient?.id?.toString()}
|
||||||
@@ -54,7 +54,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
.map((r) => (
|
.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={r.id}
|
key={r.id}
|
||||||
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
className="relative flex flex-col gap-4 rounded-lg border border-border bg-widget p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -69,15 +69,15 @@ export default function EnvelopeSignerForm() {
|
|||||||
{r.name}
|
{r.name}
|
||||||
|
|
||||||
{r.id === recipient.id && (
|
{r.id === recipient.id && (
|
||||||
<span className="text-muted-foreground ml-2">
|
<span className="ml-2 text-muted-foreground">
|
||||||
<Trans>(You)</Trans>
|
<Trans>(You)</Trans>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">{r.email}</p>
|
<p className="text-xs text-muted-foreground">{r.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-xs leading-[inherit]">
|
<div className="text-xs leading-[inherit] text-muted-foreground">
|
||||||
<Plural
|
<Plural
|
||||||
value={assistantFields.filter((field) => field.recipientId === r.id).length}
|
value={assistantFields.filter((field) => field.recipientId === r.id).length}
|
||||||
one="# field"
|
one="# field"
|
||||||
@@ -103,7 +103,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="full-name"
|
id="full-name"
|
||||||
className="bg-background mt-2"
|
className="mt-2 bg-background"
|
||||||
value={fullName}
|
value={fullName}
|
||||||
disabled={isNameLocked}
|
disabled={isNameLocked}
|
||||||
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
||||||
@@ -119,6 +119,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
<SignaturePadDialog
|
<SignaturePadDialog
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
fullName={fullName}
|
||||||
value={signature ?? ''}
|
value={signature ?? ''}
|
||||||
onChange={(v) => setSignature(v ?? '')}
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
|
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
.with({ type: FieldType.SIGNATURE }, (field) => {
|
.with({ type: FieldType.SIGNATURE }, (field) => {
|
||||||
handleSignatureFieldClick({
|
handleSignatureFieldClick({
|
||||||
field,
|
field,
|
||||||
|
fullName,
|
||||||
signature,
|
signature,
|
||||||
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
|
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
|
||||||
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
|
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,14 +123,14 @@ export const EnvelopeDropZoneWrapper = ({
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => t`You have reached the limit of the number of files per envelope`,
|
() => t`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => t`An error occurred during upload.`);
|
.otherwise(() => t`An error occurred during upload.`);
|
||||||
|
|
||||||
|
|||||||
@@ -126,14 +126,14 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
|
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||||
() => t`You have reached the limit of the number of files per envelope`,
|
() => t`You have reached the limit of the number of files per envelope.`,
|
||||||
)
|
)
|
||||||
.otherwise(() => t`An error occurred while uploading your document.`);
|
.otherwise(() => t`An error occurred while uploading your document.`);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
|||||||
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
||||||
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
||||||
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
|
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
|
||||||
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
|
|
||||||
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy';
|
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy';
|
||||||
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
@@ -70,7 +69,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
<div
|
<div
|
||||||
className="text-muted-foreground hover:text-muted-foreground/80 flex flex-1 items-center text-sm font-medium"
|
className="flex flex-1 items-center text-sm font-medium text-muted-foreground hover:text-muted-foreground/80"
|
||||||
data-testid="folder-grid-breadcrumbs"
|
data-testid="folder-grid-breadcrumbs"
|
||||||
>
|
>
|
||||||
<Link to={formatRootPath()} className="flex items-center">
|
<Link to={formatRootPath()} className="flex items-center">
|
||||||
@@ -100,10 +99,9 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
||||||
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
||||||
|
|
||||||
{type === FolderType.DOCUMENT ? (
|
{/* If you delete this, delete the component as well. */}
|
||||||
<DocumentUploadButtonLegacy /> // If you delete this, delete the component as well.
|
{organisation.organisationClaim.flags.allowLegacyEnvelopes && (
|
||||||
) : (
|
<DocumentUploadButtonLegacy type={type} />
|
||||||
<TemplateCreateDialog folderId={parentId ?? undefined} /> // If you delete this, delete the component as well.
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FolderCreateDialog type={type} />
|
<FolderCreateDialog type={type} />
|
||||||
@@ -113,7 +111,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
<div key={index} className="border-border bg-card h-full rounded-lg border px-4 py-5">
|
<div key={index} className="h-full rounded-lg border border-border bg-card px-4 py-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Skeleton className="h-8 w-8 rounded" />
|
<Skeleton className="h-8 w-8 rounded" />
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
@@ -194,7 +192,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
{foldersData.folders.length > 12 && (
|
{foldersData.folders.length > 12 && (
|
||||||
<div className="mt-2 flex items-center justify-center">
|
<div className="mt-2 flex items-center justify-center">
|
||||||
<Link
|
<Link
|
||||||
className="text-muted-foreground hover:text-foreground text-sm font-medium"
|
className="text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||||
to={formatViewAllFoldersPath()}
|
to={formatViewAllFoldersPath()}
|
||||||
>
|
>
|
||||||
View all folders
|
View all folders
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-4 text-sm">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
<div>
|
<div>
|
||||||
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
disabled={envelope.recipients.some(
|
disabled={envelope.recipients.some(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.signingStatus !== SigningStatus.SIGNED &&
|
recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
recipient.role !== RecipientRole.CC,
|
||||||
)}
|
)}
|
||||||
onClick={() => resealDocument({ id: envelope.id })}
|
onClick={() => resealDocument({ id: envelope.id })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ const TeamDropdownMenu = ({ team }: { team: TGetOrganisationSessionResponse[0]['
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
<MoreVerticalIcon className="h-4 w-4" />
|
<MoreVerticalIcon className="h-4 w-4" />
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">
|
||||||
|
<Trans>Open menu</Trans>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ export default function OrganisationSettingsBrandingPage() {
|
|||||||
const settingsHeaderText = t`Branding Preferences`;
|
const settingsHeaderText = t`Branding Preferences`;
|
||||||
|
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||||
? t`Here you can set your general branding preferences`
|
? t`Here you can set your general branding preferences.`
|
||||||
: team
|
: team
|
||||||
? t`Here you can set branding preferences for your team`
|
? t`Here you can set branding preferences for your team.`
|
||||||
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
: t`Here you can set branding preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function OrganisationSettingsDocumentPage() {
|
|||||||
|
|
||||||
const settingsHeaderText = t`Document Preferences`;
|
const settingsHeaderText = t`Document Preferences`;
|
||||||
const settingsHeaderSubtitle = isPersonalLayoutMode
|
const settingsHeaderSubtitle = isPersonalLayoutMode
|
||||||
? t`Here you can set your general document preferences`
|
? t`Here you can set your general document preferences.`
|
||||||
: t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
: t`Here you can set document preferences for your organisation. Teams will inherit these settings by default.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function OrganisationSettingsGeneral() {
|
|||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={t`Email Preferences`}
|
title={t`Email Preferences`}
|
||||||
subtitle={t`You can manage your email preferences here`}
|
subtitle={t`You can manage your email preferences here.`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
+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();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function TeamEmailSettingsGeneral() {
|
|||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={t`Email Preferences`}
|
title={t`Email Preferences`}
|
||||||
subtitle={t`You can manage your email preferences here`}
|
subtitle={t`You can manage your email preferences here.`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
(log) =>
|
(log) =>
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
|
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
|
||||||
),
|
),
|
||||||
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT]: auditLogs[
|
||||||
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT
|
||||||
|
].filter((log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT),
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
|
||||||
].filter(
|
].filter(
|
||||||
@@ -245,11 +248,11 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
||||||
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
||||||
<div className="break-all">{recipient.email}</div>
|
<div className="break-all">{recipient.email}</div>
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
||||||
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -273,13 +276,13 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{signature.signature?.typedSignature && (
|
{signature.signature?.typedSignature && (
|
||||||
<p className="font-signature text-center text-sm">
|
<p className="text-center font-signature text-sm">
|
||||||
{signature.signature?.typedSignature}
|
{signature.signature?.typedSignature}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
||||||
<span className="block font-mono uppercase">
|
<span className="block font-mono uppercase">
|
||||||
{signature.secondaryId}
|
{signature.secondaryId}
|
||||||
@@ -290,14 +293,14 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
<p className="text-muted-foreground">N/A</p>
|
<p className="text-muted-foreground">N/A</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
<p className="mt-2 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm print:text-xs">
|
<p className="mt-1 text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||||
@@ -307,18 +310,22 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
|
|
||||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.EMAIL_SENT[0]
|
{logs.EMAIL_SENT[0]
|
||||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
: _(msg`Unknown`)}
|
: logs.DOCUMENT_SENT[0]
|
||||||
|
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||||
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
|
: _(msg`Unknown`)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_OPENED[0]
|
{logs.DOCUMENT_OPENED[0]
|
||||||
@@ -330,7 +337,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||||
@@ -341,7 +348,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||||
@@ -355,7 +362,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-sm text-muted-foreground print:text-xs">
|
||||||
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{recipient.signingStatus === SigningStatus.REJECTED
|
{recipient.signingStatus === SigningStatus.REJECTED
|
||||||
|
|||||||
@@ -371,8 +371,9 @@ const SigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV1Loade
|
|||||||
to="https://documenso.com"
|
to="https://documenso.com"
|
||||||
className="text-documenso-700 hover:text-documenso-600"
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
>
|
>
|
||||||
Check out Documenso.
|
Check out Documenso
|
||||||
</Link>
|
</Link>
|
||||||
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -470,8 +471,9 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
|||||||
to="https://documenso.com"
|
to="https://documenso.com"
|
||||||
className="text-documenso-700 hover:text-documenso-600"
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
>
|
>
|
||||||
Check out Documenso.
|
Check out Documenso
|
||||||
</Link>
|
</Link>
|
||||||
|
.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import satori from 'satori';
|
import satori from 'satori';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
||||||
|
import { svgToPng } from '@documenso/lib/utils/images/svg-to-png';
|
||||||
|
|
||||||
import type { Route } from './+types/share.$slug.opengraph';
|
import type { Route } from './+types/share.$slug.opengraph';
|
||||||
|
|
||||||
@@ -181,8 +181,7 @@ export const loader = async ({ params }: Route.LoaderArgs) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert SVG to PNG using sharp
|
const pngBuffer = await svgToPng(svg.toString());
|
||||||
const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
|
||||||
|
|
||||||
return new Response(pngBuffer, {
|
return new Response(pngBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||||
|
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
||||||
@@ -63,16 +62,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = await sharp(file)
|
const { content, contentType } = await loadLogo(file);
|
||||||
.toFormat('png', {
|
|
||||||
quality: 80,
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return new Response(Buffer.from(img), {
|
return new Response(content, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': contentType,
|
||||||
'Content-Length': img.length.toString(),
|
'Content-Length': content.length.toString(),
|
||||||
// Stale while revalidate for 1 hours to 24 hours
|
// Stale while revalidate for 1 hours to 24 hours
|
||||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||||
|
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||||
|
|
||||||
import type { Route } from './+types/branding.logo.team.$teamId';
|
import type { Route } from './+types/branding.logo.team.$teamId';
|
||||||
|
|
||||||
@@ -56,16 +55,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = await sharp(file)
|
const { content, contentType } = await loadLogo(file);
|
||||||
.toFormat('png', {
|
|
||||||
quality: 80,
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return new Response(img, {
|
return new Response(content, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': contentType,
|
||||||
'Content-Length': img.length.toString(),
|
'Content-Length': content.length.toString(),
|
||||||
// Stale while revalidate for 1 hours to 24 hours
|
// Stale while revalidate for 1 hours to 24 hours
|
||||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signat
|
|||||||
|
|
||||||
type HandleSignatureFieldClickOptions = {
|
type HandleSignatureFieldClickOptions = {
|
||||||
field: TFieldSignature;
|
field: TFieldSignature;
|
||||||
|
fullName?: string;
|
||||||
signature: string | null;
|
signature: string | null;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
uploadSignatureEnabled?: boolean;
|
uploadSignatureEnabled?: boolean;
|
||||||
@@ -17,8 +18,14 @@ type HandleSignatureFieldClickOptions = {
|
|||||||
export const handleSignatureFieldClick = async (
|
export const handleSignatureFieldClick = async (
|
||||||
options: HandleSignatureFieldClickOptions,
|
options: HandleSignatureFieldClickOptions,
|
||||||
): Promise<Extract<TSignEnvelopeFieldValue, { type: typeof FieldType.SIGNATURE }> | null> => {
|
): Promise<Extract<TSignEnvelopeFieldValue, { type: typeof FieldType.SIGNATURE }> | null> => {
|
||||||
const { field, signature, typedSignatureEnabled, uploadSignatureEnabled, drawSignatureEnabled } =
|
const {
|
||||||
options;
|
field,
|
||||||
|
fullName,
|
||||||
|
signature,
|
||||||
|
typedSignatureEnabled,
|
||||||
|
uploadSignatureEnabled,
|
||||||
|
drawSignatureEnabled,
|
||||||
|
} = options;
|
||||||
|
|
||||||
if (field.type !== FieldType.SIGNATURE) {
|
if (field.type !== FieldType.SIGNATURE) {
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
@@ -37,6 +44,7 @@ export const handleSignatureFieldClick = async (
|
|||||||
|
|
||||||
if (!signatureToInsert) {
|
if (!signatureToInsert) {
|
||||||
signatureToInsert = await SignFieldSignatureDialog.call({
|
signatureToInsert = await SignFieldSignatureDialog.call({
|
||||||
|
fullName,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
|
|||||||
@@ -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 {};
|
||||||
@@ -70,7 +70,6 @@
|
|||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
"remix-themes": "^2.0.4",
|
"remix-themes": "^2.0.4",
|
||||||
"satori": "^0.18.3",
|
"satori": "^0.18.3",
|
||||||
"sharp": "0.34.5",
|
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.18",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"ua-parser-js": "^1.0.41",
|
"ua-parser-js": "^1.0.41",
|
||||||
@@ -108,5 +107,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.7"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+290
-547
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.2",
|
"version": "2.2.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.2",
|
"version": "2.2.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "^15.5.7",
|
"next": "15.5.9",
|
||||||
"next-plausible": "^3.12.5",
|
"next-plausible": "^3.12.5",
|
||||||
"nextra": "^3",
|
"nextra": "^3",
|
||||||
"nextra-theme-docs": "^3",
|
"nextra-theme-docs": "^3",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"next": "^15.5.7"
|
"next": "15.5.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "2.2.2",
|
"version": "2.2.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.3",
|
"@cantoo/pdf-lib": "^2.5.3",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -167,7 +167,6 @@
|
|||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
"remix-themes": "^2.0.4",
|
"remix-themes": "^2.0.4",
|
||||||
"satori": "^0.18.3",
|
"satori": "^0.18.3",
|
||||||
"sharp": "0.34.5",
|
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.18",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"ua-parser-js": "^1.0.41",
|
"ua-parser-js": "^1.0.41",
|
||||||
@@ -666,58 +665,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",
|
||||||
@@ -5089,9 +5036,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.5.7",
|
"version": "15.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
||||||
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
|
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -5963,12 +5910,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 +12102,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 +14349,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 +17048,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 +17728,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 +19793,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 +19996,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 +21488,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 +26189,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",
|
||||||
@@ -26632,12 +26396,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^2.0.0",
|
"jwa": "^2.0.1",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -29148,12 +28912,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "15.5.7",
|
"version": "15.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
||||||
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
|
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.5.7",
|
"@next/env": "15.5.9",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@@ -29480,21 +29244,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 +31335,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 +31670,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 +36939,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.7",
|
||||||
"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,4 +1,4 @@
|
|||||||
import { type Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
|
||||||
@@ -58,3 +58,15 @@ const getCsrfToken = async (page: Page) => {
|
|||||||
|
|
||||||
return csrfToken;
|
return csrfToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkSessionValid = async (page: Page): Promise<boolean> => {
|
||||||
|
const { request } = page.context();
|
||||||
|
|
||||||
|
const response = await request.fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/session`, {
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await response.json();
|
||||||
|
|
||||||
|
return session.isAuthenticated === 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 }) => {
|
||||||
@@ -356,6 +364,25 @@ test('[TEAMS]: can create a template subfolder inside a template folder', async
|
|||||||
test('[TEAMS]: can create a template inside a template folder', async ({ page }) => {
|
test('[TEAMS]: can create a template inside a template folder', async ({ page }) => {
|
||||||
const { team, teamOwner } = await seedTeamDocuments();
|
const { team, teamOwner } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
const organisationClaim = await prisma.organisationClaim.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
organisation: {
|
||||||
|
id: team.organisationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.organisationClaim.update({
|
||||||
|
where: {
|
||||||
|
id: organisationClaim.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
flags: {
|
||||||
|
allowLegacyEnvelopes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const folder = await seedBlankFolder(teamOwner, team.id, {
|
const folder = await seedBlankFolder(teamOwner, team.id, {
|
||||||
createFolderOptions: {
|
createFolderOptions: {
|
||||||
name: 'Team Client Templates',
|
name: 'Team Client Templates',
|
||||||
@@ -372,16 +399,15 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
|
|||||||
|
|
||||||
await expect(page.getByText('Team Client Templates')).toBeVisible();
|
await expect(page.getByText('Team Client Templates')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Template (Legacy)' }).click();
|
// Upload document.
|
||||||
|
const [fileChooser] = await Promise.all([
|
||||||
|
page.waitForEvent('filechooser'),
|
||||||
|
page.getByRole('button', { name: 'Template (Legacy)' }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
await page.getByText('Upload Template Document').click();
|
await fileChooser.setFiles(
|
||||||
|
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
|
||||||
await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' });
|
);
|
||||||
|
|
||||||
await page
|
|
||||||
.locator('input[type="file"]')
|
|
||||||
.nth(0)
|
|
||||||
.setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
|
|
||||||
|
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
@@ -410,7 +436,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 +464,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 +492,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 +520,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 +553,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 +608,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 +799,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 +821,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 +843,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 +1010,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 +1064,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 +1119,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 +1174,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 +1229,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 +1282,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 +1335,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 +1388,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 +1441,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 +1494,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 +1547,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 +1600,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 +1653,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 +1700,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 +1747,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 +1794,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 +1841,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 +1888,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 +1935,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 +1982,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 +2029,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 +2076,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();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type Page, expect, test } from '@playwright/test';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout, checkSessionValid } from '../fixtures/authentication';
|
||||||
|
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
|
|
||||||
await page.goto('http://localhost:3000/signin');
|
await page.goto('http://localhost:3000/signin');
|
||||||
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Email' }).click();
|
await page.getByRole('textbox', { name: 'Email' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
@@ -24,7 +25,9 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
||||||
await page.getByRole('button', { name: 'Reset Password' }).click();
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
|
||||||
await expect(page.locator('body')).toContainText('Reset email sent', { timeout: 10000 });
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -109,3 +112,116 @@ test('[USER] can reset password via user settings', async ({ page }: { page: Pag
|
|||||||
await page.waitForURL('/settings/profile');
|
await page.waitForURL('/settings/profile');
|
||||||
await expect(page).toHaveURL('/settings/profile');
|
await expect(page).toHaveURL('/settings/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[USER] password reset invalidates all sessions', async ({ page }: { page: Page }) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/signin');
|
||||||
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`http://localhost:3000/reset-password/${foundToken.token}`);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat Password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Your password has been updated successfully.');
|
||||||
|
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: newPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/settings/profile');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[USER] password update invalidates other sessions but keeps current', async ({
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
page: Page;
|
||||||
|
}) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await page.getByLabel('Current password').fill(oldPassword);
|
||||||
|
await page.getByLabel('New password').fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Update password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Password updated');
|
||||||
|
|
||||||
|
const finalCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(finalCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/settings/security');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ auth.onError((err, c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle other errors
|
// Handle other errors
|
||||||
|
console.error('Unknown Error:', err);
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
code: AppErrorCode.UNKNOWN_ERROR,
|
code: AppErrorCode.UNKNOWN_ERROR,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { env } from '@documenso/lib/utils/env';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||||
|
import { invalidateSessions } from '../lib/session/session';
|
||||||
import { getCsrfCookie } from '../lib/session/session-cookies';
|
import { getCsrfCookie } from '../lib/session/session-cookies';
|
||||||
import { onAuthorize } from '../lib/utils/authorizer';
|
import { onAuthorize } from '../lib/utils/authorizer';
|
||||||
import { getSession } from '../lib/utils/get-session';
|
import { getSession } from '../lib/utils/get-session';
|
||||||
@@ -170,15 +171,38 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
const { password, currentPassword } = c.req.valid('json');
|
const { password, currentPassword } = c.req.valid('json');
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
const session = await getSession(c);
|
const { session, user } = await getSession(c);
|
||||||
|
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
userId: session.user.id,
|
userId: user.id,
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id satisfies number, // Incase we pass undefined somehow.
|
||||||
|
id: {
|
||||||
|
not: session.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((s) => s.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId: user.id,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -231,12 +255,33 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
|
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
await resetPassword({
|
const { userId } = await resetPassword({
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate all sessions after successful password reset
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: userId satisfies number, // Incase we pass undefined somehow.
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((session) => session.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user