mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge branch 'documenso:main' into feat/732-toggle-signup-form
This commit is contained in:
10
.env.example
10
.env.example
@ -29,15 +29,15 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_password"
|
|||||||
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
||||||
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
|
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
|
||||||
# OPTIONAL: Defines the endpoint to use for the S3 storage transport. Relevant when using third-party S3-compatible providers.
|
# OPTIONAL: Defines the endpoint to use for the S3 storage transport. Relevant when using third-party S3-compatible providers.
|
||||||
NEXT_PRIVATE_UPLOAD_ENDPOINT=
|
NEXT_PRIVATE_UPLOAD_ENDPOINT="http://127.0.0.1:9002"
|
||||||
# OPTIONAL: Defines the region to use for the S3 storage transport. Defaults to us-east-1.
|
# OPTIONAL: Defines the region to use for the S3 storage transport. Defaults to us-east-1.
|
||||||
NEXT_PRIVATE_UPLOAD_REGION=
|
NEXT_PRIVATE_UPLOAD_REGION="unknown"
|
||||||
# REQUIRED: Defines the bucket to use for the S3 storage transport.
|
# REQUIRED: Defines the bucket to use for the S3 storage transport.
|
||||||
NEXT_PRIVATE_UPLOAD_BUCKET=
|
NEXT_PRIVATE_UPLOAD_BUCKET="documenso"
|
||||||
# OPTIONAL: Defines the access key ID to use for the S3 storage transport.
|
# OPTIONAL: Defines the access key ID to use for the S3 storage transport.
|
||||||
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=
|
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso"
|
||||||
# OPTIONAL: Defines the secret access key to use for the S3 storage transport.
|
# OPTIONAL: Defines the secret access key to use for the S3 storage transport.
|
||||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=
|
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
|
||||||
|
|
||||||
# [[SMTP]]
|
# [[SMTP]]
|
||||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
||||||
|
|||||||
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/labeler.yml
vendored
Normal file
21
.github/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
|
||||||
10
.github/workflows/e2e-tests.yml
vendored
10
.github/workflows/e2e-tests.yml
vendored
@ -1,18 +1,20 @@
|
|||||||
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
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
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 }}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
27
.github/workflows/semantic-pull-requests.yml
vendored
27
.github/workflows/semantic-pull-requests.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "Validate PR Name"
|
name: 'Validate PR Name'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
@ -9,7 +9,7 @@ on:
|
|||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-pr:
|
validate-pr:
|
||||||
@ -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,assigned'
|
||||||
@ -139,11 +139,13 @@ npm run d
|
|||||||
|
|
||||||
1. **App** - http://localhost:3000
|
1. **App** - http://localhost:3000
|
||||||
2. **Incoming Mail Access** - http://localhost:9000
|
2. **Incoming Mail Access** - http://localhost:9000
|
||||||
|
|
||||||
3. **Database Connection Details**
|
3. **Database Connection Details**
|
||||||
|
|
||||||
- **Port**: 54320
|
- **Port**: 54320
|
||||||
- **Connection**: Use your favorite database client to connect using the provided port.
|
- **Connection**: Use your favorite database client to connect using the provided port.
|
||||||
|
|
||||||
|
4. **S3 Storage Dashboard** - http://localhost:9001
|
||||||
|
|
||||||
## Developer Setup
|
## Developer Setup
|
||||||
|
|
||||||
### Manual Setup
|
### Manual Setup
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -79,8 +79,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
return searchDocumentsData.map((document) => ({
|
return searchDocumentsData.map((document) => ({
|
||||||
label: document.title,
|
label: document.title,
|
||||||
path: `/documents/${document.id}`,
|
path: `/documents/${document.id}`,
|
||||||
value:
|
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||||
document.title + ' ' + document.Recipient.map((recipient) => recipient.email).join(' '),
|
|
||||||
}));
|
}));
|
||||||
}, [searchDocumentsData]);
|
}, [searchDocumentsData]);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { HTMLAttributes, useEffect, useState } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
@ -32,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-50 flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
|
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[1000] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
|
||||||
scrollY > 5 && 'border-b-border',
|
scrollY > 5 && 'border-b-border',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { signIn } from 'next-auth/react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, 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';
|
||||||
@ -36,6 +37,7 @@ export type SignUpFormProps = {
|
|||||||
|
|
||||||
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const analytics = useAnalytics();
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -64,6 +66,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({
|
||||||
|
|||||||
@ -17,3 +17,20 @@ services:
|
|||||||
- 9000:9000
|
- 9000:9000
|
||||||
- 2500:2500
|
- 2500:2500
|
||||||
- 1100:1100
|
- 1100:1100
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: minio
|
||||||
|
ports:
|
||||||
|
- 9002:9002
|
||||||
|
- 9001:9001
|
||||||
|
volumes:
|
||||||
|
- minio:/data
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: documenso
|
||||||
|
MINIO_ROOT_PASSWORD: password
|
||||||
|
entrypoint: sh
|
||||||
|
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio:
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
'**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}': ['prettier --write'],
|
'**/*.{ts,tsx,cts,mts}': ['eslint --fix'],
|
||||||
|
'**/*.{js,jsx,cjs,mjs}': ['prettier --write'],
|
||||||
|
'**/*.{yml,mdx}': ['prettier --write'],
|
||||||
|
'**/*/package.json': ['npm run precommit'],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
"prisma:seed": "npm run with:env -- npm run prisma:seed -w @documenso/prisma",
|
"prisma:seed": "npm run with:env -- npm run prisma:seed -w @documenso/prisma",
|
||||||
"prisma:studio": "npm run with:env -- npx prisma studio --schema packages/prisma/schema.prisma",
|
"prisma:studio": "npm run with:env -- npx prisma studio --schema packages/prisma/schema.prisma",
|
||||||
"with:env": "dotenv -e .env -e .env.local --",
|
"with:env": "dotenv -e .env -e .env.local --",
|
||||||
"reset:hard": "npm run clean && npm i && npm run prisma:generate"
|
"reset:hard": "npm run clean && npm i && npm run prisma:generate",
|
||||||
|
"precommit": "npm install && git add package.json package-lock.json"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=8.6.0",
|
"npm": ">=8.6.0",
|
||||||
|
|||||||
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();
|
||||||
|
});
|
||||||
@ -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';
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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