Merge branch 'documenso:main' into feat/732-toggle-signup-form

This commit is contained in:
Navindu Amarakoon
2023-12-09 11:55:55 +05:30
committed by GitHub
61 changed files with 1004 additions and 411 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
View 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/**

View File

@ -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

View File

@ -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:
@ -23,9 +23,9 @@ jobs:
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

24
.github/workflows/deploy.yml vendored Normal file
View 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

View File

@ -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
View 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)

View 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
View 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
View 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: ''

View 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 }}`,
}
});
}

View File

@ -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
View 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'

View File

@ -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

View File

@ -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>

View File

@ -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}
setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
>
{/* Add fields to PDF page. */} {/* Add fields to PDF page. */}
{step === 'fields' && (
<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>

View File

@ -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({

View File

@ -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>

View File

@ -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,6 +39,7 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onDownloadClick = async () => { const onDownloadClick = async () => {
try {
let document: DocumentWithData | null = null; let document: DocumentWithData | null = null;
if (!recipient) { if (!recipient) {
@ -70,6 +73,13 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
link.click(); link.click();
window.URL.revokeObjectURL(link.href); window.URL.revokeObjectURL(link.href);
} catch (error) {
toast({
title: 'Something went wrong',
description: 'An error occurred while trying to download file.',
variant: 'destructive',
});
}
}; };
return match({ return match({

View File

@ -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);

View File

@ -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) {

View File

@ -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`);
}; };

View File

@ -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]);

View File

@ -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,
)} )}

View File

@ -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({

View File

@ -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({

View File

@ -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:

View File

@ -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'],
}; };

View File

@ -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",

View 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();
});

View File

@ -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,

View File

@ -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';

View File

@ -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,

View File

@ -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';

View 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(),
},
});
};

View File

@ -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;

View File

@ -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;

View File

@ -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: {

View File

@ -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;

View File

@ -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;

View File

@ -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[];

View File

@ -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: {

View File

@ -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()}

View File

@ -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,6 +211,12 @@ export const AddSignatureFormPartial = ({
}; };
return ( return (
<>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<Form {...form}> <Form {...form}>
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}> <fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
@ -296,8 +307,8 @@ export const AddSignatureFormPartial = ({
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep <DocumentFlowFormContainerStep
title={documentFlow.title} title={documentFlow.title}
step={documentFlow.stepIndex} step={currentStep}
maxStep={numberOfSteps} maxStep={totalSteps}
/> />
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
@ -340,5 +351,6 @@ export const AddSignatureFormPartial = ({
)} )}
</ElementVisible> </ElementVisible>
</Form> </Form>
</>
); );
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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];

View File

@ -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;

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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';

View 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;
};