mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
Merge branch 'main' into refactor-forms
This commit is contained in:
7
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -1,11 +1,10 @@
|
|||||||
name: "Bug Report"
|
name: 'Bug Report'
|
||||||
labels: ["bug"]
|
labels: ['bug']
|
||||||
description: Create a bug report to help us improve
|
description: Create a bug report to help us improve
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value:
|
value: Thank you for reporting an issue.
|
||||||
Thank you for reporting an issue.
|
|
||||||
Please fill in as much of the form below as you're able to.
|
Please fill in as much of the form below as you're able to.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "Feature Request"
|
name: 'Feature Request'
|
||||||
description: Suggest a new idea or enhancement for this project
|
description: Suggest a new idea or enhancement for this project
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/improvement.yml
vendored
2
.github/ISSUE_TEMPLATE/improvement.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "General Improvement"
|
name: 'General Improvement'
|
||||||
description: Suggest a minor enhancement or improvement for this project
|
description: Suggest a minor enhancement or improvement for this project
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
32
.github/dependabot.yml
vendored
32
.github/dependabot.yml
vendored
@ -4,29 +4,29 @@ updates:
|
|||||||
- package-ecosystem: 'github-actions'
|
- package-ecosystem: 'github-actions'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: 'weekly'
|
||||||
target-branch: "main"
|
target-branch: 'main'
|
||||||
labels:
|
labels:
|
||||||
- "ci dependencies"
|
- 'ci dependencies'
|
||||||
- "ci"
|
- 'ci'
|
||||||
open-pull-requests-limit: 0
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: 'npm'
|
||||||
directory: "/apps/marketing"
|
directory: '/apps/marketing'
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: 'weekly'
|
||||||
target-branch: "main"
|
target-branch: 'main'
|
||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- 'npm dependencies'
|
||||||
- "frontend"
|
- 'frontend'
|
||||||
open-pull-requests-limit: 0
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: 'npm'
|
||||||
directory: "/apps/web"
|
directory: '/apps/web'
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: 'weekly'
|
||||||
target-branch: "main"
|
target-branch: 'main'
|
||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- 'npm dependencies'
|
||||||
- "frontend"
|
- 'frontend'
|
||||||
open-pull-requests-limit: 0
|
open-pull-requests-limit: 0
|
||||||
|
|||||||
21
.github/pr-labeler.yml
vendored
Normal file
21
.github/pr-labeler.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'apps: marketing':
|
||||||
|
- apps/marketing/**
|
||||||
|
|
||||||
|
'apps: web':
|
||||||
|
- apps/web/**
|
||||||
|
|
||||||
|
'version bump 👀':
|
||||||
|
- '**/package.json'
|
||||||
|
- '**/package-lock.json'
|
||||||
|
|
||||||
|
'🚨 migrations 🚨':
|
||||||
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
'🚨 e2e changes 🚨':
|
||||||
|
- packages/app-tests/e2e/**
|
||||||
|
|
||||||
|
'🚨 .env changes 🚨':
|
||||||
|
- .env.example
|
||||||
|
|
||||||
|
'pkg: ee changes':
|
||||||
|
- packages/ee/**
|
||||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@ -1,10 +1,10 @@
|
|||||||
name: "Continuous Integration"
|
name: 'Continuous Integration'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
@ -19,12 +19,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
@ -43,10 +43,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: ./docker/build.sh
|
run: ./docker/build.sh
|
||||||
|
|
||||||
|
|||||||
44
.github/workflows/codeql-analysis.yml
vendored
44
.github/workflows/codeql-analysis.yml
vendored
@ -1,11 +1,11 @@
|
|||||||
name: "CodeQL"
|
name: 'CodeQL'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@ -19,30 +19,30 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript' ]
|
language: ['javascript']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Copy env
|
- name: Copy env
|
||||||
run: cp .env.example .env
|
run: cp .env.example .env
|
||||||
|
|
||||||
- name: Build Documenso
|
- name: Build Documenso
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
24
.github/workflows/deploy.yml
vendored
Normal file
24
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Push to release branch
|
||||||
|
run: |
|
||||||
|
git checkout release || git checkout -b release
|
||||||
|
git merge --ff-only main
|
||||||
|
git push origin release
|
||||||
41
.github/workflows/e2e-tests.yml
vendored
41
.github/workflows/e2e-tests.yml
vendored
@ -1,51 +1,50 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ['main']
|
||||||
jobs:
|
jobs:
|
||||||
e2e_tests:
|
e2e_tests:
|
||||||
|
name: "E2E Tests"
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Copy env
|
- name: Copy env
|
||||||
run: cp .env.example .env
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Start Services
|
||||||
|
run: npm run dx:up
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
- name: Generate Prisma Client
|
- name: Generate Prisma Client
|
||||||
run: npm run prisma:generate -w @documenso/prisma
|
run: npm run prisma:generate -w @documenso/prisma
|
||||||
|
|
||||||
- name: Create the database
|
- name: Create the database
|
||||||
run: npm run prisma:migrate-dev
|
run: npm run prisma:migrate-dev
|
||||||
|
|
||||||
|
- name: Seed the database
|
||||||
|
run: npm run prisma:seed
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: npm run ci
|
run: npm run ci
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: test-results
|
||||||
path: playwright-report/
|
path: "packages/app-tests/**/test-results/*"
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
env:
|
env:
|
||||||
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
|
||||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|||||||
29
.github/workflows/first-interaction.yml
vendored
Normal file
29
.github/workflows/first-interaction.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: 'Welcome New Contributors'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: ['opened']
|
||||||
|
issues:
|
||||||
|
types: ['opened']
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
welcome-message:
|
||||||
|
name: Welcome Contributors
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/first-interaction@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pr-message: |
|
||||||
|
Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀
|
||||||
|
<br /> Feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||||
|
issue-message: |
|
||||||
|
Thank you for opening your first issue and for being a part of the open signing revolution!
|
||||||
|
<br /> One of our team members will review it and get back to you as soon as it possible 💚
|
||||||
|
<br /> Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||||
59
.github/workflows/issue-assignee-check.yml
vendored
Normal file
59
.github/workflows/issue-assignee-check.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
name: 'Issue Assignee Check'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: ['assigned']
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
countIssues:
|
||||||
|
if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Octokit
|
||||||
|
run: npm install @octokit/rest@18
|
||||||
|
|
||||||
|
- name: Check Assigned User's Issue Count
|
||||||
|
id: parse-comment
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { Octokit } = require("@octokit/rest");
|
||||||
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||||
|
|
||||||
|
const username = context.payload.issue.assignee.login;
|
||||||
|
console.log(`Username Extracted: ${username}`);
|
||||||
|
|
||||||
|
const { data: issues } = await octokit.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
assignee: username,
|
||||||
|
state: 'open'
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueCount = issues.length;
|
||||||
|
console.log(`Issue Count For ${username}: ${issueCount}`);
|
||||||
|
|
||||||
|
if (issueCount > 3) {
|
||||||
|
let issueCountMessage = `### 🚨 Documenso Police 🚨`;
|
||||||
|
issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`;
|
||||||
|
|
||||||
|
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body: issueCountMessage,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
21
.github/workflows/issue-opened.yml
vendored
Normal file
21
.github/workflows/issue-opened.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: 'Label Issues'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: ['opened', 'reopened']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label_issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.addLabels({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: ["needs triage"]
|
||||||
|
})
|
||||||
20
.github/workflows/pr-labeler.yml
vendored
Normal file
20
.github/workflows/pr-labeler.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: 'PR Labeler'
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request_target
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
labeler:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v4
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
sync-labels: ''
|
||||||
60
.github/workflows/pr-review-reminder.yml
vendored
Normal file
60
.github/workflows/pr-review-reminder.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: 'PR Review Reminder'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: ['opened', 'reopened', 'ready_for_review', 'review_requested']
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checkPRs:
|
||||||
|
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'reopened' || 'ready_for_review' || 'review_requested')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Octokit
|
||||||
|
run: npm install @octokit/rest@18
|
||||||
|
|
||||||
|
- name: Check user's PRs awaiting review
|
||||||
|
id: parse-prs
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { Octokit } = require("@octokit/rest");
|
||||||
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||||
|
|
||||||
|
const username = context.payload.pull_request.user.login;
|
||||||
|
console.log(`Username Extracted: ${username}`);
|
||||||
|
|
||||||
|
const { data: pullRequests } = await octokit.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
sort: 'created',
|
||||||
|
direction: 'asc',
|
||||||
|
});
|
||||||
|
|
||||||
|
const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review'));
|
||||||
|
const prCount = userPullRequests.length;
|
||||||
|
console.log(`PR Count for ${username}: ${prCount}`);
|
||||||
|
|
||||||
|
if (prCount > 3) {
|
||||||
|
let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`;
|
||||||
|
|
||||||
|
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.payload.pull_request.number,
|
||||||
|
body: prReminderMessage,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
23
.github/workflows/semantic-pull-requests.yml
vendored
23
.github/workflows/semantic-pull-requests.yml
vendored
@ -17,5 +17,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
id: lint_pr_title
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Hey There! and thank you for opening this pull request! 📝👋🏼
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
|
```
|
||||||
|
|
||||||
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Thank you for following the naming conventions for pull request titles! 💚🚀
|
||||||
|
|||||||
25
.github/workflows/stale.yml
vendored
Normal file
25
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: 'Mark Stale Issues and PRs'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */8 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-pr-stale: 30
|
||||||
|
days-before-issue-stale: 30
|
||||||
|
stale-issue-message: 'This issue has not seen activity for a while. It will be closed in 30 days unless further activity is detected'
|
||||||
|
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
|
||||||
|
close-issue-message: 'This issue has been closed because of inactivity.'
|
||||||
|
close-pr-message: 'This PR has been closed because of inactivity.'
|
||||||
|
exempt-pr-labels: 'WIP,on-hold,needs review'
|
||||||
|
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap'
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -6,5 +6,8 @@
|
|||||||
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
||||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"javascript.preferences.useAliasesForRenames": false,
|
"javascript.preferences.useAliasesForRenames": false,
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true
|
||||||
}
|
}
|
||||||
|
|||||||
13
apps/marketing/content/careers.mdx
Normal file
13
apps/marketing/content/careers.mdx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: Careers at Documenso
|
||||||
|
---
|
||||||
|
|
||||||
|
# Careers at Documenso
|
||||||
|
|
||||||
|
So you love Documenso and all the things that we do and now you want to work with us to unlock the future of open signing?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Positions
|
||||||
|
|
||||||
|
Unfortunately we have no open positions available at the moment. Our team has grown and so we must grow with it, please check back from time to time as now is not forever and we may be hiring again in the future.
|
||||||
@ -29,7 +29,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('relative max-w-[100vw] pt-20 md:pt-28', {
|
className={cn('relative flex min-h-[100vh] max-w-[100vw] flex-col pt-20 md:pt-28', {
|
||||||
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
|
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -41,7 +41,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
|||||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div>
|
<div className="relative mx-auto max-w-screen-xl flex-1 px-4 lg:px-8">{children}</div>
|
||||||
|
|
||||||
<Footer className="bg-background border-muted mt-24 border-t" />
|
<Footer className="bg-background border-muted mt-24 border-t" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,15 +17,14 @@ import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add
|
|||||||
import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
|
||||||
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
|
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
|
||||||
import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
||||||
import {
|
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||||
DocumentFlowFormContainer,
|
|
||||||
DocumentFlowFormContainerHeader,
|
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
|
||||||
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type SinglePlayerModeStep = 'fields' | 'sign';
|
const SinglePlayerModeSteps = ['fields', 'sign'] as const;
|
||||||
|
type SinglePlayerModeStep = (typeof SinglePlayerModeSteps)[number];
|
||||||
|
|
||||||
// !: This entire file is a hack to get around failed prerendering of
|
// !: This entire file is a hack to get around failed prerendering of
|
||||||
// !: the Single Player Mode page. This regression was introduced during
|
// !: the Single Player Mode page. This regression was introduced during
|
||||||
@ -226,37 +225,35 @@ export const SinglePlayerClient = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||||
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
|
<DocumentFlowFormContainer
|
||||||
<DocumentFlowFormContainerHeader
|
className="top-24 lg:h-[calc(100vh-7rem)]"
|
||||||
title={currentDocumentFlow.title}
|
onSubmit={(e) => e.preventDefault()}
|
||||||
description={currentDocumentFlow.description}
|
>
|
||||||
/>
|
<Stepper
|
||||||
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
{/* Add fields to PDF page. */}
|
setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
|
||||||
{step === 'fields' && (
|
>
|
||||||
|
{/* Add fields to PDF page. */}
|
||||||
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
|
||||||
<AddFieldsFormPartial
|
<AddFieldsFormPartial
|
||||||
documentFlow={documentFlow.fields}
|
documentFlow={documentFlow.fields}
|
||||||
hideRecipients={true}
|
hideRecipients={true}
|
||||||
recipients={uploadedFile ? [placeholderRecipient] : []}
|
recipients={uploadedFile ? [placeholderRecipient] : []}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onFieldsSubmit}
|
onSubmit={onFieldsSubmit}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Enter user details and signature. */}
|
{/* Enter user details and signature. */}
|
||||||
{step === 'sign' && (
|
|
||||||
<AddSignatureFormPartial
|
<AddSignatureFormPartial
|
||||||
documentFlow={documentFlow.sign}
|
documentFlow={documentFlow.sign}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onSignSubmit}
|
onSubmit={onSignSubmit}
|
||||||
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
|
||||||
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
|
||||||
/>
|
/>
|
||||||
)}
|
</Stepper>
|
||||||
</DocumentFlowFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,6 +31,7 @@ const FOOTER_LINKS = [
|
|||||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
||||||
{ href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
|
{ href: 'mailto:support@documenso.com', text: 'Support', target: '_blank' },
|
||||||
{ href: '/oss-friends', text: 'OSS Friends' },
|
{ href: '/oss-friends', text: 'OSS Friends' },
|
||||||
|
{ href: '/careers', text: 'Careers' },
|
||||||
{ href: '/privacy', text: 'Privacy' },
|
{ href: '/privacy', text: 'Privacy' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,11 @@ import { appRouter } from '@documenso/trpc/server/router';
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
maxDuration: 60,
|
maxDuration: 60,
|
||||||
|
api: {
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: '50mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"e2e:prepare": "next build && next start",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"clean": "rimraf .next && rimraf node_modules",
|
"clean": "rimraf .next && rimraf node_modules",
|
||||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
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';
|
||||||
@ -18,12 +18,10 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
|
|||||||
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||||
import { AddTitleFormPartial } from '@documenso/ui/primitives/document-flow/add-title';
|
import { AddTitleFormPartial } from '@documenso/ui/primitives/document-flow/add-title';
|
||||||
import type { TAddTitleFormSchema } from '@documenso/ui/primitives/document-flow/add-title.types';
|
import type { TAddTitleFormSchema } from '@documenso/ui/primitives/document-flow/add-title.types';
|
||||||
import {
|
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||||
DocumentFlowFormContainer,
|
|
||||||
DocumentFlowFormContainerHeader,
|
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
|
||||||
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type EditDocumentFormProps = {
|
export type EditDocumentFormProps = {
|
||||||
@ -36,6 +34,7 @@ export type EditDocumentFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type EditDocumentStep = 'title' | 'signers' | 'fields' | 'subject';
|
type EditDocumentStep = 'title' | 'signers' | 'fields' | 'subject';
|
||||||
|
const EditDocumentSteps: EditDocumentStep[] = ['title', 'signers', 'fields', 'subject'];
|
||||||
|
|
||||||
export const EditDocumentForm = ({
|
export const EditDocumentForm = ({
|
||||||
className,
|
className,
|
||||||
@ -48,6 +47,7 @@ export const EditDocumentForm = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// controlled stepper state
|
||||||
const [step, setStep] = useState<EditDocumentStep>(
|
const [step, setStep] = useState<EditDocumentStep>(
|
||||||
document.status === DocumentStatus.DRAFT ? 'title' : 'signers',
|
document.status === DocumentStatus.DRAFT ? 'title' : 'signers',
|
||||||
);
|
);
|
||||||
@ -67,24 +67,19 @@ export const EditDocumentForm = ({
|
|||||||
title: 'Add Signers',
|
title: 'Add Signers',
|
||||||
description: 'Add the people who will sign the document.',
|
description: 'Add the people who will sign the document.',
|
||||||
stepIndex: 2,
|
stepIndex: 2,
|
||||||
onBackStep: () => document.status === DocumentStatus.DRAFT && setStep('title'),
|
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
title: 'Add Fields',
|
title: 'Add Fields',
|
||||||
description: 'Add all relevant fields for each recipient.',
|
description: 'Add all relevant fields for each recipient.',
|
||||||
stepIndex: 3,
|
stepIndex: 3,
|
||||||
onBackStep: () => setStep('signers'),
|
|
||||||
},
|
},
|
||||||
subject: {
|
subject: {
|
||||||
title: 'Add Subject',
|
title: 'Add Subject',
|
||||||
description: 'Add the subject and message you wish to send to signers.',
|
description: 'Add the subject and message you wish to send to signers.',
|
||||||
stepIndex: 4,
|
stepIndex: 4,
|
||||||
onBackStep: () => setStep('fields'),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentDocumentFlow = documentFlow[step];
|
|
||||||
|
|
||||||
const onAddTitleFormSubmit = async (data: TAddTitleFormSchema) => {
|
const onAddTitleFormSubmit = async (data: TAddTitleFormSchema) => {
|
||||||
try {
|
try {
|
||||||
// Custom invocation server action
|
// Custom invocation server action
|
||||||
@ -116,7 +111,6 @@ export const EditDocumentForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
setStep('fields');
|
setStep('fields');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -138,7 +132,6 @@ export const EditDocumentForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
setStep('subject');
|
setStep('subject');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -181,6 +174,8 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentDocumentFlow = documentFlow[step];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
||||||
<Card
|
<Card
|
||||||
@ -197,56 +192,43 @@ export const EditDocumentForm = ({
|
|||||||
className="lg:h-[calc(100vh-6rem)]"
|
className="lg:h-[calc(100vh-6rem)]"
|
||||||
onSubmit={(e) => e.preventDefault()}
|
onSubmit={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DocumentFlowFormContainerHeader
|
<Stepper
|
||||||
title={currentDocumentFlow.title}
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
description={currentDocumentFlow.description}
|
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
|
||||||
/>
|
>
|
||||||
|
|
||||||
{step === 'title' && (
|
|
||||||
<AddTitleFormPartial
|
<AddTitleFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.title}
|
documentFlow={documentFlow.title}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
document={document}
|
document={document}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
onSubmit={onAddTitleFormSubmit}
|
onSubmit={onAddTitleFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'signers' && (
|
|
||||||
<AddSignersFormPartial
|
<AddSignersFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
document={document}
|
document={document}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'fields' && (
|
|
||||||
<AddFieldsFormPartial
|
<AddFieldsFormPartial
|
||||||
key={fields.length}
|
key={fields.length}
|
||||||
documentFlow={documentFlow.fields}
|
documentFlow={documentFlow.fields}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
onSubmit={onAddFieldsFormSubmit}
|
onSubmit={onAddFieldsFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'subject' && (
|
|
||||||
<AddSubjectFormPartial
|
<AddSubjectFormPartial
|
||||||
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.subject}
|
documentFlow={documentFlow.subject}
|
||||||
document={document}
|
document={document}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
numberOfSteps={Object.keys(documentFlow).length}
|
|
||||||
onSubmit={onAddSubjectFormSubmit}
|
onSubmit={onAddSubjectFormSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
</Stepper>
|
||||||
</DocumentFlowFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
|||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DataTableActionButtonProps = {
|
export type DataTableActionButtonProps = {
|
||||||
row: Document & {
|
row: Document & {
|
||||||
@ -22,6 +23,7 @@ export type DataTableActionButtonProps = {
|
|||||||
|
|
||||||
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return null;
|
return null;
|
||||||
@ -37,39 +39,47 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
let document: DocumentWithData | null = null;
|
try {
|
||||||
|
let document: DocumentWithData | null = null;
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
document = await trpcClient.document.getDocumentById.query({
|
document = await trpcClient.document.getDocumentById.query({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document = await trpcClient.document.getDocumentByToken.query({
|
||||||
|
token: recipient.token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentData = document?.documentData;
|
||||||
|
|
||||||
|
if (!documentData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentBytes = await getFile(documentData);
|
||||||
|
|
||||||
|
const blob = new Blob([documentBytes], {
|
||||||
|
type: 'application/pdf',
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
document = await trpcClient.document.getDocumentByToken.query({
|
const link = window.document.createElement('a');
|
||||||
token: recipient.token,
|
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
|
||||||
|
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description: 'An error occurred while trying to download file.',
|
||||||
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentData = document?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentBytes = await getFile(documentData);
|
|
||||||
|
|
||||||
const blob = new Blob([documentBytes], {
|
|
||||||
type: 'application/pdf',
|
|
||||||
});
|
|
||||||
|
|
||||||
const link = window.document.createElement('a');
|
|
||||||
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
|
|
||||||
|
|
||||||
link.href = window.URL.createObjectURL(blob);
|
|
||||||
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
|
|
||||||
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
window.URL.revokeObjectURL(link.href);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
||||||
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
import { DeleteDocumentDialog } from './delete-document-dialog';
|
||||||
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
||||||
|
|
||||||
export type DataTableActionDropdownProps = {
|
export type DataTableActionDropdownProps = {
|
||||||
@ -60,7 +60,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
// const isPending = row.status === DocumentStatus.PENDING;
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
|
const isDocumentDeletable = isOwner;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
let document: DocumentWithData | null = null;
|
let document: DocumentWithData | null = null;
|
||||||
@ -161,8 +161,9 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
{isDocumentDeletable && (
|
{isDocumentDeletable && (
|
||||||
<DeleteDraftDocumentDialog
|
<DeleteDocumentDialog
|
||||||
id={row.id}
|
id={row.id}
|
||||||
|
status={row.status}
|
||||||
open={isDeleteDialogOpen}
|
open={isDeleteDialogOpen}
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useSession } from 'next-auth/react';
|
|||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||||
|
|
||||||
@ -74,12 +75,14 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) =>
|
||||||
<div className="flex items-center gap-x-4">
|
(!row.original.deletedAt ||
|
||||||
<DataTableActionButton row={row.original} />
|
row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
||||||
<DataTableActionDropdown row={row.original} />
|
<div className="flex items-center gap-x-4">
|
||||||
</div>
|
<DataTableActionButton row={row.original} />
|
||||||
),
|
<DataTableActionDropdown row={row.original} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
data={results.data}
|
data={results.data}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -10,41 +13,46 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DeleteDraftDocumentDialogProps = {
|
type DeleteDraftDocumentDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
status: DocumentStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteDraftDocumentDialog = ({
|
export const DeleteDocumentDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
status,
|
||||||
}: DeleteDraftDocumentDialogProps) => {
|
}: DeleteDraftDocumentDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isLoading } =
|
const [inputValue, setInputValue] = useState('');
|
||||||
trpcReact.document.deleteDraftDocument.useMutation({
|
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||||
onSuccess: () => {
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
toast({
|
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
|
||||||
title: 'Document deleted',
|
onSuccess: () => {
|
||||||
description: 'Your document has been successfully deleted.',
|
router.refresh();
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
onOpenChange(false);
|
toast({
|
||||||
},
|
title: 'Document deleted',
|
||||||
});
|
description: 'Your document has been successfully deleted.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
const onDraftDelete = async () => {
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDocument({ id });
|
await deleteDocument({ id, status });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
@ -55,6 +63,11 @@ export const DeleteDraftDocumentDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
setIsDeleteEnabled(event.target.value === 'delete');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -67,6 +80,17 @@ export const DeleteDraftDocumentDialog = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{status !== DocumentStatus.DRAFT && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onInputChange}
|
||||||
|
placeholder="Type 'delete' to confirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
<Button
|
<Button
|
||||||
@ -78,7 +102,14 @@ export const DeleteDraftDocumentDialog = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="button" loading={isLoading} onClick={onDraftDelete} className="flex-1">
|
<Button
|
||||||
|
type="button"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={onDelete}
|
||||||
|
disabled={!isDeleteEnabled}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -9,6 +9,7 @@ import { Loader } from 'lucide-react';
|
|||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
@ -23,6 +24,7 @@ export type UploadDocumentProps = {
|
|||||||
|
|
||||||
export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -55,6 +57,12 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
analytics.capture('App: Document Uploaded', {
|
||||||
|
userId: session?.user.id,
|
||||||
|
documentId: id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
router.push(`/documents/${id}`);
|
router.push(`/documents/${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
|
|||||||
let stripeCustomer: Stripe.Customer | null = null;
|
let stripeCustomer: Stripe.Customer | null = null;
|
||||||
|
|
||||||
// Find the Stripe customer for the current user subscription.
|
// Find the Stripe customer for the current user subscription.
|
||||||
if (existingSubscription) {
|
if (existingSubscription?.periodEnd && existingSubscription.periodEnd >= new Date()) {
|
||||||
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
||||||
|
|
||||||
if (!stripeCustomer) {
|
if (!stripeCustomer) {
|
||||||
|
|||||||
@ -67,18 +67,24 @@ export default async function CompletedSigningPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative mt-6 flex w-full flex-col items-center">
|
<div className="relative mt-6 flex w-full flex-col items-center">
|
||||||
{match(document.status)
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with(DocumentStatus.COMPLETED, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<div className="text-documenso-700 flex items-center text-center">
|
<div className="text-documenso-700 flex items-center text-center">
|
||||||
<CheckCircle2 className="mr-2 h-5 w-5" />
|
<CheckCircle2 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">Everyone has signed</span>
|
<span className="text-sm">Everyone has signed</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.with({ deletedAt: null }, () => (
|
||||||
<div className="flex items-center text-center text-blue-600">
|
<div className="flex items-center text-center text-blue-600">
|
||||||
<Clock8 className="mr-2 h-5 w-5" />
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">Waiting for others to sign</span>
|
<span className="text-sm">Waiting for others to sign</span>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<div className="flex items-center text-center text-red-600">
|
||||||
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
|
<span className="text-sm">Document no longer available to sign</span>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
@ -86,16 +92,22 @@ export default async function CompletedSigningPage({
|
|||||||
<span className="mt-1.5 block">"{document.title}"</span>
|
<span className="mt-1.5 block">"{document.title}"</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{match(document.status)
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with(DocumentStatus.COMPLETED, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
Everyone has signed! You will receive an Email copy of the signed document.
|
Everyone has signed! You will receive an Email copy of the signed document.
|
||||||
</p>
|
</p>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.with({ deletedAt: null }, () => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
You will receive an Email copy of the signed document once everyone has signed.
|
You will receive an Email copy of the signed document once everyone has signed.
|
||||||
</p>
|
</p>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
|
This document has been cancelled by the owner and is no longer available for others to
|
||||||
|
sign.
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -29,6 +30,7 @@ export type SigningFormProps = {
|
|||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
@ -60,6 +62,12 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
|
signerId: recipient.id,
|
||||||
|
documentId: document.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
router.push(`/sign/${recipient.token}/complete`);
|
router.push(`/sign/${recipient.token}/complete`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Clock8 } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||||
|
import type { Document, Signature } from '@documenso/prisma/client';
|
||||||
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
|
|
||||||
|
type NoLongerAvailableProps = {
|
||||||
|
document: Document;
|
||||||
|
recipientName: string;
|
||||||
|
recipientSignature: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoLongerAvailable = ({
|
||||||
|
document,
|
||||||
|
recipientName,
|
||||||
|
recipientSignature,
|
||||||
|
}: NoLongerAvailableProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-16 xl:pt-24">
|
||||||
|
<SigningCard3D
|
||||||
|
name={recipientName}
|
||||||
|
signature={recipientSignature}
|
||||||
|
signingCelebrationImage={signingCelebration}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mt-2 flex w-full flex-col items-center">
|
||||||
|
<div className="mt-8 flex items-center text-center text-red-600">
|
||||||
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
|
<span className="text-sm">Document Cancelled</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
|
<span className="mt-1.5 block">"{document.title}"</span>
|
||||||
|
is no longer available to sign
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
|
This document has been cancelled by the owner.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{session?.user ? (
|
||||||
|
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
||||||
|
Go Back Home
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground/60 mt-36 text-sm">
|
||||||
|
Want to send slick signing links like this one?{' '}
|
||||||
|
<Link
|
||||||
|
href="https://documenso.com"
|
||||||
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
|
>
|
||||||
|
Check out Documenso.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
|
|||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
@ -17,6 +18,7 @@ import { DateField } from './date-field';
|
|||||||
import { EmailField } from './email-field';
|
import { EmailField } from './email-field';
|
||||||
import { SigningForm } from './form';
|
import { SigningForm } from './form';
|
||||||
import { NameField } from './name-field';
|
import { NameField } from './name-field';
|
||||||
|
import { NoLongerAvailable } from './no-longer-available';
|
||||||
import { SigningProvider } from './provider';
|
import { SigningProvider } from './provider';
|
||||||
import { SignatureField } from './signature-field';
|
import { SignatureField } from './signature-field';
|
||||||
|
|
||||||
@ -55,6 +57,18 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
redirect(`/sign/${token}/complete`);
|
redirect(`/sign/${token}/complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [recipientSignature] = await getRecipientSignatures({ recipientId: recipient.id });
|
||||||
|
|
||||||
|
if (document.deletedAt) {
|
||||||
|
return (
|
||||||
|
<NoLongerAvailable
|
||||||
|
document={document}
|
||||||
|
recipientName={recipient.name}
|
||||||
|
recipientSignature={recipientSignature}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningProvider
|
<SigningProvider
|
||||||
email={recipient.email}
|
email={recipient.email}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
DOCUMENTS_PAGE_SHORTCUT,
|
DOCUMENTS_PAGE_SHORTCUT,
|
||||||
SETTINGS_PAGE_SHORTCUT,
|
SETTINGS_PAGE_SHORTCUT,
|
||||||
} from '@documenso/lib/constants/keyboard-shortcuts';
|
} from '@documenso/lib/constants/keyboard-shortcuts';
|
||||||
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -29,13 +30,20 @@ const DOCUMENTS_PAGES = [
|
|||||||
shortcut: DOCUMENTS_PAGE_SHORTCUT.replace('+', ''),
|
shortcut: DOCUMENTS_PAGE_SHORTCUT.replace('+', ''),
|
||||||
},
|
},
|
||||||
{ label: 'Draft documents', path: '/documents?status=DRAFT' },
|
{ label: 'Draft documents', path: '/documents?status=DRAFT' },
|
||||||
{ label: 'Completed documents', path: '/documents?status=COMPLETED' },
|
{
|
||||||
|
label: 'Completed documents',
|
||||||
|
path: '/documents?status=COMPLETED',
|
||||||
|
},
|
||||||
{ label: 'Pending documents', path: '/documents?status=PENDING' },
|
{ label: 'Pending documents', path: '/documents?status=PENDING' },
|
||||||
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
|
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SETTINGS_PAGES = [
|
const SETTINGS_PAGES = [
|
||||||
{ label: 'Settings', path: '/settings', shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', '') },
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
path: '/settings',
|
||||||
|
shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
|
||||||
|
},
|
||||||
{ label: 'Profile', path: '/settings/profile' },
|
{ label: 'Profile', path: '/settings/profile' },
|
||||||
{ label: 'Password', path: '/settings/password' },
|
{ label: 'Password', path: '/settings/password' },
|
||||||
];
|
];
|
||||||
@ -53,6 +61,29 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
|
||||||
|
trpcReact.document.searchDocuments.useQuery(
|
||||||
|
{
|
||||||
|
query: search,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchResults = useMemo(() => {
|
||||||
|
if (!searchDocumentsData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchDocumentsData.map((document) => ({
|
||||||
|
label: document.title,
|
||||||
|
path: `/documents/${document.id}`,
|
||||||
|
value:
|
||||||
|
document.title + ' ' + document.Recipient.map((recipient) => recipient.email).join(' '),
|
||||||
|
}));
|
||||||
|
}, [searchDocumentsData]);
|
||||||
|
|
||||||
const currentPage = pages[pages.length - 1];
|
const currentPage = pages[pages.length - 1];
|
||||||
|
|
||||||
const toggleOpen = () => {
|
const toggleOpen = () => {
|
||||||
@ -113,7 +144,13 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog commandProps={{ onKeyDown: handleKeyDown }} open={open} onOpenChange={setOpen}>
|
<CommandDialog
|
||||||
|
commandProps={{
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
@ -121,7 +158,17 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
{isSearchingDocuments ? (
|
||||||
|
<CommandEmpty>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="animate-spin">
|
||||||
|
<Loader />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandEmpty>
|
||||||
|
) : (
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
)}
|
||||||
{!currentPage && (
|
{!currentPage && (
|
||||||
<>
|
<>
|
||||||
<CommandGroup heading="Documents">
|
<CommandGroup heading="Documents">
|
||||||
@ -133,6 +180,11 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
<CommandGroup heading="Preferences">
|
<CommandGroup heading="Preferences">
|
||||||
<CommandItem onSelect={() => addPage('theme')}>Change theme</CommandItem>
|
<CommandItem onSelect={() => addPage('theme')}>Change theme</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<CommandGroup heading="Your documents">
|
||||||
|
<Commands push={push} pages={searchResults} />
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
||||||
@ -146,10 +198,14 @@ const Commands = ({
|
|||||||
pages,
|
pages,
|
||||||
}: {
|
}: {
|
||||||
push: (_path: string) => void;
|
push: (_path: string) => void;
|
||||||
pages: { label: string; path: string; shortcut?: string }[];
|
pages: { label: string; path: string; shortcut?: string; value?: string }[];
|
||||||
}) => {
|
}) => {
|
||||||
return pages.map((page) => (
|
return pages.map((page, idx) => (
|
||||||
<CommandItem key={page.path} onSelect={() => push(page.path)}>
|
<CommandItem
|
||||||
|
key={page.path + idx}
|
||||||
|
value={page.value ?? page.label}
|
||||||
|
onSelect={() => push(page.path)}
|
||||||
|
>
|
||||||
{page.label}
|
{page.label}
|
||||||
{page.shortcut && <CommandShortcut>{page.shortcut}</CommandShortcut>}
|
{page.shortcut && <CommandShortcut>{page.shortcut}</CommandShortcut>}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { LuGithub } from 'react-icons/lu';
|
|||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -56,7 +56,11 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
title="Profile Dropdown"
|
||||||
|
className="relative h-10 w-10 rounded-full"
|
||||||
|
>
|
||||||
<Avatar className="h-10 w-10">
|
<Avatar className="h-10 w-10">
|
||||||
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { signIn } from 'next-auth/react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
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';
|
||||||
@ -64,6 +65,11 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
password,
|
password,
|
||||||
callbackUrl: '/',
|
callbackUrl: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
analytics.capture('App: User Sign Up', {
|
||||||
|
email,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@ -4,6 +4,11 @@ import { appRouter } from '@documenso/trpc/server/router';
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
maxDuration: 60,
|
maxDuration: 60,
|
||||||
|
api: {
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: '50mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
export default trpcNext.createNextApiHandler({
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
'**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}': ['prettier --write'],
|
'**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}': ['prettier --write'],
|
||||||
|
'**/*.yml': ['prettier --write'],
|
||||||
};
|
};
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"apps/marketing": {
|
"apps/marketing": {
|
||||||
"name": "@documenso/marketing",
|
"name": "@documenso/marketing",
|
||||||
"version": "1.2.1",
|
"version": "1.2.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.2.1",
|
"version": "1.2.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
@ -19227,6 +19227,7 @@
|
|||||||
"start-server-and-test": "^2.0.1"
|
"start-server-and-test": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@documenso/prisma": "*",
|
||||||
"@documenso/web": "*",
|
"@documenso/web": "*",
|
||||||
"@playwright/test": "^1.18.1",
|
"@playwright/test": "^1.18.1",
|
||||||
"@types/node": "^20.8.2"
|
"@types/node": "^20.8.2"
|
||||||
|
|||||||
192
packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts
Normal file
192
packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { TEST_USERS } from '@documenso/prisma/seed/pr-711-deletion-of-documents';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
test('[PR-711]: seeded documents should be visible', async ({ page }) => {
|
||||||
|
const [sender, ...recipients] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(sender.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(recipient.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(recipient.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PR-711]: deleting a completed document should not remove it from recipients', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const [sender, ...recipients] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
await page.getByLabel('Email').fill(sender.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
// open actions menu
|
||||||
|
await page
|
||||||
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
|
.getByRole('cell', { name: 'Download' })
|
||||||
|
.getByRole('button')
|
||||||
|
.nth(1)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// delete document
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible();
|
||||||
|
|
||||||
|
// signout
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
await page.getByLabel('Email').fill(recipient.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(recipient.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto(`/sign/completed-token-${recipients.indexOf(recipient)}`);
|
||||||
|
await expect(page.getByText('Everyone has signed').nth(0)).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/documents');
|
||||||
|
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PR-711]: deleting a pending document should remove it from recipients', async ({ page }) => {
|
||||||
|
const [sender, ...recipients] = TEST_USERS;
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await page.goto(`/sign/pending-token-${recipients.indexOf(recipient)}`);
|
||||||
|
|
||||||
|
await expect(page.getByText('Waiting for others to sign').nth(0)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
await page.getByLabel('Email').fill(sender.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
// open actions menu
|
||||||
|
await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click();
|
||||||
|
|
||||||
|
// delete document
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible();
|
||||||
|
|
||||||
|
// signout
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
await page.getByLabel('Email').fill(recipient.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(recipient.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.goto(`/sign/pending-token-${recipients.indexOf(recipient)}`);
|
||||||
|
await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/documents');
|
||||||
|
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/signin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PR-711]: deleting a draft document should remove it without additional prompting', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const [sender] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
await page.getByLabel('Email').fill(sender.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
// open actions menu
|
||||||
|
await page
|
||||||
|
.locator('tr', { hasText: 'Document 1 - Draft' })
|
||||||
|
.getByRole('cell', { name: 'Edit' })
|
||||||
|
.getByRole('button')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// delete document
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
||||||
|
});
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { TEST_USERS } from '@documenso/prisma/seed/pr-713-add-document-search-to-command-menu';
|
||||||
|
|
||||||
|
test('[PR-713]: should see sent documents', async ({ page }) => {
|
||||||
|
const [user] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(user.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await page.keyboard.press('Meta+K');
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Type a command or search...').fill('sent');
|
||||||
|
await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// signout
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PR-713]: should see received documents', async ({ page }) => {
|
||||||
|
const [user] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(user.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await page.keyboard.press('Meta+K');
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Type a command or search...').fill('received');
|
||||||
|
await expect(page.getByRole('option', { name: '[713] Document - Received' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// signout
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PR-713]: should be able to search by recipient', async ({ page }) => {
|
||||||
|
const [user, recipient] = TEST_USERS;
|
||||||
|
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(user.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await page.keyboard.press('Meta+K');
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Type a command or search...').fill(recipient.email);
|
||||||
|
await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// signout
|
||||||
|
await page.getByTitle('Profile Dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||||
|
});
|
||||||
75
packages/app-tests/e2e/pr-718-add-stepper-component.spec.ts
Normal file
75
packages/app-tests/e2e/pr-718-add-stepper-component.spec.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { TEST_USER } from '@documenso/prisma/seed/pr-718-add-stepper-component';
|
||||||
|
|
||||||
|
test(`[PR-718]: should be able to create a document`, async ({ page }) => {
|
||||||
|
await page.goto('/signin');
|
||||||
|
|
||||||
|
const documentTitle = `example-${Date.now()}.pdf`;
|
||||||
|
|
||||||
|
// Sign in
|
||||||
|
await page.getByLabel('Email').fill(TEST_USER.email);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(TEST_USER.password);
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
// Upload document
|
||||||
|
const [fileChooser] = await Promise.all([
|
||||||
|
page.waitForEvent('filechooser'),
|
||||||
|
page.locator('input[type=file]').evaluate((e) => {
|
||||||
|
if (e instanceof HTMLInputElement) {
|
||||||
|
e.click();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf'));
|
||||||
|
|
||||||
|
// Wait to be redirected to the edit page
|
||||||
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
|
||||||
|
// Set title
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Title' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('Title').fill(documentTitle);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Add signers
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('Email*').fill('user1@example.com');
|
||||||
|
await page.getByLabel('Name').fill('User 1');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Add fields
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'User 1 Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Email Email' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
// Add subject and send
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
// Assert document was created
|
||||||
|
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
|
||||||
|
});
|
||||||
@ -10,9 +10,9 @@ test.use({ storageState: { cookies: [], origins: [] } });
|
|||||||
*/
|
*/
|
||||||
test.describe.configure({ mode: 'serial' });
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
const username = process.env.E2E_TEST_AUTHENTICATE_USERNAME;
|
const username = 'Test User';
|
||||||
const email = process.env.E2E_TEST_AUTHENTICATE_USER_EMAIL;
|
const email = 'test-user@auth-flow.documenso.com';
|
||||||
const password = process.env.E2E_TEST_AUTHENTICATE_USER_PASSWORD;
|
const password = 'Password123';
|
||||||
|
|
||||||
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
|
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
|
||||||
await page.goto('/signup');
|
await page.goto('/signup');
|
||||||
|
|||||||
@ -6,13 +6,14 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:dev": "playwright test",
|
"test:dev": "playwright test",
|
||||||
"test:e2e": "start-server-and-test \"(cd ../../apps/web && npm run start)\" http://localhost:3000 \"playwright test\""
|
"test:e2e": "start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.18.1",
|
"@playwright/test": "^1.18.1",
|
||||||
"@types/node": "^20.8.2",
|
"@types/node": "^20.8.2",
|
||||||
|
"@documenso/prisma": "*",
|
||||||
"@documenso/web": "*"
|
"@documenso/web": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -28,8 +28,12 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* 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-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
|
video: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
timeout: 30_000,
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|||||||
8
packages/app-tests/tsconfig.json
Normal file
8
packages/app-tests/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["@documenso/tsconfig/process-env.d.ts"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import { buffer } from 'micro';
|
import { buffer } from 'micro';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@ -174,6 +175,13 @@ export const stripeWebhookHandler = async (
|
|||||||
|
|
||||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||||
|
|
||||||
|
if (subscription.status === 'incomplete_expired') {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Webhook received',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.subscription.findFirst({
|
const result = await prisma.subscription.findFirst({
|
||||||
select: {
|
select: {
|
||||||
userId: true,
|
userId: true,
|
||||||
@ -218,6 +226,13 @@ export const stripeWebhookHandler = async (
|
|||||||
|
|
||||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||||
|
|
||||||
|
if (subscription.status === 'incomplete_expired') {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Webhook received',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.subscription.findFirst({
|
const result = await prisma.subscription.findFirst({
|
||||||
select: {
|
select: {
|
||||||
userId: true,
|
userId: true,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Stripe from 'stripe';
|
import type Stripe from 'stripe';
|
||||||
|
|
||||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { Section, Text } from '../components';
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
|
export interface TemplateDocumentCancelProps {
|
||||||
|
inviterName: string;
|
||||||
|
inviterEmail: string;
|
||||||
|
documentName: string;
|
||||||
|
assetBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentCancel = ({
|
||||||
|
inviterName,
|
||||||
|
documentName,
|
||||||
|
assetBaseUrl,
|
||||||
|
}: TemplateDocumentCancelProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
|
{inviterName} has cancelled the document
|
||||||
|
<br />"{documentName}"
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
You don't need to sign it anymore.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentCancel;
|
||||||
66
packages/email/templates/document-cancel.tsx
Normal file
66
packages/email/templates/document-cancel.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { Body, Container, Head, Hr, Html, Img, Preview, Section, Tailwind } from '../components';
|
||||||
|
import type { TemplateDocumentCancelProps } from '../template-components/template-document-cancel';
|
||||||
|
import { TemplateDocumentCancel } from '../template-components/template-document-cancel';
|
||||||
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
|
export type DocumentCancelEmailTemplateProps = Partial<TemplateDocumentCancelProps>;
|
||||||
|
|
||||||
|
export const DocumentCancelTemplate = ({
|
||||||
|
inviterName = 'Lucas Smith',
|
||||||
|
inviterEmail = 'lucas@documenso.com',
|
||||||
|
documentName = 'Open Source Pledge.pdf',
|
||||||
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
|
}: DocumentCancelEmailTemplateProps) => {
|
||||||
|
const previewText = `${inviterName} has cancelled the document ${documentName}, you don't need to sign it anymore.`;
|
||||||
|
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="mx-auto my-auto bg-white font-sans">
|
||||||
|
<Section>
|
||||||
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
|
<Section>
|
||||||
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TemplateDocumentCancel
|
||||||
|
inviterName={inviterName}
|
||||||
|
inviterEmail={inviterEmail}
|
||||||
|
documentName={documentName}
|
||||||
|
assetBaseUrl={assetBaseUrl}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||||
|
|
||||||
|
<Container className="mx-auto max-w-xl">
|
||||||
|
<TemplateFooter />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentCancelTemplate;
|
||||||
88
packages/lib/server-only/document/delete-document.ts
Normal file
88
packages/lib/server-only/document/delete-document.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import { render } from '@documenso/email/render';
|
||||||
|
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
|
|
||||||
|
export type DeleteDocumentOptions = {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
status: DocumentStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDocument = async ({ id, userId, status }: DeleteDocumentOptions) => {
|
||||||
|
// if the document is a draft, hard-delete
|
||||||
|
if (status === DocumentStatus.DRAFT) {
|
||||||
|
return await prisma.document.delete({ where: { id, userId, status: DocumentStatus.DRAFT } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the document is pending, send cancellation emails to all recipients
|
||||||
|
if (status === DocumentStatus.PENDING) {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Recipient: true,
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.Recipient.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
document.Recipient.map(async (recipient) => {
|
||||||
|
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const template = createElement(DocumentCancelTemplate, {
|
||||||
|
documentName: document.title,
|
||||||
|
inviterName: user.name || undefined,
|
||||||
|
inviterEmail: user.email,
|
||||||
|
assetBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
address: recipient.email,
|
||||||
|
name: recipient.name,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: 'Document Cancelled',
|
||||||
|
html: render(template),
|
||||||
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the document is not a draft, only soft-delete.
|
||||||
|
return await prisma.document.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,13 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export type DeleteDraftDocumentOptions = {
|
|
||||||
id: number;
|
|
||||||
userId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteDraftDocument = async ({ id, userId }: DeleteDraftDocumentOptions) => {
|
|
||||||
return await prisma.document.delete({ where: { id, userId, status: DocumentStatus.DRAFT } });
|
|
||||||
};
|
|
||||||
@ -55,17 +55,25 @@ export const findDocuments = async ({
|
|||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: {
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
not: ExtendedDocumentStatus.DRAFT,
|
|
||||||
},
|
|
||||||
Recipient: {
|
Recipient: {
|
||||||
some: {
|
some: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
.with(ExtendedDocumentStatus.INBOX, () => ({
|
.with(ExtendedDocumentStatus.INBOX, () => ({
|
||||||
@ -78,26 +86,29 @@ export const findDocuments = async ({
|
|||||||
signingStatus: SigningStatus.NOT_SIGNED,
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deletedAt: null,
|
||||||
}))
|
}))
|
||||||
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
||||||
userId,
|
userId,
|
||||||
status: ExtendedDocumentStatus.DRAFT,
|
status: ExtendedDocumentStatus.DRAFT,
|
||||||
|
deletedAt: null,
|
||||||
}))
|
}))
|
||||||
.with(ExtendedDocumentStatus.PENDING, () => ({
|
.with(ExtendedDocumentStatus.PENDING, () => ({
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
status: ExtendedDocumentStatus.PENDING,
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: ExtendedDocumentStatus.PENDING,
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
|
|
||||||
Recipient: {
|
Recipient: {
|
||||||
some: {
|
some: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
@ -106,6 +117,7 @@ export const findDocuments = async ({
|
|||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
status: ExtendedDocumentStatus.COMPLETED,
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: ExtendedDocumentStatus.COMPLETED,
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SigningStatus, User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
import { SigningStatus } from '@documenso/prisma/client';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
|
|||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.groupBy({
|
prisma.document.groupBy({
|
||||||
@ -31,6 +33,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
|
|||||||
signingStatus: SigningStatus.NOT_SIGNED,
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.groupBy({
|
prisma.document.groupBy({
|
||||||
@ -39,15 +42,27 @@ export const getStats = async ({ user }: GetStatsInput) => {
|
|||||||
_all: true,
|
_all: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
status: {
|
OR: [
|
||||||
not: ExtendedDocumentStatus.DRAFT,
|
{
|
||||||
},
|
status: ExtendedDocumentStatus.PENDING,
|
||||||
Recipient: {
|
Recipient: {
|
||||||
some: {
|
some: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
status: ExtendedDocumentStatus.COMPLETED,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { signPdf } from '@documenso/signing';
|
import { signPdf } from '@documenso/signing';
|
||||||
@ -83,6 +85,18 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
|||||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const postHog = PostHogServerClient();
|
||||||
|
|
||||||
|
if (postHog) {
|
||||||
|
postHog.capture({
|
||||||
|
distinctId: nanoid(),
|
||||||
|
event: 'App: Document Sealed',
|
||||||
|
properties: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.documentData.update({
|
await prisma.documentData.update({
|
||||||
where: {
|
where: {
|
||||||
id: documentData.id,
|
id: documentData.id,
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type SearchDocumentsWithKeywordOptions = {
|
||||||
|
query: string;
|
||||||
|
userId: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchDocumentsWithKeyword = async ({
|
||||||
|
query,
|
||||||
|
userId,
|
||||||
|
limit = 5,
|
||||||
|
}: SearchDocumentsWithKeywordOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = await prisma.document.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
userId: userId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userId: userId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: DocumentStatus.COMPLETED,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
Recipient: {
|
||||||
|
some: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Recipient: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
};
|
||||||
@ -37,6 +37,10 @@ export const signFieldWithToken = async ({
|
|||||||
throw new Error(`Document ${document.id} has already been completed`);
|
throw new Error(`Document ${document.id} has already been completed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.deletedAt) {
|
||||||
|
throw new Error(`Document ${document.id} has been deleted`);
|
||||||
|
}
|
||||||
|
|
||||||
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import type { TFeatureFlagValue } from '@documenso/lib/client-only/providers/feature-flag.types';
|
||||||
TFeatureFlagValue,
|
import { ZFeatureFlagValueSchema } from '@documenso/lib/client-only/providers/feature-flag.types';
|
||||||
ZFeatureFlagValueSchema,
|
|
||||||
} from '@documenso/lib/client-only/providers/feature-flag.types';
|
|
||||||
import { APP_BASE_URL } from '@documenso/lib/constants/app';
|
import { APP_BASE_URL } from '@documenso/lib/constants/app';
|
||||||
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "VerificationToken" DROP CONSTRAINT "VerificationToken_userId_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -60,7 +60,7 @@ model VerificationToken {
|
|||||||
expires DateTime
|
expires DateTime
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
userId Int
|
userId Int
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SubscriptionStatus {
|
enum SubscriptionStatus {
|
||||||
@ -135,6 +135,7 @@ model Document {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
|
deletedAt DateTime?
|
||||||
|
|
||||||
@@unique([documentDataId])
|
@@unique([documentDataId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
|||||||
@ -1,74 +1,22 @@
|
|||||||
import { DocumentDataType, Role } from '@prisma/client';
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
|
||||||
|
|
||||||
import { prisma } from './index';
|
|
||||||
|
|
||||||
const seedDatabase = async () => {
|
const seedDatabase = async () => {
|
||||||
const examplePdf = fs
|
const files = fs.readdirSync(path.join(__dirname, './seed'));
|
||||||
.readFileSync(path.join(__dirname, '../../assets/example.pdf'))
|
|
||||||
.toString('base64');
|
|
||||||
|
|
||||||
const exampleUser = await prisma.user.upsert({
|
for (const file of files) {
|
||||||
where: {
|
const stat = fs.statSync(path.join(__dirname, './seed', file));
|
||||||
email: 'example@documenso.com',
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
name: 'Example User',
|
|
||||||
email: 'example@documenso.com',
|
|
||||||
password: hashSync('password'),
|
|
||||||
roles: [Role.USER],
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const adminUser = await prisma.user.upsert({
|
if (stat.isFile()) {
|
||||||
where: {
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
email: 'admin@documenso.com',
|
const mod = require(path.join(__dirname, './seed', file));
|
||||||
},
|
|
||||||
create: {
|
|
||||||
name: 'Admin User',
|
|
||||||
email: 'admin@documenso.com',
|
|
||||||
password: hashSync('password'),
|
|
||||||
roles: [Role.USER, Role.ADMIN],
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const examplePdfData = await prisma.documentData.upsert({
|
if ('seedDatabase' in mod && typeof mod.seedDatabase === 'function') {
|
||||||
where: {
|
console.log(`[SEEDING]: ${file}`);
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
await mod.seedDatabase();
|
||||||
},
|
}
|
||||||
create: {
|
}
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
}
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: examplePdf,
|
|
||||||
initialData: examplePdf,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.document.upsert({
|
|
||||||
where: {
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: 1,
|
|
||||||
title: 'Example Document',
|
|
||||||
documentDataId: examplePdfData.id,
|
|
||||||
userId: exampleUser.id,
|
|
||||||
Recipient: {
|
|
||||||
create: {
|
|
||||||
name: String(adminUser.name),
|
|
||||||
email: adminUser.email,
|
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
seedDatabase()
|
seedDatabase()
|
||||||
|
|||||||
67
packages/prisma/seed/initial-seed.ts
Normal file
67
packages/prisma/seed/initial-seed.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||||
|
|
||||||
|
import { prisma } from '..';
|
||||||
|
import { DocumentDataType, Role } from '../client';
|
||||||
|
|
||||||
|
export const seedDatabase = async () => {
|
||||||
|
const examplePdf = fs
|
||||||
|
.readFileSync(path.join(__dirname, '../../../assets/example.pdf'))
|
||||||
|
.toString('base64');
|
||||||
|
|
||||||
|
const exampleUser = await prisma.user.upsert({
|
||||||
|
where: {
|
||||||
|
email: 'example@documenso.com',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'Example User',
|
||||||
|
email: 'example@documenso.com',
|
||||||
|
password: hashSync('password'),
|
||||||
|
roles: [Role.USER],
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminUser = await prisma.user.upsert({
|
||||||
|
where: {
|
||||||
|
email: 'admin@documenso.com',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'Admin User',
|
||||||
|
email: 'admin@documenso.com',
|
||||||
|
password: hashSync('password'),
|
||||||
|
roles: [Role.USER, Role.ADMIN],
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const examplePdfData = await prisma.documentData.upsert({
|
||||||
|
where: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: 'Example Document',
|
||||||
|
documentDataId: examplePdfData.id,
|
||||||
|
userId: exampleUser.id,
|
||||||
|
Recipient: {
|
||||||
|
create: {
|
||||||
|
name: String(adminUser.name),
|
||||||
|
email: adminUser.email,
|
||||||
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
221
packages/prisma/seed/pr-711-deletion-of-documents.ts
Normal file
221
packages/prisma/seed/pr-711-deletion-of-documents.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import type { User } from '@prisma/client';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||||
|
|
||||||
|
import { prisma } from '..';
|
||||||
|
import {
|
||||||
|
DocumentDataType,
|
||||||
|
DocumentStatus,
|
||||||
|
FieldType,
|
||||||
|
Prisma,
|
||||||
|
ReadStatus,
|
||||||
|
SendStatus,
|
||||||
|
SigningStatus,
|
||||||
|
} from '../client';
|
||||||
|
|
||||||
|
const PULL_REQUEST_NUMBER = 711;
|
||||||
|
const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`;
|
||||||
|
|
||||||
|
export const TEST_USERS = [
|
||||||
|
{
|
||||||
|
name: 'Sender 1',
|
||||||
|
email: `sender1@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sender 2',
|
||||||
|
email: `sender2@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sender 3',
|
||||||
|
email: `sender3@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const examplePdf = fs
|
||||||
|
.readFileSync(path.join(__dirname, '../../../assets/example.pdf'))
|
||||||
|
.toString('base64');
|
||||||
|
|
||||||
|
export const seedDatabase = async () => {
|
||||||
|
const users = await Promise.all(
|
||||||
|
TEST_USERS.map(async (u) =>
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: u.name,
|
||||||
|
email: u.email,
|
||||||
|
password: hashSync(u.password),
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [user1, user2, user3] = users;
|
||||||
|
|
||||||
|
await createDraftDocument(user1, [user2, user3]);
|
||||||
|
await createPendingDocument(user1, [user2, user3]);
|
||||||
|
await createCompletedDocument(user1, [user2, user3]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDraftDocument = async (sender: User, recipients: User[]) => {
|
||||||
|
const documentData = await prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: `[${PULL_REQUEST_NUMBER}] Document 1 - Draft`,
|
||||||
|
status: DocumentStatus.DRAFT,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: sender.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
await prisma.recipient.create({
|
||||||
|
data: {
|
||||||
|
email: String(recipient.email),
|
||||||
|
name: String(recipient.name),
|
||||||
|
token: `draft-token-${index}`,
|
||||||
|
readStatus: ReadStatus.NOT_OPENED,
|
||||||
|
sendStatus: SendStatus.NOT_SENT,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
|
Document: {
|
||||||
|
connect: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
create: {
|
||||||
|
page: 1,
|
||||||
|
type: FieldType.NAME,
|
||||||
|
inserted: true,
|
||||||
|
customText: String(recipient.name),
|
||||||
|
positionX: new Prisma.Decimal(1),
|
||||||
|
positionY: new Prisma.Decimal(1),
|
||||||
|
width: new Prisma.Decimal(1),
|
||||||
|
height: new Prisma.Decimal(1),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPendingDocument = async (sender: User, recipients: User[]) => {
|
||||||
|
const documentData = await prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: `[${PULL_REQUEST_NUMBER}] Document 1 - Pending`,
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: sender.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
await prisma.recipient.create({
|
||||||
|
data: {
|
||||||
|
email: String(recipient.email),
|
||||||
|
name: String(recipient.name),
|
||||||
|
token: `pending-token-${index}`,
|
||||||
|
readStatus: ReadStatus.OPENED,
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
|
Document: {
|
||||||
|
connect: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
create: {
|
||||||
|
page: 1,
|
||||||
|
type: FieldType.NAME,
|
||||||
|
inserted: true,
|
||||||
|
customText: String(recipient.name),
|
||||||
|
positionX: new Prisma.Decimal(1),
|
||||||
|
positionY: new Prisma.Decimal(1),
|
||||||
|
width: new Prisma.Decimal(1),
|
||||||
|
height: new Prisma.Decimal(1),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCompletedDocument = async (sender: User, recipients: User[]) => {
|
||||||
|
const documentData = await prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: `[${PULL_REQUEST_NUMBER}] Document 1 - Completed`,
|
||||||
|
status: DocumentStatus.COMPLETED,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: sender.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
await prisma.recipient.create({
|
||||||
|
data: {
|
||||||
|
email: String(recipient.email),
|
||||||
|
name: String(recipient.name),
|
||||||
|
token: `completed-token-${index}`,
|
||||||
|
readStatus: ReadStatus.OPENED,
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
|
Document: {
|
||||||
|
connect: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
create: {
|
||||||
|
page: 1,
|
||||||
|
type: FieldType.NAME,
|
||||||
|
inserted: true,
|
||||||
|
customText: String(recipient.name),
|
||||||
|
positionX: new Prisma.Decimal(1),
|
||||||
|
positionY: new Prisma.Decimal(1),
|
||||||
|
width: new Prisma.Decimal(1),
|
||||||
|
height: new Prisma.Decimal(1),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
import type { User } from '@prisma/client';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||||
|
|
||||||
|
import { prisma } from '..';
|
||||||
|
import {
|
||||||
|
DocumentDataType,
|
||||||
|
DocumentStatus,
|
||||||
|
FieldType,
|
||||||
|
Prisma,
|
||||||
|
ReadStatus,
|
||||||
|
SendStatus,
|
||||||
|
SigningStatus,
|
||||||
|
} from '../client';
|
||||||
|
|
||||||
|
//
|
||||||
|
// https://github.com/documenso/documenso/pull/713
|
||||||
|
//
|
||||||
|
|
||||||
|
const PULL_REQUEST_NUMBER = 713;
|
||||||
|
|
||||||
|
const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`;
|
||||||
|
|
||||||
|
export const TEST_USERS = [
|
||||||
|
{
|
||||||
|
name: 'User 1',
|
||||||
|
email: `user1@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User 2',
|
||||||
|
email: `user2@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const examplePdf = fs
|
||||||
|
.readFileSync(path.join(__dirname, '../../../assets/example.pdf'))
|
||||||
|
.toString('base64');
|
||||||
|
|
||||||
|
export const seedDatabase = async () => {
|
||||||
|
const users = await Promise.all(
|
||||||
|
TEST_USERS.map(async (u) =>
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: u.name,
|
||||||
|
email: u.email,
|
||||||
|
password: hashSync(u.password),
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [user1, user2] = users;
|
||||||
|
|
||||||
|
await createSentDocument(user1, [user2]);
|
||||||
|
await createReceivedDocument(user2, [user1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSentDocument = async (sender: User, recipients: User[]) => {
|
||||||
|
const documentData = await prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: `[${PULL_REQUEST_NUMBER}] Document - Sent`,
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: sender.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
await prisma.recipient.create({
|
||||||
|
data: {
|
||||||
|
email: String(recipient.email),
|
||||||
|
name: String(recipient.name),
|
||||||
|
token: `sent-token-${index}`,
|
||||||
|
readStatus: ReadStatus.NOT_OPENED,
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
|
Document: {
|
||||||
|
connect: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
create: {
|
||||||
|
page: 1,
|
||||||
|
type: FieldType.NAME,
|
||||||
|
inserted: true,
|
||||||
|
customText: String(recipient.name),
|
||||||
|
positionX: new Prisma.Decimal(1),
|
||||||
|
positionY: new Prisma.Decimal(1),
|
||||||
|
width: new Prisma.Decimal(1),
|
||||||
|
height: new Prisma.Decimal(1),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createReceivedDocument = async (sender: User, recipients: User[]) => {
|
||||||
|
const documentData = await prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title: `[${PULL_REQUEST_NUMBER}] Document - Received`,
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: sender.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
await prisma.recipient.create({
|
||||||
|
data: {
|
||||||
|
email: String(recipient.email),
|
||||||
|
name: String(recipient.name),
|
||||||
|
token: `received-token-${index}`,
|
||||||
|
readStatus: ReadStatus.NOT_OPENED,
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
signingStatus: SigningStatus.NOT_SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
|
Document: {
|
||||||
|
connect: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
create: {
|
||||||
|
page: 1,
|
||||||
|
type: FieldType.NAME,
|
||||||
|
inserted: true,
|
||||||
|
customText: String(recipient.name),
|
||||||
|
positionX: new Prisma.Decimal(1),
|
||||||
|
positionY: new Prisma.Decimal(1),
|
||||||
|
width: new Prisma.Decimal(1),
|
||||||
|
height: new Prisma.Decimal(1),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
28
packages/prisma/seed/pr-718-add-stepper-component.ts
Normal file
28
packages/prisma/seed/pr-718-add-stepper-component.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||||
|
|
||||||
|
import { prisma } from '..';
|
||||||
|
|
||||||
|
//
|
||||||
|
// https://github.com/documenso/documenso/pull/713
|
||||||
|
//
|
||||||
|
|
||||||
|
const PULL_REQUEST_NUMBER = 718;
|
||||||
|
|
||||||
|
const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`;
|
||||||
|
|
||||||
|
export const TEST_USER = {
|
||||||
|
name: 'User 1',
|
||||||
|
email: `user1@${EMAIL_DOMAIN}`,
|
||||||
|
password: 'Password123',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const seedDatabase = async () => {
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: TEST_USER.name,
|
||||||
|
email: TEST_USER.email,
|
||||||
|
password: hashSync(TEST_USER.password),
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -3,11 +3,12 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||||
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
||||||
import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete-draft-document';
|
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
|
||||||
import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
|
import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
|
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
|
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
|
||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
@ -20,6 +21,7 @@ import {
|
|||||||
ZGetDocumentByIdQuerySchema,
|
ZGetDocumentByIdQuerySchema,
|
||||||
ZGetDocumentByTokenQuerySchema,
|
ZGetDocumentByTokenQuerySchema,
|
||||||
ZResendDocumentMutationSchema,
|
ZResendDocumentMutationSchema,
|
||||||
|
ZSearchDocumentsMutationSchema,
|
||||||
ZSendDocumentMutationSchema,
|
ZSendDocumentMutationSchema,
|
||||||
ZSetFieldsForDocumentMutationSchema,
|
ZSetFieldsForDocumentMutationSchema,
|
||||||
ZSetRecipientsForDocumentMutationSchema,
|
ZSetRecipientsForDocumentMutationSchema,
|
||||||
@ -97,15 +99,15 @@ export const documentRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteDraftDocument: authenticatedProcedure
|
deleteDocument: authenticatedProcedure
|
||||||
.input(ZDeleteDraftDocumentMutationSchema)
|
.input(ZDeleteDraftDocumentMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { id } = input;
|
const { id, status } = input;
|
||||||
|
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
return await deleteDraftDocument({ id, userId });
|
return await deleteDocument({ id, userId, status });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -240,4 +242,23 @@ export const documentRouter = router({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
searchDocuments: authenticatedProcedure
|
||||||
|
.input(ZSearchDocumentsMutationSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { query } = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const documents = await searchDocumentsWithKeyword({
|
||||||
|
query,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
});
|
||||||
|
return documents;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'We are unable to search for documents. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
id: z.number().min(1),
|
id: z.number().min(1),
|
||||||
@ -80,6 +80,11 @@ export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSc
|
|||||||
|
|
||||||
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
||||||
id: z.number().min(1),
|
id: z.number().min(1),
|
||||||
|
status: z.nativeEnum(DocumentStatus),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
|
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
|
||||||
|
|
||||||
|
export const ZSearchDocumentsMutationSchema = z.object({
|
||||||
|
query: z.string(),
|
||||||
|
});
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import { Download } from 'lucide-react';
|
|||||||
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import type { DocumentData } from '@documenso/prisma/client';
|
import type { DocumentData } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { Button } from '../../primitives/button';
|
||||||
|
import { useToast } from '../../primitives/use-toast';
|
||||||
|
|
||||||
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import {
|
|||||||
} from '@documenso/lib/constants/toast';
|
} from '@documenso/lib/constants/toast';
|
||||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Button } from '../../primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -22,8 +23,8 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '../../primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '../../primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import { TooltipArrow } from '@radix-ui/react-tooltip';
|
import { TooltipArrow } from '@radix-ui/react-tooltip';
|
||||||
import { VariantProps, cva } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
|
import { cva } from 'class-variance-authority';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
import { cn } from '../..//lib/utils';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@documenso/ui/primitives/tooltip';
|
} from '../..//primitives/tooltip';
|
||||||
|
import type { Field } from '.prisma/client';
|
||||||
import { Field } from '.prisma/client';
|
|
||||||
|
|
||||||
const tooltipVariants = cva('font-semibold', {
|
const tooltipVariants = cva('font-semibold', {
|
||||||
variants: {
|
variants: {
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||||
import { Field } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Card, CardContent } from '../../primitives/card';
|
||||||
|
|
||||||
export type FieldRootContainerProps = {
|
export type FieldRootContainerProps = {
|
||||||
field: Field;
|
field: Field;
|
||||||
|
|||||||
@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Image, { StaticImageData } from 'next/image';
|
import type { StaticImageData } from 'next/image';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
|
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { Signature } from '@documenso/prisma/client';
|
import type { Signature } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { cn } from '../lib/utils';
|
||||||
|
import { Card, CardContent } from '../primitives/card';
|
||||||
|
|
||||||
export type SigningCardProps = {
|
export type SigningCardProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@ -3,16 +3,11 @@ import * as React from 'react';
|
|||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||||
|
|
||||||
import { Role } from '@documenso/prisma/client';
|
import { Role } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { cn } from '../lib/utils';
|
||||||
import {
|
import { Button } from './button';
|
||||||
Command,
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from './command';
|
||||||
CommandEmpty,
|
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
} from '@documenso/ui/primitives/command';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
|
||||||
|
|
||||||
type ComboboxProps = {
|
type ComboboxProps = {
|
||||||
listValues: string[];
|
listValues: string[];
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Variants, motion } from 'framer-motion';
|
import type { Variants } from 'framer-motion';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { cn } from '../lib/utils';
|
||||||
|
import { Card, CardContent } from './card';
|
||||||
|
|
||||||
const DocumentDropzoneContainerVariants: Variants = {
|
const DocumentDropzoneContainerVariants: Variants = {
|
||||||
initial: {
|
initial: {
|
||||||
|
|||||||
@ -11,29 +11,27 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
|
|||||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { FieldType, SendStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
} from '@documenso/ui/primitives/command';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
|
||||||
|
|
||||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Button } from '../button';
|
||||||
|
import { Card, CardContent } from '../card';
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
||||||
|
import { useStep } from '../stepper';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||||
|
import type { TAddFieldsFormSchema } from './add-fields.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
|
DocumentFlowFormContainerHeader,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { FieldItem } from './field-item';
|
import { FieldItem } from './field-item';
|
||||||
import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||||
|
|
||||||
const fontCaveat = Caveat({
|
const fontCaveat = Caveat({
|
||||||
weight: ['500'],
|
weight: ['500'],
|
||||||
@ -53,7 +51,6 @@ export type AddFieldsFormProps = {
|
|||||||
hideRecipients?: boolean;
|
hideRecipients?: boolean;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
numberOfSteps: number;
|
|
||||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,10 +59,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
hideRecipients = false,
|
hideRecipients = false,
|
||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
numberOfSteps,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
||||||
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -287,6 +284,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DocumentFlowFormContainerHeader
|
||||||
|
title={documentFlow.title}
|
||||||
|
description={documentFlow.description}
|
||||||
|
/>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{selectedField && (
|
{selectedField && (
|
||||||
@ -513,15 +514,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
<DocumentFlowFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<DocumentFlowFormContainerStep
|
<DocumentFlowFormContainerStep
|
||||||
title={documentFlow.title}
|
title={documentFlow.title}
|
||||||
step={documentFlow.stepIndex}
|
step={currentStep}
|
||||||
maxStep={numberOfSteps}
|
maxStep={totalSteps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoBackClick={() => {
|
onGoBackClick={() => {
|
||||||
documentFlow.onBackStep?.();
|
previousStep();
|
||||||
remove();
|
remove();
|
||||||
}}
|
}}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
|
|||||||
@ -9,35 +9,38 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import { Field, FieldType } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||||
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Card, CardContent } from '../card';
|
||||||
|
import { ElementVisible } from '../element-visible';
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||||
|
import { Input } from '../input';
|
||||||
|
import { SignaturePad } from '../signature-pad';
|
||||||
|
import { useStep } from '../stepper';
|
||||||
|
import type { TAddSignatureFormSchema } from './add-signature.types';
|
||||||
|
import { ZAddSignatureFormSchema } from './add-signature.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
|
DocumentFlowFormContainerHeader,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
} from './document-flow-root';
|
||||||
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
|
||||||
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
|
||||||
import { ZAddSignatureFormSchema } from './add-signature.types';
|
|
||||||
import {
|
import {
|
||||||
SinglePlayerModeCustomTextField,
|
SinglePlayerModeCustomTextField,
|
||||||
SinglePlayerModeSignatureField,
|
SinglePlayerModeSignatureField,
|
||||||
} from './single-player-mode-fields';
|
} from './single-player-mode-fields';
|
||||||
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSignatureFormProps = {
|
export type AddSignatureFormProps = {
|
||||||
defaultValues?: TAddSignatureFormSchema;
|
defaultValues?: TAddSignatureFormSchema;
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
fields: FieldWithSignature[];
|
fields: FieldWithSignature[];
|
||||||
numberOfSteps: number;
|
|
||||||
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
|
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
|
||||||
requireName?: boolean;
|
requireName?: boolean;
|
||||||
requireSignature?: boolean;
|
requireSignature?: boolean;
|
||||||
@ -47,11 +50,13 @@ export const AddSignatureFormPartial = ({
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
documentFlow,
|
documentFlow,
|
||||||
fields,
|
fields,
|
||||||
numberOfSteps,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
requireName = false,
|
requireName = false,
|
||||||
requireSignature = true,
|
requireSignature = true,
|
||||||
}: AddSignatureFormProps) => {
|
}: AddSignatureFormProps) => {
|
||||||
|
const { currentStep, totalSteps } = useStep();
|
||||||
|
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
// Refined schema which takes into account whether to allow an empty name or signature.
|
// Refined schema which takes into account whether to allow an empty name or signature.
|
||||||
@ -206,46 +211,30 @@ export const AddSignatureFormPartial = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<>
|
||||||
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
<DocumentFlowFormContainerHeader
|
||||||
<DocumentFlowFormContainerContent>
|
title={documentFlow.title}
|
||||||
<div className="space-y-4">
|
description={documentFlow.description}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel required>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
className="bg-background"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
{...field}
|
|
||||||
onChange={(value) => {
|
|
||||||
onFormValueChange(FieldType.EMAIL);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{requireName && (
|
<Form {...form}>
|
||||||
|
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
||||||
|
<DocumentFlowFormContainerContent>
|
||||||
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required={requireName}>Name</FormLabel>
|
<FormLabel required>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onFormValueChange(FieldType.NAME);
|
onFormValueChange(FieldType.EMAIL);
|
||||||
field.onChange(value);
|
field.onChange(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -254,91 +243,114 @@ export const AddSignatureFormPartial = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{requireSignature && (
|
{requireName && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="signature"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required={requireSignature}>Signature</FormLabel>
|
<FormLabel required={requireName}>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Card
|
<Input
|
||||||
className={cn('mt-2', {
|
className="bg-background"
|
||||||
'rounded-sm ring-2 ring-red-500 ring-offset-2 transition-all':
|
{...field}
|
||||||
form.formState.errors.signature,
|
onChange={(value) => {
|
||||||
})}
|
onFormValueChange(FieldType.NAME);
|
||||||
gradient={!form.formState.errors.signature}
|
field.onChange(value);
|
||||||
degrees={-120}
|
}}
|
||||||
>
|
/>
|
||||||
<CardContent className="p-0">
|
</FormControl>
|
||||||
<SignaturePad
|
<FormMessage />
|
||||||
className="h-44 w-full"
|
</FormItem>
|
||||||
defaultValue={field.value}
|
)}
|
||||||
onBlur={field.onBlur}
|
/>
|
||||||
onChange={(value) => {
|
)}
|
||||||
onFormValueChange(FieldType.SIGNATURE);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DocumentFlowFormContainerContent>
|
|
||||||
|
|
||||||
<DocumentFlowFormContainerFooter>
|
{requireSignature && (
|
||||||
<DocumentFlowFormContainerStep
|
<FormField
|
||||||
title={documentFlow.title}
|
control={form.control}
|
||||||
step={documentFlow.stepIndex}
|
name="signature"
|
||||||
maxStep={numberOfSteps}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem>
|
||||||
|
<FormLabel required={requireSignature}>Signature</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Card
|
||||||
|
className={cn('mt-2', {
|
||||||
|
'rounded-sm ring-2 ring-red-500 ring-offset-2 transition-all':
|
||||||
|
form.formState.errors.signature,
|
||||||
|
})}
|
||||||
|
gradient={!form.formState.errors.signature}
|
||||||
|
degrees={-120}
|
||||||
|
>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<SignaturePad
|
||||||
|
className="h-44 w-full"
|
||||||
|
defaultValue={field.value}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onChange={(value) => {
|
||||||
|
onFormValueChange(FieldType.SIGNATURE);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerFooter>
|
||||||
loading={form.formState.isSubmitting}
|
<DocumentFlowFormContainerStep
|
||||||
disabled={form.formState.isSubmitting}
|
title={documentFlow.title}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
step={currentStep}
|
||||||
onGoNextClick={form.handleSubmit(onValidateFields)}
|
maxStep={totalSteps}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
{validateUninsertedFields && uninsertedFields[0] && (
|
<DocumentFlowFormContainerActions
|
||||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
loading={form.formState.isSubmitting}
|
||||||
Click to insert field
|
disabled={form.formState.isSubmitting}
|
||||||
</FieldToolTip>
|
onGoBackClick={documentFlow.onBackStep}
|
||||||
)}
|
onGoNextClick={form.handleSubmit(onValidateFields)}
|
||||||
|
/>
|
||||||
|
</DocumentFlowFormContainerFooter>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
{localFields.map((field) =>
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
match(field.type)
|
Click to insert field
|
||||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
</FieldToolTip>
|
||||||
return (
|
)}
|
||||||
<SinglePlayerModeCustomTextField
|
|
||||||
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
|
{localFields.map((field) =>
|
||||||
|
match(field.type)
|
||||||
|
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
||||||
|
return (
|
||||||
|
<SinglePlayerModeCustomTextField
|
||||||
|
onClick={insertField(field)}
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.with(FieldType.SIGNATURE, () => (
|
||||||
|
<SinglePlayerModeSignatureField
|
||||||
onClick={insertField(field)}
|
onClick={insertField(field)}
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
/>
|
/>
|
||||||
);
|
))
|
||||||
})
|
.otherwise(() => {
|
||||||
.with(FieldType.SIGNATURE, () => (
|
return null;
|
||||||
<SinglePlayerModeSignatureField
|
}),
|
||||||
onClick={insertField(field)}
|
)}
|
||||||
key={field.id}
|
</ElementVisible>
|
||||||
field={field}
|
</Form>
|
||||||
/>
|
</>
|
||||||
))
|
|
||||||
.otherwise(() => {
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
</ElementVisible>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,35 +9,37 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { DocumentStatus, Field, Recipient, SendStatus } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { TAddSignersFormSchema, ZAddSignersFormSchema } from './add-signers.types';
|
import { Button } from '../button';
|
||||||
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
|
import { Input } from '../input';
|
||||||
|
import { Label } from '../label';
|
||||||
|
import { useStep } from '../stepper';
|
||||||
|
import { useToast } from '../use-toast';
|
||||||
|
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||||
|
import { ZAddSignersFormSchema } from './add-signers.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
|
DocumentFlowFormContainerHeader,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
document: DocumentWithData;
|
document: DocumentWithData;
|
||||||
numberOfSteps: number;
|
|
||||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSignersFormPartial = ({
|
export const AddSignersFormPartial = ({
|
||||||
documentFlow,
|
documentFlow,
|
||||||
numberOfSteps,
|
|
||||||
recipients,
|
recipients,
|
||||||
document,
|
document,
|
||||||
fields: _fields,
|
fields: _fields,
|
||||||
@ -48,6 +50,8 @@ export const AddSignersFormPartial = ({
|
|||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
|
|
||||||
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -126,6 +130,10 @@ export const AddSignersFormPartial = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DocumentFlowFormContainerHeader
|
||||||
|
title={documentFlow.title}
|
||||||
|
description={documentFlow.description}
|
||||||
|
/>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<div className="flex w-full flex-col gap-y-4">
|
<div className="flex w-full flex-col gap-y-4">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -221,15 +229,15 @@ export const AddSignersFormPartial = ({
|
|||||||
<DocumentFlowFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<DocumentFlowFormContainerStep
|
<DocumentFlowFormContainerStep
|
||||||
title={documentFlow.title}
|
title={documentFlow.title}
|
||||||
step={documentFlow.stepIndex}
|
step={currentStep}
|
||||||
maxStep={numberOfSteps}
|
maxStep={totalSteps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
canGoBack={document.status === DocumentStatus.DRAFT}
|
canGoBack={document.status === DocumentStatus.DRAFT}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={previousStep}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
@ -2,28 +2,30 @@
|
|||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
|
||||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
|
||||||
|
|
||||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
|
import { Input } from '../input';
|
||||||
|
import { Label } from '../label';
|
||||||
|
import { useStep } from '../stepper';
|
||||||
|
import { Textarea } from '../textarea';
|
||||||
|
import type { TAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
|
DocumentFlowFormContainerHeader,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSubjectFormProps = {
|
export type AddSubjectFormProps = {
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
document: DocumentWithData;
|
document: DocumentWithData;
|
||||||
numberOfSteps: number;
|
|
||||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,7 +34,6 @@ export const AddSubjectFormPartial = ({
|
|||||||
recipients: _recipients,
|
recipients: _recipients,
|
||||||
fields: _fields,
|
fields: _fields,
|
||||||
document,
|
document,
|
||||||
numberOfSteps,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
const {
|
const {
|
||||||
@ -49,9 +50,14 @@ export const AddSubjectFormPartial = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DocumentFlowFormContainerHeader
|
||||||
|
title={documentFlow.title}
|
||||||
|
description={documentFlow.description}
|
||||||
|
/>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
@ -124,15 +130,15 @@ export const AddSubjectFormPartial = ({
|
|||||||
<DocumentFlowFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<DocumentFlowFormContainerStep
|
<DocumentFlowFormContainerStep
|
||||||
title={documentFlow.title}
|
title={documentFlow.title}
|
||||||
step={documentFlow.stepIndex}
|
step={currentStep}
|
||||||
maxStep={numberOfSteps}
|
maxStep={totalSteps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={previousStep}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
@ -4,15 +4,17 @@ import { useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
|
||||||
|
|
||||||
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
|
import { Input } from '../input';
|
||||||
|
import { Label } from '../label';
|
||||||
|
import { useStep } from '../stepper';
|
||||||
import type { TAddTitleFormSchema } from './add-title.types';
|
import type { TAddTitleFormSchema } from './add-title.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
|
DocumentFlowFormContainerHeader,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import type { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
@ -22,7 +24,6 @@ export type AddTitleFormProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
document: DocumentWithData;
|
document: DocumentWithData;
|
||||||
numberOfSteps: number;
|
|
||||||
onSubmit: (_data: TAddTitleFormSchema) => void;
|
onSubmit: (_data: TAddTitleFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,7 +32,6 @@ export const AddTitleFormPartial = ({
|
|||||||
recipients: _recipients,
|
recipients: _recipients,
|
||||||
fields: _fields,
|
fields: _fields,
|
||||||
document,
|
document,
|
||||||
numberOfSteps,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddTitleFormProps) => {
|
}: AddTitleFormProps) => {
|
||||||
const {
|
const {
|
||||||
@ -46,8 +46,14 @@ export const AddTitleFormPartial = ({
|
|||||||
|
|
||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
|
|
||||||
|
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DocumentFlowFormContainerHeader
|
||||||
|
title={documentFlow.title}
|
||||||
|
description={documentFlow.description}
|
||||||
|
/>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
@ -72,14 +78,15 @@ export const AddTitleFormPartial = ({
|
|||||||
<DocumentFlowFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
<DocumentFlowFormContainerStep
|
<DocumentFlowFormContainerStep
|
||||||
title={documentFlow.title}
|
title={documentFlow.title}
|
||||||
step={documentFlow.stepIndex}
|
step={currentStep}
|
||||||
maxStep={numberOfSteps}
|
maxStep={totalSteps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
canGoBack={stepIndex !== 0}
|
||||||
|
onGoBackClick={previousStep}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '../button';
|
||||||
|
|
||||||
export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import { createPortal } from 'react-dom';
|
|||||||
import { Rnd } from 'react-rnd';
|
import { Rnd } from 'react-rnd';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
import { FRIENDLY_FIELD_TYPE, TDocumentFlowFormSchema } from './types';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Card, CardContent } from '../card';
|
||||||
|
import type { TDocumentFlowFormSchema } from './types';
|
||||||
|
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||||
|
|
||||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { Button, ButtonProps } from '@documenso/ui/primitives/button';
|
import type { ButtonProps } from '../button';
|
||||||
|
import { Button } from '../button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -11,7 +12,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '../dialog';
|
||||||
|
|
||||||
export type SendDocumentActionDialogProps = ButtonProps & {
|
export type SendDocumentActionDialogProps = ButtonProps & {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|||||||
@ -13,9 +13,11 @@ import {
|
|||||||
MIN_HANDWRITING_FONT_SIZE,
|
MIN_HANDWRITING_FONT_SIZE,
|
||||||
MIN_STANDARD_FONT_SIZE,
|
MIN_STANDARD_FONT_SIZE,
|
||||||
} from '@documenso/lib/constants/pdf';
|
} from '@documenso/lib/constants/pdf';
|
||||||
import { Field, FieldType } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
|
||||||
|
import { FieldRootContainer } from '../../components/field/field';
|
||||||
|
|
||||||
export type SinglePlayerModeFieldContainerProps = {
|
export type SinglePlayerModeFieldContainerProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
|||||||
export interface DocumentFlowStep {
|
export interface DocumentFlowStep {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
stepIndex: number;
|
stepIndex?: number;
|
||||||
onBackStep?: () => unknown;
|
onBackStep?: () => unknown;
|
||||||
onNextStep?: () => unknown;
|
onNextStep?: () => unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
export type FormErrorMessageProps = {
|
export type FormErrorMessageProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
import type * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
||||||
Controller,
|
import { Controller, FormProvider, useFormContext } from 'react-hook-form';
|
||||||
ControllerProps,
|
|
||||||
FieldPath,
|
|
||||||
FieldValues,
|
|
||||||
FormProvider,
|
|
||||||
useFormContext,
|
|
||||||
} from 'react-hook-form';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
import { Label } from '../label';
|
import { Label } from '../label';
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|||||||
@ -3,16 +3,16 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { PDFDocumentProxy } from 'pdfjs-dist';
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { DocumentData } from '@documenso/prisma/client';
|
import type { DocumentData } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
import { useToast } from './use-toast';
|
import { useToast } from './use-toast';
|
||||||
|
|
||||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||||
|
|||||||
@ -1,20 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
|
||||||
HTMLAttributes,
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
MouseEvent,
|
|
||||||
PointerEvent,
|
|
||||||
TouchEvent,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { StrokeOptions, getStroke } from 'perfect-freehand';
|
import type { StrokeOptions } from 'perfect-freehand';
|
||||||
|
import { getStroke } from 'perfect-freehand';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
import { getSvgPathFromStroke } from './helper';
|
import { getSvgPathFromStroke } from './helper';
|
||||||
import { Point } from './point';
|
import { Point } from './point';
|
||||||
|
|
||||||
|
|||||||
109
packages/ui/primitives/stepper.tsx
Normal file
109
packages/ui/primitives/stepper.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
type StepContextValue = {
|
||||||
|
isCompleting: boolean;
|
||||||
|
stepIndex: number;
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
nextStep: () => void;
|
||||||
|
previousStep: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StepContext = createContext<StepContextValue | null>(null);
|
||||||
|
|
||||||
|
type StepperProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onComplete?: () => void | Promise<void>;
|
||||||
|
onStepChanged?: (currentStep: number) => void;
|
||||||
|
currentStep?: number; // external control prop
|
||||||
|
setCurrentStep?: (step: number) => void; // external control function
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Stepper: FC<StepperProps> = ({
|
||||||
|
children,
|
||||||
|
onComplete,
|
||||||
|
onStepChanged,
|
||||||
|
currentStep: propCurrentStep,
|
||||||
|
setCurrentStep: propSetCurrentStep,
|
||||||
|
}) => {
|
||||||
|
const [stateCurrentStep, stateSetCurrentStep] = useState(1);
|
||||||
|
const [isCompleting, setIsCompleting] = useState(false);
|
||||||
|
|
||||||
|
// Determine if props are provided, otherwise use state
|
||||||
|
const isControlled = propCurrentStep !== undefined && propSetCurrentStep !== undefined;
|
||||||
|
const currentStep = isControlled ? propCurrentStep : stateCurrentStep;
|
||||||
|
const setCurrentStep = isControlled ? propSetCurrentStep : stateSetCurrentStep;
|
||||||
|
|
||||||
|
const totalSteps = React.Children.count(children);
|
||||||
|
|
||||||
|
const handleComplete = async () => {
|
||||||
|
try {
|
||||||
|
if (!onComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCompleting(true);
|
||||||
|
|
||||||
|
await onComplete();
|
||||||
|
|
||||||
|
setIsCompleting(false);
|
||||||
|
} catch (error) {
|
||||||
|
setIsCompleting(false);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStepChange = (nextStep: number) => {
|
||||||
|
setCurrentStep(nextStep);
|
||||||
|
onStepChanged?.(nextStep);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep < totalSteps) {
|
||||||
|
void handleStepChange(currentStep + 1);
|
||||||
|
} else {
|
||||||
|
void handleComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousStep = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
void handleStepChange(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stepper
|
||||||
|
if (totalSteps === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentChild = React.Children.toArray(children)[currentStep - 1];
|
||||||
|
|
||||||
|
const stepContextValue: StepContextValue = {
|
||||||
|
isCompleting,
|
||||||
|
stepIndex: currentStep - 1,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
|
isFirst: currentStep === 1,
|
||||||
|
isLast: currentStep === totalSteps,
|
||||||
|
nextStep,
|
||||||
|
previousStep,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <StepContext.Provider value={stepContextValue}>{currentChild}</StepContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Hook for children to use the step context */
|
||||||
|
export const useStep = (): StepContextValue => {
|
||||||
|
const context = useContext(StepContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useStep must be used within a Stepper');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user