Compare commits
2 Commits
v1.5.6-rc.
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc035ed08c | ||
|
|
fb2fd17ad8 |
@@ -75,7 +75,7 @@ NEXT_PRIVATE_SMTP_APIKEY=
|
|||||||
# OPTIONAL: Defines whether to force the use of TLS.
|
# OPTIONAL: Defines whether to force the use of TLS.
|
||||||
NEXT_PRIVATE_SMTP_SECURE=
|
NEXT_PRIVATE_SMTP_SECURE=
|
||||||
# REQUIRED: Defines the sender name to use for the from address.
|
# REQUIRED: Defines the sender name to use for the from address.
|
||||||
NEXT_PRIVATE_SMTP_FROM_NAME="Documenso"
|
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||||
# REQUIRED: Defines the email address to use as the from address.
|
# REQUIRED: Defines the email address to use as the from address.
|
||||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||||
# OPTIONAL: The API key to use for Resend.com
|
# OPTIONAL: The API key to use for Resend.com
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
|||||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -33,9 +33,9 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
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@v3
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
2
.github/workflows/e2e-tests.yml
vendored
2
.github/workflows/e2e-tests.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: npm run ci
|
run: npm run ci
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
|
|||||||
2
.github/workflows/issue-assignee-check.yml
vendored
2
.github/workflows/issue-assignee-check.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check Assigned User's Issue Count
|
- name: Check Assigned User's Issue Count
|
||||||
id: parse-comment
|
id: parse-comment
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
25
.github/workflows/issue-labeler.yml
vendored
25
.github/workflows/issue-labeler.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Auto Label Assigned Issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [assigned]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-when-assigned:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Label issue
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
script: |
|
|
||||||
const issue = context.issue;
|
|
||||||
// To run only on issues and not on PR
|
|
||||||
if (github.context.payload.issue.pull_request === undefined) {
|
|
||||||
const labelResponse = await github.rest.issues.addLabels({
|
|
||||||
owner: issue.owner,
|
|
||||||
repo: issue.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: ['status: assigned']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
2
.github/workflows/issue-opened.yml
vendored
2
.github/workflows/issue-opened.yml
vendored
@@ -17,5 +17,5 @@ jobs:
|
|||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
labels: ["status: triage"]
|
labels: ["needs triage"]
|
||||||
})
|
})
|
||||||
|
|||||||
4
.github/workflows/pr-review-reminder.yml
vendored
4
.github/workflows/pr-review-reminder.yml
vendored
@@ -2,14 +2,14 @@ name: 'PR Review Reminder'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ['opened', 'ready_for_review']
|
types: ['opened', 'reopened', 'ready_for_review', 'review_requested']
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checkPRs:
|
checkPRs:
|
||||||
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review')
|
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'reopened' || 'ready_for_review' || 'review_requested')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v4
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-pr-stale: 90
|
days-before-pr-stale: 90
|
||||||
|
|||||||
14
.gitpod.yml
14
.gitpod.yml
@@ -29,6 +29,16 @@ ports:
|
|||||||
visibility: private
|
visibility: private
|
||||||
onOpen: ignore
|
onOpen: ignore
|
||||||
|
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
master: true
|
||||||
|
pullRequests: true
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
addCheck: true
|
||||||
|
addComment: true
|
||||||
|
addBadge: true
|
||||||
|
|
||||||
vscode:
|
vscode:
|
||||||
extensions:
|
extensions:
|
||||||
- aaron-bond.better-comments
|
- aaron-bond.better-comments
|
||||||
@@ -37,5 +47,9 @@ vscode:
|
|||||||
- esbenp.prettier-vscode
|
- esbenp.prettier-vscode
|
||||||
- mikestead.dotenv
|
- mikestead.dotenv
|
||||||
- unifiedjs.vscode-mdx
|
- unifiedjs.vscode-mdx
|
||||||
|
- GitHub.copilot-chat
|
||||||
|
- GitHub.copilot-labs
|
||||||
|
- GitHub.copilot
|
||||||
- GitHub.vscode-pull-request-github
|
- GitHub.vscode-pull-request-github
|
||||||
- Prisma.prisma
|
- Prisma.prisma
|
||||||
|
- VisualStudioExptTeam.vscodeintellicode
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ For the digital signature of your documents you need a signing certificate in .p
|
|||||||
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
||||||
|
|
||||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||||
|
5. Place the certificate `/apps/web/resources/certificate.p12`
|
||||||
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,11 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
|
|||||||
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
|
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const FONT_NOTO_SANS_BYTES = fs.readFileSync(
|
|
||||||
path.join(__dirname, '../../packages/assets/fonts/noto-sans.ttf'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
experimental: {
|
experimental: {
|
||||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||||
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign'],
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '50mb',
|
bodySizeLimit: '50mb',
|
||||||
},
|
},
|
||||||
@@ -42,7 +38,6 @@ const config = {
|
|||||||
env: {
|
env: {
|
||||||
NEXT_PUBLIC_PROJECT: 'marketing',
|
NEXT_PUBLIC_PROJECT: 'marketing',
|
||||||
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
||||||
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
|
|
||||||
},
|
},
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
'lucide-react': {
|
'lucide-react': {
|
||||||
|
|||||||
2
apps/marketing/public/pdf.worker.min.js
vendored
2
apps/marketing/public/pdf.worker.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
|||||||
import type { TClaimPlanRequestSchema } from './types';
|
import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types';
|
||||||
import { ZClaimPlanResponseSchema } from './types';
|
|
||||||
|
|
||||||
export const claimPlan = async ({
|
export const claimPlan = async ({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
|||||||
>
|
>
|
||||||
{showProfilesAnnouncementBar && (
|
{showProfilesAnnouncementBar && (
|
||||||
<div className="relative inline-flex w-full items-center justify-center overflow-hidden bg-[#e7f3df] px-4 py-2.5">
|
<div className="relative inline-flex w-full items-center justify-center overflow-hidden bg-[#e7f3df] px-4 py-2.5">
|
||||||
<div className="text-black text-center text-sm font-medium">
|
<div className="text-foreground text-center text-sm font-medium">
|
||||||
Claim your documenso public profile username now!{' '}
|
Claim your documenso public profile username now!{' '}
|
||||||
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
|
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
|
||||||
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
|
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
dataKey={metricKey as string}
|
dataKey={metricKey as string}
|
||||||
maxBarSize={60}
|
maxBarSize={60}
|
||||||
fill="hsl(var(--primary))"
|
fill="hsl(var(--primary))"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||||
const formattedData = data.map((item) => ({
|
const formattedData = data.map((item) => ({
|
||||||
amount: Number(item.amount),
|
amount: Number(item.amount),
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
date: formatMonth(item.date as string),
|
date: formatMonth(item.date as string),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import type { Variants } from 'framer-motion';
|
import { Variants, motion } from 'framer-motion';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import type { TOSSFriendsSchema } from './schema';
|
import { TOSSFriendsSchema } from './schema';
|
||||||
|
|
||||||
const ContainerVariants: Variants = {
|
const ContainerVariants: Variants = {
|
||||||
initial: {
|
initial: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentDataType, Prisma } from '@documenso/prisma/client';
|
import { DocumentDataType, Prisma } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -115,7 +115,7 @@ export const SinglePlayerClient = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const putFileData = await putPdfFile(uploadedFile.file);
|
const putFileData = await putFile(uploadedFile.file);
|
||||||
|
|
||||||
const documentToken = await createSinglePlayerDocument({
|
const documentToken = await createSinglePlayerDocument({
|
||||||
documentData: {
|
documentData: {
|
||||||
@@ -158,7 +158,6 @@ export const SinglePlayerClient = () => {
|
|||||||
expired: null,
|
expired: null,
|
||||||
signedAt: null,
|
signedAt: null,
|
||||||
readStatus: 'OPENED',
|
readStatus: 'OPENED',
|
||||||
documentDeletedAt: null,
|
|
||||||
signingStatus: 'NOT_SIGNED',
|
signingStatus: 'NOT_SIGNED',
|
||||||
sendStatus: 'NOT_SENT',
|
sendStatus: 'NOT_SENT',
|
||||||
role: 'SIGNER',
|
role: 'SIGNER',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
import { allBlogPosts, allGenericPages } from 'contentlayer/generated';
|
import { allBlogPosts, allGenericPages } from 'contentlayer/generated';
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import LogoImage from '@documenso/assets/logo.png';
|
|||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
// import { StatusWidgetContainer } from './status-widget-container';
|
import { StatusWidgetContainer } from './status-widget-container';
|
||||||
|
|
||||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="mt-6">
|
<div className="mt-6">
|
||||||
<StatusWidgetContainer />
|
<StatusWidgetContainer />
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
|
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
variants={HeroTitleVariants}
|
variants={HeroTitleVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]"
|
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]"
|
||||||
>
|
>
|
||||||
Document signing,
|
Document signing,
|
||||||
<span className="block" /> finally open source.
|
<span className="block" /> finally open source.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { StatusWidget } from './status-widget';
|
|||||||
export function StatusWidgetContainer() {
|
export function StatusWidgetContainer() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<StatusWidgetFallback />}>
|
<Suspense fallback={<StatusWidgetFallback />}>
|
||||||
<StatusWidget slug="documenso-status" />
|
<StatusWidget />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { memo, use } from 'react';
|
import { use, useMemo } from 'react';
|
||||||
|
|
||||||
import { type Status, getStatus } from '@openstatus/react';
|
import type { Status } from '@openstatus/react';
|
||||||
|
import { getStatus } from '@openstatus/react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
@@ -44,8 +45,9 @@ const getStatusLevel = (level: Status) => {
|
|||||||
}[level];
|
}[level];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StatusWidget = memo(function StatusWidget({ slug }: { slug: string }) {
|
export function StatusWidget() {
|
||||||
const { status } = use(getStatus(slug));
|
const getStatusMemoized = useMemo(async () => getStatus('documenso-status'), []);
|
||||||
|
const { status } = use(getStatusMemoized);
|
||||||
const level = getStatusLevel(status);
|
const level = getStatusLevel(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,4 +72,4 @@ export const StatusWidget = memo(function StatusWidget({ slug }: { slug: string
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
{signatureText && (
|
{signatureText && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground truncate text-4xl font-semibold [font-family:var(--font-caveat)]',
|
'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{signatureText}
|
{signatureText}
|
||||||
@@ -360,7 +360,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="signatureText"
|
id="signatureText"
|
||||||
className="text-foreground placeholder:text-muted-foreground truncate border-none p-0 text-sm focus-visible:ring-0"
|
className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0"
|
||||||
placeholder="Draw or type name here"
|
placeholder="Draw or type name here"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('signatureText', {
|
{...register('signatureText', {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import type { FieldError } from 'react-hook-form';
|
import { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SVGAttributes } from 'react';
|
import { SVGAttributes } from 'react';
|
||||||
|
|
||||||
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import type { ThemeProviderProps } from 'next-themes/dist/types';
|
import { ThemeProviderProps } from 'next-themes/dist/types';
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
|||||||
@@ -18,16 +18,12 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
|
|||||||
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
|
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const FONT_NOTO_SANS_BYTES = fs.readFileSync(
|
|
||||||
path.join(__dirname, '../../packages/assets/fonts/noto-sans.ttf'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
|
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
|
||||||
experimental: {
|
experimental: {
|
||||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||||
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign'],
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '50mb',
|
bodySizeLimit: '50mb',
|
||||||
},
|
},
|
||||||
@@ -46,7 +42,6 @@ const config = {
|
|||||||
APP_VERSION: version,
|
APP_VERSION: version,
|
||||||
NEXT_PUBLIC_PROJECT: 'web',
|
NEXT_PUBLIC_PROJECT: 'web',
|
||||||
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
||||||
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
|
|
||||||
},
|
},
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
'lucide-react': {
|
'lucide-react': {
|
||||||
|
|||||||
56591
apps/web/public/pdf.worker.min.js
vendored
56591
apps/web/public/pdf.worker.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import { type Document, DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { type Document, SigningStatus } from '@documenso/prisma/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';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -18,10 +17,9 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
export type AdminActionsProps = {
|
export type AdminActionsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
document: Document;
|
document: Document;
|
||||||
recipients: Recipient[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminActions = ({ className, document, recipients }: AdminActionsProps) => {
|
export const AdminActions = ({ className, document }: AdminActionsProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
|
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
|
||||||
@@ -49,9 +47,7 @@ export const AdminActions = ({ className, document, recipients }: AdminActionsPr
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
loading={isResealDocumentLoading}
|
loading={isResealDocumentLoading}
|
||||||
disabled={recipients.some(
|
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||||
(recipient) => recipient.signingStatus !== SigningStatus.SIGNED,
|
|
||||||
)}
|
|
||||||
onClick={() => resealDocument({ id: document.id })}
|
onClick={() => resealDocument({ id: document.id })}
|
||||||
>
|
>
|
||||||
Reseal document
|
Reseal document
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
|
|||||||
|
|
||||||
<h2 className="text-lg font-semibold">Admin Actions</h2>
|
<h2 className="text-lg font-semibold">Admin Actions</h2>
|
||||||
|
|
||||||
<AdminActions className="mt-2" document={document} recipients={document.Recipient} />
|
<AdminActions className="mt-2" document={document} />
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
<h2 className="text-lg font-semibold">Recipients</h2>
|
<h2 className="text-lg font-semibold">Recipients</h2>
|
||||||
|
|||||||
@@ -4,22 +4,13 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {
|
import { Copy, Download, Edit, Loader, MoreHorizontal, Share, Trash2 } from 'lucide-react';
|
||||||
Copy,
|
|
||||||
Download,
|
|
||||||
Edit,
|
|
||||||
Loader,
|
|
||||||
MoreHorizontal,
|
|
||||||
ScrollTextIcon,
|
|
||||||
Share,
|
|
||||||
Trash2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import type { Document, Recipient, Team, TeamEmail, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +32,7 @@ export type DocumentPageViewDropdownProps = {
|
|||||||
Recipient: Recipient[];
|
Recipient: Recipient[];
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
|
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
|
||||||
@@ -59,10 +50,9 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
|
|
||||||
const isOwner = document.User.id === session.user.id;
|
const isOwner = document.User.id === session.user.id;
|
||||||
const isDraft = document.status === DocumentStatus.DRAFT;
|
const isDraft = document.status === DocumentStatus.DRAFT;
|
||||||
const isDeleted = document.deletedAt !== null;
|
|
||||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||||
|
const isDocumentDeletable = isOwner;
|
||||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||||
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
|
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team?.url);
|
const documentsPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
@@ -116,22 +106,12 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link href={`${documentsPath}/${document.id}/logs`}>
|
|
||||||
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
|
||||||
Audit Log
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
Duplicate
|
Duplicate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
|
||||||
disabled={Boolean(!canManageDocument && team?.teamEmail) || isDeleted}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -158,15 +138,15 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
/>
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
|
{isDocumentDeletable && (
|
||||||
<DeleteDocumentDialog
|
<DeleteDocumentDialog
|
||||||
id={document.id}
|
id={document.id}
|
||||||
status={document.status}
|
status={document.status}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
open={isDeleteDialogOpen}
|
open={isDeleteDialogOpen}
|
||||||
canManageDocument={canManageDocument}
|
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isDuplicateDialogOpen && (
|
{isDuplicateDialogOpen && (
|
||||||
<DuplicateDocumentDialog
|
<DuplicateDocumentDialog
|
||||||
id={document.id}
|
id={document.id}
|
||||||
|
|||||||
@@ -8,20 +8,17 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
|||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||||
import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document';
|
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import type { Team, TeamEmail } from '@documenso/prisma/client';
|
import type { Team } from '@documenso/prisma/client';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
||||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
|
||||||
import {
|
import {
|
||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
FRIENDLY_STATUS_MAP,
|
FRIENDLY_STATUS_MAP,
|
||||||
@@ -37,7 +34,7 @@ export type DocumentPageViewProps = {
|
|||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
team?: Team & { teamEmail: TeamEmail | null };
|
team?: Team;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
||||||
@@ -86,16 +83,11 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
documentMeta.password = securePassword;
|
documentMeta.password = securePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [recipients, completedFields] = await Promise.all([
|
const recipients = await getRecipientsForDocument({
|
||||||
getRecipientsForDocument({
|
|
||||||
documentId,
|
documentId,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}),
|
});
|
||||||
getCompletedFieldsForDocument({
|
|
||||||
documentId,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const documentWithRecipients = {
|
const documentWithRecipients = {
|
||||||
...document,
|
...document,
|
||||||
@@ -126,17 +118,11 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
<div className="text-muted-foreground flex items-center">
|
<div className="text-muted-foreground flex items-center">
|
||||||
<Users2 className="mr-2 h-5 w-5" />
|
<Users2 className="mr-2 h-5 w-5" />
|
||||||
|
|
||||||
<StackAvatarsWithTooltip
|
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
|
||||||
recipients={recipients}
|
|
||||||
documentStatus={document.status}
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<span>{recipients.length} Recipient(s)</span>
|
<span>{recipients.length} Recipient(s)</span>
|
||||||
</StackAvatarsWithTooltip>
|
</StackAvatarsWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{document.deletedAt && <Badge variant="destructive">Document deleted</Badge>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,13 +148,6 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{document.status === DocumentStatus.PENDING && (
|
|
||||||
<DocumentReadOnlyFields
|
|
||||||
fields={completedFields}
|
|
||||||
documentMeta={document.documentMeta || undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
|
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ export const EditDocumentForm = ({
|
|||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
onSubmit={onAddSettingsFormSubmit}
|
onSubmit={onAddSettingsFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddSignersFormPartial
|
<AddSignersFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const document = await getDocumentWithDetailsById({
|
const document = await getDocumentWithDetailsById({
|
||||||
id: documentId,
|
id: documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -69,11 +74,6 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
documentMeta.password = securePassword;
|
documentMeta.password = securePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
@@ -92,11 +92,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
<div className="text-muted-foreground flex items-center">
|
<div className="text-muted-foreground flex items-center">
|
||||||
<Users2 className="mr-2 h-5 w-5" />
|
<Users2 className="mr-2 h-5 w-5" />
|
||||||
|
|
||||||
<StackAvatarsWithTooltip
|
<StackAvatarsWithTooltip recipients={recipients} position="bottom">
|
||||||
recipients={recipients}
|
|
||||||
documentStatus={document.status}
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<span>{recipients.length} Recipient(s)</span>
|
<span>{recipients.length} Recipient(s)</span>
|
||||||
</StackAvatarsWithTooltip>
|
</StackAvatarsWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft, DownloadIcon } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
@@ -10,6 +10,7 @@ import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
|||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Recipient, Team } from '@documenso/prisma/client';
|
import type { Recipient, Team } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card } from '@documenso/ui/primitives/card';
|
import { Card } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,8 +19,6 @@ import {
|
|||||||
} from '~/components/formatter/document-status';
|
} from '~/components/formatter/document-status';
|
||||||
|
|
||||||
import { DocumentLogsDataTable } from './document-logs-data-table';
|
import { DocumentLogsDataTable } from './document-logs-data-table';
|
||||||
import { DownloadAuditLogButton } from './download-audit-log-button';
|
|
||||||
import { DownloadCertificateButton } from './download-certificate-button';
|
|
||||||
|
|
||||||
export type DocumentLogsPageViewProps = {
|
export type DocumentLogsPageViewProps = {
|
||||||
params: {
|
params: {
|
||||||
@@ -133,13 +132,15 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||||
<DownloadCertificateButton
|
<Button variant="outline" className="mr-2 w-full sm:w-auto">
|
||||||
className="mr-2"
|
<DownloadIcon className="mr-1.5 h-4 w-4" />
|
||||||
documentId={document.id}
|
Download certificate
|
||||||
documentStatus={document.status}
|
</Button>
|
||||||
/>
|
|
||||||
|
|
||||||
<DownloadAuditLogButton documentId={document.id} />
|
<Button className="w-full sm:w-auto">
|
||||||
|
<DownloadIcon className="mr-1.5 h-4 w-4" />
|
||||||
|
Download PDF
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DownloadIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export type DownloadAuditLogButtonProps = {
|
|
||||||
className?: string;
|
|
||||||
documentId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const { mutateAsync: downloadAuditLogs, isLoading } =
|
|
||||||
trpc.document.downloadAuditLogs.useMutation();
|
|
||||||
|
|
||||||
const onDownloadAuditLogsClick = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await downloadAuditLogs({ documentId });
|
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(iframe.style, {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '0',
|
|
||||||
height: '0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLoaded = () => {
|
|
||||||
if (iframe.contentDocument?.readyState === 'complete') {
|
|
||||||
iframe.contentWindow?.print();
|
|
||||||
|
|
||||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
|
||||||
document.body.removeChild(iframe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
|
||||||
iframe.addEventListener('load', onLoaded);
|
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
onLoaded();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'Sorry, we were unable to download the audit logs. Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn('w-full sm:w-auto', className)}
|
|
||||||
loading={isLoading}
|
|
||||||
onClick={() => void onDownloadAuditLogsClick()}
|
|
||||||
>
|
|
||||||
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
|
|
||||||
Download Audit Logs
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DownloadIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export type DownloadCertificateButtonProps = {
|
|
||||||
className?: string;
|
|
||||||
documentId: number;
|
|
||||||
documentStatus: DocumentStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DownloadCertificateButton = ({
|
|
||||||
className,
|
|
||||||
documentId,
|
|
||||||
documentStatus,
|
|
||||||
}: DownloadCertificateButtonProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const { mutateAsync: downloadCertificate, isLoading } =
|
|
||||||
trpc.document.downloadCertificate.useMutation();
|
|
||||||
|
|
||||||
const onDownloadCertificatesClick = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await downloadCertificate({ documentId });
|
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(iframe.style, {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '0',
|
|
||||||
height: '0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLoaded = () => {
|
|
||||||
if (iframe.contentDocument?.readyState === 'complete') {
|
|
||||||
iframe.contentWindow?.print();
|
|
||||||
|
|
||||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
|
||||||
document.body.removeChild(iframe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
|
||||||
iframe.addEventListener('load', onLoaded);
|
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
onLoaded();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'Sorry, we were unable to download the certificate. Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn('w-full sm:w-auto', className)}
|
|
||||||
loading={isLoading}
|
|
||||||
variant="outline"
|
|
||||||
disabled={documentStatus !== DocumentStatus.COMPLETED}
|
|
||||||
onClick={() => void onDownloadCertificatesClick()}
|
|
||||||
>
|
|
||||||
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
|
|
||||||
Download Certificate
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Share,
|
Share,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
XCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export type DataTableActionDropdownProps = {
|
|||||||
Recipient: Recipient[];
|
Recipient: Recipient[];
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownProps) => {
|
export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownProps) => {
|
||||||
@@ -66,8 +67,8 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
// const isPending = row.status === DocumentStatus.PENDING;
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
const isDocumentDeletable = isOwner;
|
||||||
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
||||||
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
|
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team?.url);
|
const documentsPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
@@ -106,14 +107,14 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger data-testid="document-table-action-btn">
|
<DropdownMenuTrigger>
|
||||||
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
|
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
{!isDraft && recipient && recipient?.role !== RecipientRole.CC && (
|
{recipient && recipient?.role !== RecipientRole.CC && (
|
||||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
{recipient?.role === RecipientRole.VIEWER && (
|
{recipient?.role === RecipientRole.VIEWER && (
|
||||||
@@ -140,7 +141,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!canManageDocument || isComplete} asChild>
|
<DropdownMenuItem disabled={(!isOwner && !isCurrentTeamDocument) || isComplete} asChild>
|
||||||
<Link href={`${documentsPath}/${row.id}/edit`}>
|
<Link href={`${documentsPath}/${row.id}/edit`}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
@@ -157,18 +158,14 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
Duplicate
|
Duplicate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* No point displaying this if there's no functionality. */}
|
<DropdownMenuItem disabled>
|
||||||
{/* <DropdownMenuItem disabled>
|
|
||||||
<XCircle className="mr-2 h-4 w-4" />
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
Void
|
Void
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
|
||||||
disabled={Boolean(!canManageDocument && team?.teamEmail)}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
{canManageDocument ? 'Delete' : 'Hide'}
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
||||||
@@ -189,6 +186,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
/>
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
|
{isDocumentDeletable && (
|
||||||
<DeleteDocumentDialog
|
<DeleteDocumentDialog
|
||||||
id={row.id}
|
id={row.id}
|
||||||
status={row.status}
|
status={row.status}
|
||||||
@@ -196,9 +194,8 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
open={isDeleteDialogOpen}
|
open={isDeleteDialogOpen}
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
teamId={team?.id}
|
teamId={team?.id}
|
||||||
canManageDocument={canManageDocument}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isDuplicateDialogOpen && (
|
{isDuplicateDialogOpen && (
|
||||||
<DuplicateDocumentDialog
|
<DuplicateDocumentDialog
|
||||||
id={row.id}
|
id={row.id}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useTransition } from 'react';
|
import { useTransition } from 'react';
|
||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
@@ -30,7 +29,7 @@ export type DocumentsDataTableProps = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
showSenderColumn?: boolean;
|
showSenderColumn?: boolean;
|
||||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsDataTable = ({
|
export const DocumentsDataTable = ({
|
||||||
@@ -63,12 +62,7 @@ export const DocumentsDataTable = ({
|
|||||||
{
|
{
|
||||||
header: 'Created',
|
header: 'Created',
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||||
<LocaleDate
|
|
||||||
date={row.original.createdAt}
|
|
||||||
format={{ ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Title',
|
header: 'Title',
|
||||||
@@ -82,12 +76,7 @@ export const DocumentsDataTable = ({
|
|||||||
{
|
{
|
||||||
header: 'Recipient',
|
header: 'Recipient',
|
||||||
accessorKey: 'recipient',
|
accessorKey: 'recipient',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => <StackAvatarsWithTooltip recipients={row.original.Recipient} />,
|
||||||
<StackAvatarsWithTooltip
|
|
||||||
recipients={row.original.Recipient}
|
|
||||||
documentStatus={row.original.status}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -26,7 +23,6 @@ type DeleteDocumentDialogProps = {
|
|||||||
status: DocumentStatus;
|
status: DocumentStatus;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
canManageDocument: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteDocumentDialog = ({
|
export const DeleteDocumentDialog = ({
|
||||||
@@ -36,7 +32,6 @@ export const DeleteDocumentDialog = ({
|
|||||||
status,
|
status,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
teamId,
|
teamId,
|
||||||
canManageDocument,
|
|
||||||
}: DeleteDocumentDialogProps) => {
|
}: DeleteDocumentDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -88,70 +83,33 @@ export const DeleteDocumentDialog = ({
|
|||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Are you sure?</DialogTitle>
|
<DialogTitle>Are you sure you want to delete "{documentTitle}"?</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
You are about to {canManageDocument ? 'delete' : 'hide'}{' '}
|
Please note that this action is irreversible. Once confirmed, your document will be
|
||||||
<strong>"{documentTitle}"</strong>
|
permanently deleted.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{canManageDocument ? (
|
{status !== DocumentStatus.DRAFT && (
|
||||||
<Alert variant="warning" className="-mt-1">
|
<div className="mt-4">
|
||||||
{match(status)
|
|
||||||
.with(DocumentStatus.DRAFT, () => (
|
|
||||||
<AlertDescription>
|
|
||||||
Please note that this action is <strong>irreversible</strong>. Once confirmed,
|
|
||||||
this document will be permanently deleted.
|
|
||||||
</AlertDescription>
|
|
||||||
))
|
|
||||||
.with(DocumentStatus.PENDING, () => (
|
|
||||||
<AlertDescription>
|
|
||||||
<p>
|
|
||||||
Please note that this action is <strong>irreversible</strong>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-1">Once confirmed, the following will occur:</p>
|
|
||||||
|
|
||||||
<ul className="mt-0.5 list-inside list-disc">
|
|
||||||
<li>Document will be permanently deleted</li>
|
|
||||||
<li>Document signing process will be cancelled</li>
|
|
||||||
<li>All inserted signatures will be voided</li>
|
|
||||||
<li>All recipients will be notified</li>
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
))
|
|
||||||
.with(DocumentStatus.COMPLETED, () => (
|
|
||||||
<AlertDescription>
|
|
||||||
<p>By deleting this document, the following will occur:</p>
|
|
||||||
|
|
||||||
<ul className="mt-0.5 list-inside list-disc">
|
|
||||||
<li>The document will be hidden from your account</li>
|
|
||||||
<li>Recipients will still retain their copy of the document</li>
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
))
|
|
||||||
.exhaustive()}
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert variant="warning" className="-mt-1">
|
|
||||||
<AlertDescription>
|
|
||||||
Please contact support if you would like to revert this action.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status !== DocumentStatus.DRAFT && canManageDocument && (
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
placeholder="Type 'delete' to confirm"
|
placeholder="Type 'delete' to confirm"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -159,11 +117,13 @@ export const DeleteDocumentDialog = ({
|
|||||||
type="button"
|
type="button"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
disabled={!isDeleteEnabled && canManageDocument}
|
disabled={!isDeleteEnabled}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{canManageDocument ? 'Delete' : 'Hide'}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
const page = Number(searchParams.page) || 1;
|
const page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
||||||
const currentTeam = team
|
const currentTeam = team ? { id: team.id, url: team.url } : undefined;
|
||||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const getStatOptions: GetStatsInput = {
|
const getStatOptions: GetStatsInput = {
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -37,10 +37,7 @@ export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
|
||||||
className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4"
|
|
||||||
data-testid="empty-document-state"
|
|
||||||
>
|
|
||||||
<Icon className="h-12 w-12" strokeWidth={1.5} />
|
<Icon className="h-12 w-12" strokeWidth={1.5} />
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ 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 { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
|
||||||
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 { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -58,7 +57,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const { type, data } = await putPdfFile(file);
|
const { type, data } = await putFile(file);
|
||||||
|
|
||||||
const { id: documentDataId } = await createDocumentData({
|
const { id: documentDataId } = await createDocumentData({
|
||||||
type,
|
type,
|
||||||
@@ -84,21 +83,13 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
|
router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
const error = AppError.parseError(err);
|
console.error(error);
|
||||||
|
|
||||||
console.error(err);
|
if (error instanceof TRPCClientError) {
|
||||||
|
|
||||||
if (error.code === 'INVALID_DOCUMENT_FILE') {
|
|
||||||
toast({
|
|
||||||
title: 'Invalid file',
|
|
||||||
description: 'You cannot upload encrypted PDFs',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} else if (err instanceof TRPCClientError) {
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: err.message,
|
description: error.message,
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import {
|
import type { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client';
|
||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
SKIP_QUERY_BATCH_META,
|
|
||||||
} from '@documenso/lib/constants/trpc';
|
|
||||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
|
||||||
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';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@@ -23,135 +19,52 @@ import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-
|
|||||||
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
|
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
|
||||||
import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients';
|
import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients';
|
||||||
import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types';
|
import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types';
|
||||||
import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-settings';
|
|
||||||
import type { TAddTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
export type EditTemplateFormProps = {
|
export type EditTemplateFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
initialTemplate: TemplateWithDetails;
|
user: User;
|
||||||
isEnterprise: boolean;
|
template: Template;
|
||||||
|
recipients: Recipient[];
|
||||||
|
fields: Field[];
|
||||||
|
documentData: DocumentData;
|
||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditTemplateStep = 'settings' | 'signers' | 'fields';
|
type EditTemplateStep = 'signers' | 'fields';
|
||||||
const EditTemplateSteps: EditTemplateStep[] = ['settings', 'signers', 'fields'];
|
const EditTemplateSteps: EditTemplateStep[] = ['signers', 'fields'];
|
||||||
|
|
||||||
export const EditTemplateForm = ({
|
export const EditTemplateForm = ({
|
||||||
initialTemplate,
|
|
||||||
className,
|
className,
|
||||||
isEnterprise,
|
template,
|
||||||
|
recipients,
|
||||||
|
fields,
|
||||||
|
user: _user,
|
||||||
|
documentData,
|
||||||
templateRootPath,
|
templateRootPath,
|
||||||
}: EditTemplateFormProps) => {
|
}: EditTemplateFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const [step, setStep] = useState<EditTemplateStep>('signers');
|
||||||
|
|
||||||
const [step, setStep] = useState<EditTemplateStep>('settings');
|
|
||||||
|
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
|
|
||||||
const { data: template, refetch: refetchTemplate } =
|
|
||||||
trpc.template.getTemplateWithDetailsById.useQuery(
|
|
||||||
{
|
|
||||||
id: initialTemplate.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initialData: initialTemplate,
|
|
||||||
...SKIP_QUERY_BATCH_META,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { Recipient: recipients, Field: fields, templateDocumentData } = template;
|
|
||||||
|
|
||||||
const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = {
|
const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = {
|
||||||
settings: {
|
|
||||||
title: 'General',
|
|
||||||
description: 'Configure general settings for the template.',
|
|
||||||
stepIndex: 1,
|
|
||||||
},
|
|
||||||
signers: {
|
signers: {
|
||||||
title: 'Add Placeholders',
|
title: 'Add Placeholders',
|
||||||
description: 'Add all relevant placeholders for each recipient.',
|
description: 'Add all relevant placeholders for each recipient.',
|
||||||
stepIndex: 2,
|
stepIndex: 1,
|
||||||
},
|
},
|
||||||
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: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentDocumentFlow = documentFlow[step];
|
const currentDocumentFlow = documentFlow[step];
|
||||||
|
|
||||||
const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplateSettings.useMutation({
|
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation();
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation();
|
||||||
onSuccess: (newData) => {
|
|
||||||
utils.template.getTemplateWithDetailsById.setData(
|
|
||||||
{
|
|
||||||
id: initialTemplate.id,
|
|
||||||
},
|
|
||||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
onSuccess: (newData) => {
|
|
||||||
utils.template.getTemplateWithDetailsById.setData(
|
|
||||||
{
|
|
||||||
id: initialTemplate.id,
|
|
||||||
},
|
|
||||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
|
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
onSuccess: (newData) => {
|
|
||||||
utils.template.getTemplateWithDetailsById.setData(
|
|
||||||
{
|
|
||||||
id: initialTemplate.id,
|
|
||||||
},
|
|
||||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
|
||||||
try {
|
|
||||||
await updateTemplateSettings({
|
|
||||||
templateId: template.id,
|
|
||||||
teamId: team?.id,
|
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
|
||||||
},
|
|
||||||
meta: data.meta,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setStep('signers');
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while updating the document settings.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddTemplatePlaceholderFormSubmit = async (
|
const onAddTemplatePlaceholderFormSubmit = async (
|
||||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||||
@@ -159,11 +72,9 @@ export const EditTemplateForm = ({
|
|||||||
try {
|
try {
|
||||||
await addTemplateSigners({
|
await addTemplateSigners({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
teamId: team?.id,
|
|
||||||
signers: data.signers,
|
signers: data.signers,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
setStep('fields');
|
setStep('fields');
|
||||||
@@ -189,9 +100,6 @@ export const EditTemplateForm = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
router.push(templateRootPath);
|
router.push(templateRootPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
@@ -202,15 +110,6 @@ export const EditTemplateForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the data in the background when steps change.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
void refetchTemplate();
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [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
|
||||||
@@ -218,11 +117,7 @@ export const EditTemplateForm = ({
|
|||||||
gradient
|
gradient
|
||||||
>
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<LazyPDFViewer
|
<LazyPDFViewer key={documentData.id} documentData={documentData} />
|
||||||
key={templateDocumentData.id}
|
|
||||||
documentData={templateDocumentData}
|
|
||||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -240,25 +135,12 @@ export const EditTemplateForm = ({
|
|||||||
currentStep={currentDocumentFlow.stepIndex}
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
setCurrentStep={(step) => setStep(EditTemplateSteps[step - 1])}
|
setCurrentStep={(step) => setStep(EditTemplateSteps[step - 1])}
|
||||||
>
|
>
|
||||||
<AddTemplateSettingsFormPartial
|
|
||||||
key={recipients.length}
|
|
||||||
template={template}
|
|
||||||
documentFlow={documentFlow.settings}
|
|
||||||
recipients={recipients}
|
|
||||||
fields={fields}
|
|
||||||
onSubmit={onAddSettingsFormSubmit}
|
|
||||||
isEnterprise={isEnterprise}
|
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AddTemplatePlaceholderRecipientsFormPartial
|
<AddTemplatePlaceholderRecipientsFormPartial
|
||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||||
isEnterprise={isEnterprise}
|
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddTemplateFieldsFormPartial
|
<AddTemplateFieldsFormPartial
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
|
import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template';
|
||||||
|
import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template';
|
||||||
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
import type { Team } from '@documenso/prisma/client';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
|||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const template = await getTemplateWithDetailsById({
|
const template = await getTemplateById({
|
||||||
id: templateId,
|
id: templateId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
@@ -43,13 +44,21 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
|||||||
redirect(templateRootPath);
|
redirect(templateRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTemplateEnterprise = await isUserEnterprise({
|
const { templateDocumentData } = template;
|
||||||
|
|
||||||
|
const [templateRecipients, templateFields] = await Promise.all([
|
||||||
|
getRecipientsForTemplate({
|
||||||
|
templateId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
}),
|
||||||
});
|
getFieldsForTemplate({
|
||||||
|
templateId,
|
||||||
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||||
<Link href="/templates" className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link href="/templates" className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
Templates
|
Templates
|
||||||
@@ -64,10 +73,13 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditTemplateForm
|
<EditTemplateForm
|
||||||
className="mt-6"
|
className="mt-8"
|
||||||
initialTemplate={template}
|
template={template}
|
||||||
|
user={user}
|
||||||
|
recipients={templateRecipients}
|
||||||
|
fields={templateFields}
|
||||||
|
documentData={templateDocumentData}
|
||||||
templateRootPath={templateRootPath}
|
templateRootPath={templateRootPath}
|
||||||
isEnterprise={isTemplateEnterprise}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,29 +1,48 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { FilePlus, Loader } from 'lucide-react';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { FilePlus, X } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
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 { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
const ZCreateTemplateFormSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TCreateTemplateFormSchema = z.infer<typeof ZCreateTemplateFormSchema>;
|
||||||
|
|
||||||
type NewTemplateDialogProps = {
|
type NewTemplateDialogProps = {
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
@@ -35,20 +54,51 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
const form = useForm<TCreateTemplateFormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZCreateTemplateFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createTemplate, isLoading: isCreatingTemplate } =
|
||||||
|
trpc.template.createTemplate.useMutation();
|
||||||
|
|
||||||
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
if (isUploadingFile) {
|
try {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const base64String = base64.encode(new Uint8Array(arrayBuffer));
|
||||||
|
|
||||||
|
setUploadedFile({
|
||||||
|
file,
|
||||||
|
fileBase64: `data:application/pdf;base64,${base64String}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!form.getValues('name')) {
|
||||||
|
form.setValue('name', file.name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description: 'Please try again later.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (values: TCreateTemplateFormSchema) => {
|
||||||
|
if (!uploadedFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploadingFile(true);
|
const file: File = uploadedFile.file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { type, data } = await putPdfFile(file);
|
const { type, data } = await putFile(file);
|
||||||
|
|
||||||
const { id: templateDocumentDataId } = await createDocumentData({
|
const { id: templateDocumentDataId } = await createDocumentData({
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
@@ -56,7 +106,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
teamId,
|
teamId,
|
||||||
title: file.name,
|
title: values.name ? values.name : file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,16 +126,25 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
description: 'Please try again later.',
|
description: 'Please try again later.',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsUploadingFile(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
if (form.getValues('name') === uploadedFile?.file.name) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showNewTemplateDialog) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}, [form, showNewTemplateDialog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={showNewTemplateDialog} onOpenChange={setShowNewTemplateDialog}>
|
||||||
open={showNewTemplateDialog}
|
|
||||||
onOpenChange={(value) => !isUploadingFile && setShowNewTemplateDialog(value)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="cursor-pointer" disabled={!session?.user.emailVerified}>
|
<Button className="cursor-pointer" disabled={!session?.user.emailVerified}>
|
||||||
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
||||||
@@ -95,29 +154,81 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
|
|
||||||
<DialogContent className="w-full max-w-xl">
|
<DialogContent className="w-full max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>New Template</DialogTitle>
|
<DialogTitle className="mb-4">New Template</DialogTitle>
|
||||||
<DialogDescription>
|
|
||||||
Templates allow you to quickly generate documents with pre-filled recipients and fields.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="relative">
|
<div>
|
||||||
<DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name your template</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="email" type="text" className="bg-background mt-1.5" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
Leave this empty if you would like to use your document's name for the
|
||||||
|
template
|
||||||
|
</span>
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{isUploadingFile && (
|
<div>
|
||||||
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
|
<Label htmlFor="template">Upload a Document</Label>
|
||||||
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
|
||||||
|
<div className="my-3">
|
||||||
|
{uploadedFile ? (
|
||||||
|
<Card gradient className="h-[40vh]">
|
||||||
|
<CardContent className="flex h-full flex-col items-center justify-center p-2">
|
||||||
|
<button
|
||||||
|
onClick={() => resetForm()}
|
||||||
|
title="Remove Template"
|
||||||
|
className="text-muted-foreground absolute right-2.5 top-2.5 rounded-sm opacity-60 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
<span className="sr-only">Remove Template</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm">
|
||||||
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||||
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="group-hover:text-foreground text-muted-foreground mt-4 font-medium">
|
||||||
|
Uploaded Document
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span className="text-muted-foreground/80 mt-1 text-sm">
|
||||||
|
{uploadedFile.file.name}
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<DocumentDropzone
|
||||||
|
className="mt-1.5 h-[40vh]"
|
||||||
|
onDrop={onFileDrop}
|
||||||
|
type="template"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<div className="flex w-full justify-end">
|
||||||
<DialogClose asChild>
|
<Button loading={isCreatingTemplate} type="submit">
|
||||||
<Button type="button" variant="secondary" disabled={isUploadingFile}>
|
Create Template
|
||||||
Close
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</div>
|
||||||
</DialogFooter>
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { InfoIcon, Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import {
|
|
||||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
|
||||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
|
||||||
} from '@documenso/lib/constants/template';
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -26,58 +19,23 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import {
|
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@documenso/ui/primitives/form/form';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z.object({
|
||||||
.object({
|
|
||||||
sendDocument: z.boolean(),
|
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
|
||||||
// Display exactly which rows are duplicates.
|
|
||||||
.superRefine((items, ctx) => {
|
|
||||||
const uniqueEmails = new Map<string, number>();
|
|
||||||
|
|
||||||
for (const [index, recipients] of items.recipients.entries()) {
|
|
||||||
const email = recipients.email.toLowerCase();
|
|
||||||
|
|
||||||
const firstFoundIndex = uniqueEmails.get(email);
|
|
||||||
|
|
||||||
if (firstFoundIndex === undefined) {
|
|
||||||
uniqueEmails.set(email, index);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: 'Emails must be unique',
|
|
||||||
path: ['recipients', index, 'email'],
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: 'Emails must be unique',
|
|
||||||
path: ['recipients', firstFoundIndex, 'email'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||||
@@ -96,33 +54,35 @@ export function UseTemplateDialog({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
sendDocument: false,
|
recipients:
|
||||||
recipients: recipients.map((recipient) => {
|
recipients.length > 0
|
||||||
const isRecipientEmailPlaceholder = recipient.email.match(
|
? recipients.map((recipient) => ({
|
||||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
nativeId: recipient.id,
|
||||||
);
|
formId: String(recipient.id),
|
||||||
|
name: recipient.name,
|
||||||
const isRecipientNamePlaceholder = recipient.name.match(
|
email: recipient.email,
|
||||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
role: recipient.role,
|
||||||
);
|
}))
|
||||||
|
: [
|
||||||
return {
|
{
|
||||||
id: recipient.id,
|
name: '',
|
||||||
name: !isRecipientNamePlaceholder ? recipient.name : '',
|
email: '',
|
||||||
email: !isRecipientEmailPlaceholder ? recipient.email : '',
|
role: RecipientRole.SIGNER,
|
||||||
};
|
},
|
||||||
}),
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createDocumentFromTemplate } =
|
const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } =
|
||||||
trpc.template.createDocumentFromTemplate.useMutation();
|
trpc.template.createDocumentFromTemplate.useMutation();
|
||||||
|
|
||||||
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
||||||
@@ -131,7 +91,6 @@ export function UseTemplateDialog({
|
|||||||
templateId,
|
templateId,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: data.recipients,
|
recipients: data.recipients,
|
||||||
sendDocument: data.sendDocument,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -142,35 +101,23 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
router.push(`${documentRootPath}/${id}`);
|
router.push(`${documentRootPath}/${id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
toast({
|
||||||
|
|
||||||
const toastPayload: Toast = {
|
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: 'An error occurred while creating document from template.',
|
description: 'An error occurred while creating document from template.',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
};
|
});
|
||||||
|
|
||||||
if (error.code === 'DOCUMENT_SEND_FAILED') {
|
|
||||||
toastPayload.description = 'The document was created but could not be sent to recipients.';
|
|
||||||
}
|
|
||||||
|
|
||||||
toast(toastPayload);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCreateDocumentFromTemplate = handleSubmit(onSubmit);
|
||||||
|
|
||||||
const { fields: formRecipients } = useFieldArray({
|
const { fields: formRecipients } = useFieldArray({
|
||||||
control: form.control,
|
control,
|
||||||
name: 'recipients',
|
name: 'recipients',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
}, [open, form]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="cursor-pointer">
|
<Button className="cursor-pointer">
|
||||||
<Plus className="-ml-1 mr-2 h-4 w-4" />
|
<Plus className="-ml-1 mr-2 h-4 w-4" />
|
||||||
@@ -179,110 +126,121 @@ export function UseTemplateDialog({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create document from template</DialogTitle>
|
<DialogTitle>Document Recipients</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Add the recipients to create the template with.</DialogDescription>
|
||||||
{recipients.length === 0
|
|
||||||
? 'A draft document will be created'
|
|
||||||
: 'Add the recipients to create the document with'}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
|
||||||
<div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1">
|
|
||||||
{formRecipients.map((recipient, index) => (
|
{formRecipients.map((recipient, index) => (
|
||||||
<div className="flex w-full flex-row space-x-4" key={recipient.id}>
|
<div
|
||||||
<FormField
|
key={recipient.id}
|
||||||
control={form.control}
|
data-native-id={recipient.id}
|
||||||
|
className="flex flex-wrap items-end gap-x-4"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor={`recipient-${recipient.id}-email`}>
|
||||||
|
Email
|
||||||
|
<span className="text-destructive ml-1 inline-block font-medium">*</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
name={`recipients.${index}.email`}
|
name={`recipients.${index}.email`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<Input
|
||||||
{index === 0 && <FormLabel required>Email</FormLabel>}
|
id={`recipient-${recipient.id}-email`}
|
||||||
|
type="email"
|
||||||
<FormControl>
|
className="bg-background mt-2"
|
||||||
<Input {...field} placeholder={recipients[index].email || 'Email'} />
|
disabled={isSubmitting}
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div className="flex-1">
|
||||||
control={form.control}
|
<Label htmlFor={`recipient-${recipient.id}-name`}>Name</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
name={`recipients.${index}.name`}
|
name={`recipients.${index}.name`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<Input
|
||||||
{index === 0 && <FormLabel>Name</FormLabel>}
|
id={`recipient-${recipient.id}-name`}
|
||||||
|
type="text"
|
||||||
<FormControl>
|
className="bg-background mt-2"
|
||||||
<Input {...field} placeholder={recipients[index].name || 'Name'} />
|
disabled={isSubmitting}
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-[60px]">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`recipients.${index}.role`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Select value={value} onValueChange={(x) => onChange(x)}>
|
||||||
|
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent className="" align="end">
|
||||||
|
<SelectItem value={RecipientRole.SIGNER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||||
|
Signer
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.CC}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||||
|
Receives copy
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.APPROVER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||||
|
Approver
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.VIEWER}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||||
|
Viewer
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.email} />
|
||||||
|
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipients.length > 0 && (
|
<DialogFooter className="justify-end">
|
||||||
<div className="mt-4 flex flex-row items-center">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sendDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="sendDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checkClassName="dark:text-white text-primary"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="sendDocument"
|
|
||||||
>
|
|
||||||
Send document
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
The document will be immediately sent to recipients if this is
|
|
||||||
checked.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Otherwise, the document will be created as a draft.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary">
|
<Button type="button" variant="secondary">
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button
|
||||||
{form.getValues('sendDocument') ? 'Create and send' : 'Create as draft'}
|
type="button"
|
||||||
|
loading={isCreatingDocumentFromTemplate}
|
||||||
|
disabled={isCreatingDocumentFromTemplate}
|
||||||
|
onClick={onCreateDocumentFromTemplate}
|
||||||
|
>
|
||||||
|
Create Document
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import type { DateTimeFormatOptions } from 'luxon';
|
|
||||||
import { UAParser } from 'ua-parser-js';
|
|
||||||
|
|
||||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@documenso/ui/primitives/table';
|
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
export type AuditLogDataTableProps = {
|
|
||||||
logs: TDocumentAuditLog[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateFormat: DateTimeFormatOptions = {
|
|
||||||
...DateTime.DATETIME_SHORT,
|
|
||||||
hourCycle: 'h12',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
|
|
||||||
const parser = new UAParser();
|
|
||||||
|
|
||||||
const uppercaseFistLetter = (text: string) => {
|
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table overflowHidden>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Time</TableHead>
|
|
||||||
<TableHead>User</TableHead>
|
|
||||||
<TableHead>Action</TableHead>
|
|
||||||
<TableHead>IP Address</TableHead>
|
|
||||||
<TableHead>Browser</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
|
|
||||||
<TableBody className="print:text-xs">
|
|
||||||
{logs.map((log, i) => (
|
|
||||||
<TableRow className="break-inside-avoid" key={i}>
|
|
||||||
<TableCell>
|
|
||||||
<LocaleDate format={dateFormat} date={log.createdAt} />
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
|
||||||
{log.name || log.email ? (
|
|
||||||
<div>
|
|
||||||
{log.name && (
|
|
||||||
<p className="break-all" title={log.name}>
|
|
||||||
{log.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{log.email && (
|
|
||||||
<p className="text-muted-foreground break-all" title={log.email}>
|
|
||||||
{log.email}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>N/A</p>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
|
||||||
{uppercaseFistLetter(formatDocumentAuditLogAction(log).description)}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell>{log.ipAddress}</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
|
||||||
{log.userAgent ? parser.setUA(log.userAgent).getBrowser().name : 'N/A'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
|
||||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
|
||||||
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { AuditLogDataTable } from './data-table';
|
|
||||||
|
|
||||||
type AuditLogProps = {
|
|
||||||
searchParams: {
|
|
||||||
d: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function AuditLog({ searchParams }: AuditLogProps) {
|
|
||||||
const { d } = searchParams;
|
|
||||||
|
|
||||||
if (typeof d !== 'string' || !d) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawDocumentId = decryptSecondaryData(d);
|
|
||||||
|
|
||||||
if (!rawDocumentId || isNaN(Number(rawDocumentId))) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentId = Number(rawDocumentId);
|
|
||||||
|
|
||||||
const document = await getEntireDocument({
|
|
||||||
id: documentId,
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: auditLogs } = await findDocumentAuditLogs({
|
|
||||||
documentId: documentId,
|
|
||||||
userId: document.userId,
|
|
||||||
perPage: 100_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h1 className="my-8 text-2xl font-bold">Version History</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Document ID</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block break-words">{document.id}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Enclosed Document</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block break-words">{document.title}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Status</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block">{document.deletedAt ? 'DELETED' : document.status}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Owner</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block break-words">
|
|
||||||
{document.User.name} ({document.User.email})
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Created At</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block">
|
|
||||||
<LocaleDate date={document.createdAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Last Updated</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block">
|
|
||||||
<LocaleDate date={document.updatedAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Time Zone</span>
|
|
||||||
|
|
||||||
<span className="mt-1 block break-words">
|
|
||||||
{document.documentMeta?.timezone ?? 'N/A'}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Recipients</p>
|
|
||||||
|
|
||||||
<ul className="mt-1 list-inside list-disc">
|
|
||||||
{document.Recipient.map((recipient) => (
|
|
||||||
<li key={recipient.id}>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
[{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}]
|
|
||||||
</span>{' '}
|
|
||||||
{recipient.name} ({recipient.email})
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="mt-8">
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<AuditLogDataTable logs={auditLogs} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="my-8 flex-row-reverse">
|
|
||||||
<div className="flex items-end justify-end gap-x-4">
|
|
||||||
<Logo className="max-h-6 print:max-h-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import { UAParser } from 'ua-parser-js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
RECIPIENT_ROLES_DESCRIPTION,
|
|
||||||
RECIPIENT_ROLE_SIGNING_REASONS,
|
|
||||||
} from '@documenso/lib/constants/recipient-roles';
|
|
||||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
|
||||||
import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@documenso/ui/primitives/table';
|
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
type SigningCertificateProps = {
|
|
||||||
searchParams: {
|
|
||||||
d: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const FRIENDLY_SIGNING_REASONS = {
|
|
||||||
['__OWNER__']: 'I am the owner of this document',
|
|
||||||
...RECIPIENT_ROLE_SIGNING_REASONS,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
|
|
||||||
const { d } = searchParams;
|
|
||||||
|
|
||||||
if (typeof d !== 'string' || !d) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawDocumentId = decryptSecondaryData(d);
|
|
||||||
|
|
||||||
if (!rawDocumentId || isNaN(Number(rawDocumentId))) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentId = Number(rawDocumentId);
|
|
||||||
|
|
||||||
const document = await getEntireDocument({
|
|
||||||
id: documentId,
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const auditLogs = await getDocumentCertificateAuditLogs({
|
|
||||||
id: documentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOwner = (email: string) => {
|
|
||||||
return email.toLowerCase() === document.User.email.toLowerCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDevice = (userAgent?: string | null) => {
|
|
||||||
if (!userAgent) {
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new UAParser(userAgent);
|
|
||||||
|
|
||||||
parser.setUA(userAgent);
|
|
||||||
|
|
||||||
const result = parser.getResult();
|
|
||||||
|
|
||||||
return `${result.os.name} - ${result.browser.name} ${result.browser.version}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAuthenticationLevel = (recipientId: number) => {
|
|
||||||
const recipient = document.Recipient.find((recipient) => recipient.id === recipientId);
|
|
||||||
|
|
||||||
if (!recipient) {
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractedAuthMethods = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
recipientAuth: recipient.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
|
|
||||||
.with('ACCOUNT', () => 'Account Re-Authentication')
|
|
||||||
.with('TWO_FACTOR_AUTH', () => 'Two-Factor Re-Authentication')
|
|
||||||
.with('PASSKEY', () => 'Passkey Re-Authentication')
|
|
||||||
.with('EXPLICIT_NONE', () => 'Email')
|
|
||||||
.with(null, () => null)
|
|
||||||
.exhaustive();
|
|
||||||
|
|
||||||
if (!authLevel) {
|
|
||||||
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
|
|
||||||
.with('ACCOUNT', () => 'Account Authentication')
|
|
||||||
.with(null, () => 'Email')
|
|
||||||
.exhaustive();
|
|
||||||
}
|
|
||||||
|
|
||||||
return authLevel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRecipientAuditLogs = (recipientId: number) => {
|
|
||||||
return {
|
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT].filter(
|
|
||||||
(log) =>
|
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
|
|
||||||
),
|
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
|
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
|
|
||||||
].filter(
|
|
||||||
(log) =>
|
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED &&
|
|
||||||
log.data.recipientId === recipientId,
|
|
||||||
),
|
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED]: auditLogs[
|
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED
|
|
||||||
].filter(
|
|
||||||
(log) =>
|
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED &&
|
|
||||||
log.data.recipientId === recipientId,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRecipientSignatureField = (recipientId: number) => {
|
|
||||||
return document.Recipient.find((recipient) => recipient.id === recipientId)?.Field.find(
|
|
||||||
(field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h1 className="my-8 text-2xl font-bold">Signing Certificate</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<Table overflowHidden>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Signer Events</TableHead>
|
|
||||||
<TableHead>Signature</TableHead>
|
|
||||||
<TableHead>Details</TableHead>
|
|
||||||
{/* <TableHead>Security</TableHead> */}
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
|
|
||||||
<TableBody className="print:text-xs">
|
|
||||||
{document.Recipient.map((recipient, i) => {
|
|
||||||
const logs = getRecipientAuditLogs(recipient.id);
|
|
||||||
const signature = getRecipientSignatureField(recipient.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={i} className="print:break-inside-avoid">
|
|
||||||
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
|
||||||
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
|
||||||
<div className="break-all">{recipient.email}</div>
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
|
||||||
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Authentication Level:</span>{' '}
|
|
||||||
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
|
||||||
</p>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
|
||||||
{signature ? (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="inline-block rounded-lg p-1"
|
|
||||||
style={{
|
|
||||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={`${signature.Signature?.signatureImageAsBase64}`}
|
|
||||||
alt="Signature"
|
|
||||||
className="max-h-12 max-w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Signature ID:</span>{' '}
|
|
||||||
<span className="block font-mono uppercase">
|
|
||||||
{signature.secondaryId}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
|
||||||
<span className="font-medium">IP Address:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? 'Unknown'}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Device:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-muted-foreground">N/A</p>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Sent:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{logs.EMAIL_SENT[0] ? (
|
|
||||||
<LocaleDate
|
|
||||||
date={logs.EMAIL_SENT[0].createdAt}
|
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Viewed:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{logs.DOCUMENT_OPENED[0] ? (
|
|
||||||
<LocaleDate
|
|
||||||
date={logs.DOCUMENT_OPENED[0].createdAt}
|
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Signed:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] ? (
|
|
||||||
<LocaleDate
|
|
||||||
date={logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt}
|
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
|
||||||
<span className="font-medium">Reason:</span>{' '}
|
|
||||||
<span className="inline-block">
|
|
||||||
{isOwner(recipient.email)
|
|
||||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
|
||||||
: FRIENDLY_SIGNING_REASONS[recipient.role]}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="my-8 flex-row-reverse">
|
|
||||||
<div className="flex items-end justify-end gap-x-4">
|
|
||||||
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
|
|
||||||
Signing certificate provided by:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Logo className="max-h-6 print:max-h-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@documenso/ui/primitives/form/form';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export type ClaimAccountProps = {
|
|
||||||
defaultName: string;
|
|
||||||
defaultEmail: string;
|
|
||||||
trigger?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ZClaimAccountFormSchema = z
|
|
||||||
.object({
|
|
||||||
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
|
||||||
email: z.string().email().min(1),
|
|
||||||
password: ZPasswordSchema,
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const { name, email, password } = data;
|
|
||||||
return !password.includes(name) && !password.includes(email.split('@')[0]);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Password should not be common or based on personal information',
|
|
||||||
path: ['password'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TClaimAccountFormSchema = z.infer<typeof ZClaimAccountFormSchema>;
|
|
||||||
|
|
||||||
export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) => {
|
|
||||||
const analytics = useAnalytics();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<TClaimAccountFormSchema>({
|
|
||||||
values: {
|
|
||||||
name: defaultName ?? '',
|
|
||||||
email: defaultEmail,
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
resolver: zodResolver(ZClaimAccountFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
|
||||||
try {
|
|
||||||
await signup({ name, email, password });
|
|
||||||
|
|
||||||
router.push(`/unverified-account`);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Registration Successful',
|
|
||||||
description:
|
|
||||||
'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
analytics.capture('App: User Claim Account', {
|
|
||||||
email,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
|
|
||||||
toast({
|
|
||||||
title: 'An error occurred',
|
|
||||||
description: error.message,
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: 'An unknown error occurred',
|
|
||||||
description:
|
|
||||||
'We encountered an unknown error while attempting to sign you up. Please try again later.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 w-full">
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
|
||||||
<fieldset disabled={form.formState.isSubmitting} className="mt-4">
|
|
||||||
<FormField
|
|
||||||
name="name"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="Enter your name" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="email"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4">
|
|
||||||
<FormLabel>Email address</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="Enter your email" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="password"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4">
|
|
||||||
<FormLabel>Set a password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput {...field} placeholder="Pick a password" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="mt-6 w-full" loading={form.formState.isSubmitting}>
|
|
||||||
Claim account
|
|
||||||
</Button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { CheckCircle2, Clock8 } from 'lucide-react';
|
import { CheckCircle2, Clock8 } from 'lucide-react';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { env } from 'next-runtime-env';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||||
@@ -17,13 +16,10 @@ import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/clie
|
|||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
|
||||||
|
|
||||||
import { truncateTitle } from '~/helpers/truncate-title';
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
import { SigningAuthPageView } from '../signing-auth-page';
|
import { SigningAuthPageView } from '../signing-auth-page';
|
||||||
import { ClaimAccount } from './claim-account';
|
|
||||||
import { DocumentPreviewButton } from './document-preview-button';
|
import { DocumentPreviewButton } from './document-preview-button';
|
||||||
|
|
||||||
export type CompletedSigningPageProps = {
|
export type CompletedSigningPageProps = {
|
||||||
@@ -35,8 +31,6 @@ export type CompletedSigningPageProps = {
|
|||||||
export default async function CompletedSigningPage({
|
export default async function CompletedSigningPage({
|
||||||
params: { token },
|
params: { token },
|
||||||
}: CompletedSigningPageProps) {
|
}: CompletedSigningPageProps) {
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
@@ -85,30 +79,9 @@ export default async function CompletedSigningPage({
|
|||||||
|
|
||||||
const sessionData = await getServerSession();
|
const sessionData = await getServerSession();
|
||||||
const isLoggedIn = !!sessionData?.user;
|
const isLoggedIn = !!sessionData?.user;
|
||||||
const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
|
||||||
className={cn(
|
|
||||||
'-mx-4 flex flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44',
|
|
||||||
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn('relative mt-6 flex w-full flex-col items-center justify-center', {
|
|
||||||
'mt-0 flex-col divide-y overflow-hidden pt-6 md:pt-16 lg:flex-row lg:divide-x lg:divide-y-0 lg:pt-20 xl:pt-24':
|
|
||||||
canSignUp,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn('flex flex-col items-center', {
|
|
||||||
'mb-8 p-4 md:mb-0 md:p-12': canSignUp,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Badge variant="neutral" size="default" className="mb-6 rounded-xl border bg-transparent">
|
|
||||||
{truncatedTitle}
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
{/* Card with recipient */}
|
{/* Card with recipient */}
|
||||||
<SigningCard3D
|
<SigningCard3D
|
||||||
name={recipientName}
|
name={recipientName}
|
||||||
@@ -116,22 +89,16 @@ export default async function CompletedSigningPage({
|
|||||||
signingCelebrationImage={signingCelebration}
|
signingCelebrationImage={signingCelebration}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
<div className="relative mt-6 flex w-full flex-col items-center">
|
||||||
Document
|
|
||||||
{recipient.role === RecipientRole.SIGNER && ' Signed '}
|
|
||||||
{recipient.role === RecipientRole.VIEWER && ' Viewed '}
|
|
||||||
{recipient.role === RecipientRole.APPROVER && ' Approved '}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{match({ status: document.status, deletedAt: document.deletedAt })
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with({ status: DocumentStatus.COMPLETED }, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<div className="text-documenso-700 mt-4 flex items-center text-center">
|
<div className="text-documenso-700 flex items-center text-center">
|
||||||
<CheckCircle2 className="mr-2 h-5 w-5" />
|
<CheckCircle2 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">Everyone has signed</span>
|
<span className="text-sm">Everyone has signed</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null }, () => (
|
||||||
<div className="flex items-center mt-4 text-center text-blue-600">
|
<div className="flex items-center text-center text-blue-600">
|
||||||
<Clock8 className="mr-2 h-5 w-5" />
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">Waiting for others to sign</span>
|
<span className="text-sm">Waiting for others to sign</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,6 +110,14 @@ export default async function CompletedSigningPage({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
|
You have
|
||||||
|
{recipient.role === RecipientRole.SIGNER && ' signed '}
|
||||||
|
{recipient.role === RecipientRole.VIEWER && ' viewed '}
|
||||||
|
{recipient.role === RecipientRole.APPROVER && ' approved '}
|
||||||
|
<span className="mt-1.5 block">"{truncatedTitle}"</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
{match({ status: document.status, deletedAt: document.deletedAt })
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
.with({ status: DocumentStatus.COMPLETED }, () => (
|
.with({ status: DocumentStatus.COMPLETED }, () => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
@@ -156,8 +131,8 @@ export default async function CompletedSigningPage({
|
|||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||||
This document has been cancelled by the owner and is no longer available for others
|
This document has been cancelled by the owner and is no longer available for others to
|
||||||
to sign.
|
sign.
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -179,26 +154,21 @@ export default async function CompletedSigningPage({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{canSignUp && (
|
{isLoggedIn ? (
|
||||||
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
|
|
||||||
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
|
|
||||||
Need to sign documents?
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
|
|
||||||
Create your account and start using state-of-the-art document signing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoggedIn && (
|
|
||||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
||||||
Go Back Home
|
Go Back Home
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground/60 mt-36 text-sm">
|
||||||
|
Want to send slick signing links like this one?{' '}
|
||||||
|
<Link
|
||||||
|
href="https://documenso.com"
|
||||||
|
className="text-documenso-700 hover:text-documenso-600"
|
||||||
|
>
|
||||||
|
Check out Documenso.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
|
|||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
@@ -38,7 +37,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
const [document, fields, recipient] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@@ -46,15 +45,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
}).catch(() => null),
|
}).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
getCompletedFieldsForToken({ token }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (!document || !document.documentData || !recipient) {
|
||||||
!document ||
|
|
||||||
!document.documentData ||
|
|
||||||
!recipient ||
|
|
||||||
document.status === DocumentStatus.DRAFT
|
|
||||||
) {
|
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +120,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
signature={user?.email === recipient.email ? user.signature : undefined}
|
signature={user?.email === recipient.email ? user.signature : undefined}
|
||||||
>
|
>
|
||||||
<DocumentAuthProvider document={document} recipient={recipient} user={user}>
|
<DocumentAuthProvider document={document} recipient={recipient} user={user}>
|
||||||
<SigningPageView
|
<SigningPageView recipient={recipient} document={document} fields={fields} />
|
||||||
recipient={recipient}
|
|
||||||
document={document}
|
|
||||||
fields={fields}
|
|
||||||
completedFields={completedFields}
|
|
||||||
/>
|
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-form
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
|
||||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
|
||||||
import { truncateTitle } from '~/helpers/truncate-title';
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
@@ -25,15 +23,9 @@ export type SigningPageViewProps = {
|
|||||||
document: DocumentAndSender;
|
document: DocumentAndSender;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
completedFields: CompletedField[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningPageView = ({
|
export const SigningPageView = ({ document, recipient, fields }: SigningPageViewProps) => {
|
||||||
document,
|
|
||||||
recipient,
|
|
||||||
fields,
|
|
||||||
completedFields,
|
|
||||||
}: SigningPageViewProps) => {
|
|
||||||
const truncatedTitle = truncateTitle(document.title);
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
@@ -78,8 +70,6 @@ export const SigningPageView = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DocumentReadOnlyFields fields={completedFields} />
|
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
{fields.map((field) =>
|
{fields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import type { GetTeamTokensResponse } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
|
||||||
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -26,24 +23,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
|||||||
|
|
||||||
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
||||||
|
|
||||||
let tokens: GetTeamTokensResponse | null = null;
|
const tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
|
||||||
|
|
||||||
try {
|
|
||||||
tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
|
|
||||||
} catch (err) {
|
|
||||||
const error = AppError.parseError(err);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-semibold">API Tokens</h3>
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
|
||||||
{match(error.code)
|
|
||||||
.with(AppErrorCode.UNAUTHORIZED, () => error.message)
|
|
||||||
.otherwise(() => 'Something went wrong.')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
export default function SignatureDisclosure() {
|
export default function SignatureDisclosure() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<article className="prose dark:prose-invert">
|
<article className="prose">
|
||||||
<h1>Electronic Signature Disclosure</h1>
|
<h1>Electronic Signature Disclosure</h1>
|
||||||
|
|
||||||
<h2>Welcome</h2>
|
<h2>Welcome</h2>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -16,21 +15,18 @@ import { StackAvatar } from './stack-avatar';
|
|||||||
|
|
||||||
export type AvatarWithRecipientProps = {
|
export type AvatarWithRecipientProps = {
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
documentStatus: DocumentStatus;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) {
|
export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
|
|
||||||
|
|
||||||
const onRecipientClick = () => {
|
const onRecipientClick = () => {
|
||||||
if (!signingToken) {
|
if (!recipient.token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${signingToken}`).then(() => {
|
void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`).then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
description: 'The signing link has been copied to your clipboard.',
|
description: 'The signing link has been copied to your clipboard.',
|
||||||
@@ -41,10 +37,10 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('my-1 flex items-center gap-2', {
|
className={cn('my-1 flex items-center gap-2', {
|
||||||
'cursor-pointer hover:underline': signingToken,
|
'cursor-pointer hover:underline': recipient.token,
|
||||||
})}
|
})}
|
||||||
role={signingToken ? 'button' : undefined}
|
role={recipient.token ? 'button' : undefined}
|
||||||
title={signingToken ? 'Click to copy signing link for sending to recipient' : undefined}
|
title={recipient.token && 'Click to copy signing link for sending to recipient'}
|
||||||
onClick={onRecipientClick}
|
onClick={onRecipientClick}
|
||||||
>
|
>
|
||||||
<StackAvatar
|
<StackAvatar
|
||||||
@@ -53,10 +49,10 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
type={getRecipientType(recipient)}
|
type={getRecipientType(recipient)}
|
||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
<div
|
<div
|
||||||
className="text-muted-foreground text-sm"
|
className="text-muted-foreground text-sm"
|
||||||
title={signingToken ? 'Click to copy signing link for sending to recipient' : undefined}
|
title="Click to copy signing link for sending to recipient"
|
||||||
>
|
>
|
||||||
<p>{recipient.email} </p>
|
<p>{recipient.email} </p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
@@ -64,5 +60,6 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { DocumentStatus, Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
|
|
||||||
import { AvatarWithRecipient } from './avatar-with-recipient';
|
import { AvatarWithRecipient } from './avatar-with-recipient';
|
||||||
import { StackAvatar } from './stack-avatar';
|
import { StackAvatar } from './stack-avatar';
|
||||||
import { StackAvatars } from './stack-avatars';
|
import { StackAvatars } from './stack-avatars';
|
||||||
|
|
||||||
export type StackAvatarsWithTooltipProps = {
|
export type StackAvatarsWithTooltipProps = {
|
||||||
documentStatus: DocumentStatus;
|
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
position?: 'top' | 'bottom';
|
position?: 'top' | 'bottom';
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StackAvatarsWithTooltip = ({
|
export const StackAvatarsWithTooltip = ({
|
||||||
documentStatus,
|
|
||||||
recipients,
|
recipients,
|
||||||
position,
|
position,
|
||||||
children,
|
children,
|
||||||
}: StackAvatarsWithTooltipProps) => {
|
}: StackAvatarsWithTooltipProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const isControlled = useRef(false);
|
||||||
|
const isMouseOverTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const waitingRecipients = recipients.filter(
|
const waitingRecipients = recipients.filter(
|
||||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||||
);
|
);
|
||||||
@@ -39,13 +44,55 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
(recipient) => getRecipientType(recipient) === 'unsigned',
|
(recipient) => getRecipientType(recipient) === 'unsigned',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onMouseEnter = () => {
|
||||||
|
if (isMouseOverTimeout.current) {
|
||||||
|
clearTimeout(isMouseOverTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isControlled.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMouseOverTimeout.current = setTimeout(() => {
|
||||||
|
setOpen((o) => (!o ? true : o));
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
if (isMouseOverTimeout.current) {
|
||||||
|
clearTimeout(isMouseOverTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isControlled.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpen((o) => (o ? false : o));
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenChange = (newOpen: boolean) => {
|
||||||
|
isControlled.current = newOpen;
|
||||||
|
|
||||||
|
setOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverHover
|
<Popover open={open} onOpenChange={onOpenChange}>
|
||||||
trigger={children || <StackAvatars recipients={recipients} />}
|
<PopoverTrigger
|
||||||
contentProps={{
|
className="flex cursor-pointer"
|
||||||
className: 'flex flex-col gap-y-5 py-2',
|
onMouseEnter={onMouseEnter}
|
||||||
side: position,
|
onMouseLeave={onMouseLeave}
|
||||||
}}
|
>
|
||||||
|
{children || <StackAvatars recipients={recipients} />}
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
side={position}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
className="flex flex-col gap-y-5 py-2"
|
||||||
>
|
>
|
||||||
{completedRecipients.length > 0 && (
|
{completedRecipients.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -73,11 +120,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Waiting</h1>
|
<h1 className="text-base font-medium">Waiting</h1>
|
||||||
{waitingRecipients.map((recipient: Recipient) => (
|
{waitingRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||||
key={recipient.id}
|
|
||||||
recipient={recipient}
|
|
||||||
documentStatus={documentStatus}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -86,11 +129,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Opened</h1>
|
<h1 className="text-base font-medium">Opened</h1>
|
||||||
{openedRecipients.map((recipient: Recipient) => (
|
{openedRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||||
key={recipient.id}
|
|
||||||
recipient={recipient}
|
|
||||||
documentStatus={documentStatus}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -99,14 +138,11 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Uncompleted</h1>
|
<h1 className="text-base font-medium">Uncompleted</h1>
|
||||||
{uncompletedRecipients.map((recipient: Recipient) => (
|
{uncompletedRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||||
key={recipient.id}
|
|
||||||
recipient={recipient}
|
|
||||||
documentStatus={documentStatus}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PopoverHover>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
|
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -69,6 +71,7 @@ export type CommandMenuProps = {
|
|||||||
|
|
||||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -90,6 +93,17 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOwner = useCallback(
|
||||||
|
(document: Document) => document.userId === session?.user.id,
|
||||||
|
[session?.user.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSigningLink = useCallback(
|
||||||
|
(recipients: Recipient[]) =>
|
||||||
|
`/sign/${recipients.find((r) => r.email === session?.user.email)?.token}`,
|
||||||
|
[session?.user.email],
|
||||||
|
);
|
||||||
|
|
||||||
const searchResults = useMemo(() => {
|
const searchResults = useMemo(() => {
|
||||||
if (!searchDocumentsData) {
|
if (!searchDocumentsData) {
|
||||||
return [];
|
return [];
|
||||||
@@ -97,10 +111,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
|
|
||||||
return searchDocumentsData.map((document) => ({
|
return searchDocumentsData.map((document) => ({
|
||||||
label: document.title,
|
label: document.title,
|
||||||
path: document.path,
|
path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient),
|
||||||
value: document.value,
|
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||||
}));
|
}));
|
||||||
}, [searchDocumentsData]);
|
}, [searchDocumentsData, isOwner, getSigningLink]);
|
||||||
|
|
||||||
const currentPage = pages[pages.length - 1];
|
const currentPage = pages[pages.length - 1];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ import { getRootHref } from '@documenso/lib/utils/params';
|
|||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { CommandMenu } from '../common/command-menu';
|
||||||
|
|
||||||
const navigationLinks = [
|
const navigationLinks = [
|
||||||
{
|
{
|
||||||
href: '/documents',
|
href: '/documents',
|
||||||
@@ -21,14 +25,13 @@ const navigationLinks = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
||||||
setIsCommandMenuOpen: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
||||||
|
|
||||||
const rootHref = getRootHref(params, { returnEmptyRootString: true });
|
const rootHref = getRootHref(params, { returnEmptyRootString: true });
|
||||||
@@ -67,10 +70,12 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CommandMenu open={open} onOpenChange={setOpen} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
||||||
onClick={() => setIsCommandMenuOpen(true)}
|
onClick={() => setOpen((open) => !open)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Search className="mr-2 h-5 w-5" />
|
<Search className="mr-2 h-5 w-5" />
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
<Logo className="h-6 w-auto" />
|
<Logo className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
<DesktopNav />
|
||||||
|
|
||||||
<div className="flex gap-x-4 md:ml-8">
|
<div className="flex gap-x-4 md:ml-8">
|
||||||
<MenuSwitcher user={user} teams={teams} />
|
<MenuSwitcher user={user} teams={teams} />
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
<Button
|
<Button
|
||||||
data-testid="menu-switcher"
|
data-testid="menu-switcher"
|
||||||
variant="none"
|
variant="none"
|
||||||
className="relative flex h-12 flex-row items-center px-0 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent md:px-2"
|
className="relative flex h-12 flex-row items-center px-2 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent"
|
||||||
>
|
>
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
|
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
|
||||||
@@ -102,13 +102,12 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
rightSideComponent={
|
rightSideComponent={
|
||||||
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
|
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
|
||||||
}
|
}
|
||||||
textSectionClassName="hidden lg:flex"
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className={cn('z-[60] ml-6 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
|
className={cn('z-[60] ml-2 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
|
||||||
align="end"
|
align="end"
|
||||||
forceMount
|
forceMount
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
<SheetContent className="flex w-full max-w-[400px] flex-col">
|
||||||
<Link href="/" onClick={handleMenuItemClick}>
|
<Link href="/" onClick={handleMenuItemClick}>
|
||||||
<Image
|
<Image
|
||||||
src={LogoImage}
|
src={LogoImage}
|
||||||
@@ -87,7 +87,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
© {new Date().getFullYear()} Documenso, Inc. <br /> All rights reserved.
|
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { P, match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
|
||||||
convertToLocalSystemFormat,
|
|
||||||
} from '@documenso/lib/constants/date-formats';
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
|
||||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
|
||||||
import type { DocumentMeta } from '@documenso/prisma/client';
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
|
||||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|
||||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
|
||||||
|
|
||||||
export type DocumentReadOnlyFieldsProps = {
|
|
||||||
fields: CompletedField[];
|
|
||||||
documentMeta?: DocumentMeta;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnlyFieldsProps) => {
|
|
||||||
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const handleHideField = (fieldId: string) => {
|
|
||||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
|
||||||
{fields.map(
|
|
||||||
(field) =>
|
|
||||||
!hiddenFieldIds[field.secondaryId] && (
|
|
||||||
<FieldRootContainer
|
|
||||||
field={field}
|
|
||||||
key={field.id}
|
|
||||||
cardClassName="border-gray-100/50 !shadow-none backdrop-blur-[1px] bg-background/90"
|
|
||||||
>
|
|
||||||
<div className="absolute -right-3 -top-3">
|
|
||||||
<PopoverHover
|
|
||||||
trigger={
|
|
||||||
<Avatar className="dark:border-border h-8 w-8 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
|
|
||||||
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
|
|
||||||
{extractInitials(field.Recipient.name || field.Recipient.email)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
contentProps={{
|
|
||||||
className: 'flex w-fit flex-col py-2.5 text-sm',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{field.Recipient.name
|
|
||||||
? `${field.Recipient.name} (${field.Recipient.email})`
|
|
||||||
: field.Recipient.email}{' '}
|
|
||||||
</span>
|
|
||||||
inserted a {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="mt-2.5 h-6 text-xs focus:outline-none focus-visible:ring-0"
|
|
||||||
onClick={() => handleHideField(field.secondaryId)}
|
|
||||||
>
|
|
||||||
Hide field
|
|
||||||
</Button>
|
|
||||||
</PopoverHover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground break-all text-sm">
|
|
||||||
{match(field)
|
|
||||||
.with({ type: FieldType.SIGNATURE }, (field) =>
|
|
||||||
field.Signature?.signatureImageAsBase64 ? (
|
|
||||||
<img
|
|
||||||
src={field.Signature.signatureImageAsBase64}
|
|
||||||
alt="Signature"
|
|
||||||
className="h-full w-full object-contain dark:invert"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
|
|
||||||
{field.Signature?.typedSignature}
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
{ type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) },
|
|
||||||
() => field.customText,
|
|
||||||
)
|
|
||||||
.with({ type: FieldType.DATE }, () =>
|
|
||||||
convertToLocalSystemFormat(
|
|
||||||
field.customText,
|
|
||||||
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
|
||||||
documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
|
|
||||||
.exhaustive()}
|
|
||||||
</div>
|
|
||||||
</FieldRootContainer>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</ElementVisible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,8 +2,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
|
|
||||||
import NextAuth from 'next-auth';
|
import NextAuth from 'next-auth';
|
||||||
|
|
||||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
||||||
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
|
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
@@ -20,29 +18,15 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
error: '/signin',
|
error: '/signin',
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
signIn: async ({ user: { id: userId } }) => {
|
signIn: async ({ user }) => {
|
||||||
const [user] = await Promise.all([
|
|
||||||
await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
await prisma.userSecurityAuditLog.create({
|
await prisma.userSecurityAuditLog.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId: user.id,
|
||||||
ipAddress,
|
ipAddress,
|
||||||
userAgent,
|
userAgent,
|
||||||
type: UserSecurityAuditLogType.SIGN_IN,
|
type: UserSecurityAuditLogType.SIGN_IN,
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create the Stripe customer and attach it to the user if it doesn't exist.
|
|
||||||
if (user.customerId === null && IS_BILLING_ENABLED()) {
|
|
||||||
await getStripeCustomerByUser(user).catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
signOut: async ({ token }) => {
|
signOut: async ({ token }) => {
|
||||||
const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id;
|
const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createTrpcContext } from '@documenso/trpc/server/context';
|
|||||||
import { appRouter } from '@documenso/trpc/server/router';
|
import { appRouter } from '@documenso/trpc/server/router';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
maxDuration: 120,
|
maxDuration: 60,
|
||||||
api: {
|
api: {
|
||||||
bodyParser: {
|
bodyParser: {
|
||||||
sizeLimit: '50mb',
|
sizeLimit: '50mb',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ volumes:
|
|||||||
1. Run the following command to start the containers:
|
1. Run the following command to start the containers:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose --env-file ./.env up -d
|
docker-compose --env-file ./.env -d up
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the PostgreSQL database and the Documenso application containers.
|
This will start the PostgreSQL database and the Documenso application containers.
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ services:
|
|||||||
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
|
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
|
||||||
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||||
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
|
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
|
||||||
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
|
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?:-/opt/documenso/cert.p12}
|
||||||
ports:
|
ports:
|
||||||
- ${PORT:-3000}:${PORT:-3000}
|
- ${PORT:-3000}:${PORT:-3000}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
193
package-lock.json
generated
193
package-lock.json
generated
@@ -22,7 +22,6 @@
|
|||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"playwright": "1.43.0",
|
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"turbo": "^1.9.3"
|
"turbo": "^1.9.3"
|
||||||
@@ -4702,26 +4701,13 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/browser-chromium": {
|
|
||||||
"version": "1.43.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.43.0.tgz",
|
|
||||||
"integrity": "sha512-F0S4KIqSqQqm9EgsdtWjaJRpgP8cD2vWZHPSB41YI00PtXUobiv/3AnYISeL7wNuTanND7giaXQ4SIjkcIq3KQ==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.43.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.43.1",
|
"version": "1.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz",
|
||||||
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
|
"integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.43.1"
|
"playwright": "1.40.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -4730,50 +4716,6 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test/node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test/node_modules/playwright": {
|
|
||||||
"version": "1.43.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
|
|
||||||
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.43.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test/node_modules/playwright-core": {
|
|
||||||
"version": "1.43.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
|
|
||||||
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "5.4.2",
|
"version": "5.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.4.2.tgz",
|
||||||
@@ -17536,7 +17478,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
|
||||||
"integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
|
"integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -17581,15 +17522,18 @@
|
|||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
},
|
},
|
||||||
"node_modules/pdfjs-dist": {
|
"node_modules/pdfjs-dist": {
|
||||||
"version": "3.11.174",
|
"version": "3.6.172",
|
||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz",
|
||||||
"integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
|
"integrity": "sha512-bfOhCg+S9DXh/ImWhWYTOiq3aVMFSCvzGiBzsIJtdMC71kVWDBw7UXr32xh0y56qc5wMVylIeqV3hBaRsu+e+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"path2d-polyfill": "^2.0.1",
|
||||||
|
"web-streams-polyfill": "^3.2.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2"
|
||||||
"path2d-polyfill": "^2.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/peberminta": {
|
"node_modules/peberminta": {
|
||||||
@@ -17671,11 +17615,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.43.0",
|
"version": "1.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz",
|
||||||
"integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==",
|
"integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.43.0"
|
"playwright-core": "1.40.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -17688,9 +17633,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.43.0",
|
"version": "1.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz",
|
||||||
"integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==",
|
"integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
@@ -17702,6 +17648,7 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -19009,6 +18956,42 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-pdf": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"make-cancellable-promise": "^1.3.1",
|
||||||
|
"make-event-props": "^1.6.0",
|
||||||
|
"merge-refs": "^1.2.1",
|
||||||
|
"pdfjs-dist": "3.6.172",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"tiny-invariant": "^1.0.0",
|
||||||
|
"tiny-warning": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-pdf/node_modules/clsx": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-property": {
|
"node_modules/react-property": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
|
||||||
@@ -21319,6 +21302,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||||
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
|
||||||
@@ -22943,14 +22931,6 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/warning": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
@@ -24946,7 +24926,6 @@
|
|||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"oslo": "^0.17.0",
|
"oslo": "^0.17.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"playwright": "1.43.0",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"remeda": "^1.27.1",
|
"remeda": "^1.27.1",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
@@ -24954,7 +24933,6 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/browser-chromium": "1.43.0",
|
|
||||||
"@types/luxon": "^3.3.1"
|
"@types/luxon": "^3.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25355,13 +25333,11 @@
|
|||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
"pdfjs-dist": "3.11.174",
|
"pdfjs-dist": "3.6.172",
|
||||||
"react": "18.2.0",
|
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-day-picker": "^8.7.1",
|
"react-day-picker": "^8.7.1",
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.45.4",
|
"react-hook-form": "^7.45.4",
|
||||||
"react-pdf": "7.7.3",
|
"react-pdf": "7.3.3",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
@@ -25378,43 +25354,6 @@
|
|||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui/node_modules/react-pdf": {
|
|
||||||
"version": "7.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.3.tgz",
|
|
||||||
"integrity": "sha512-a2VfDl8hiGjugpqezBTUzJHYLNB7IS7a2t7GD52xMI9xHg8LdVaTMsnM9ZlNmKadnStT/tvX5IfV0yLn+JvYmw==",
|
|
||||||
"dependencies": {
|
|
||||||
"clsx": "^2.0.0",
|
|
||||||
"dequal": "^2.0.3",
|
|
||||||
"make-cancellable-promise": "^1.3.1",
|
|
||||||
"make-event-props": "^1.6.0",
|
|
||||||
"merge-refs": "^1.2.1",
|
|
||||||
"pdfjs-dist": "3.11.174",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"tiny-invariant": "^1.0.0",
|
|
||||||
"warning": "^4.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/ui/node_modules/react-pdf/node_modules/clsx": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/ui/node_modules/typescript": {
|
"packages/ui/node_modules/typescript": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"playwright": "1.43.0",
|
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"turbo": "^1.9.3"
|
"turbo": "^1.9.3"
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import {
|
|||||||
ZDeleteDocumentMutationSchema,
|
ZDeleteDocumentMutationSchema,
|
||||||
ZDeleteFieldMutationSchema,
|
ZDeleteFieldMutationSchema,
|
||||||
ZDeleteRecipientMutationSchema,
|
ZDeleteRecipientMutationSchema,
|
||||||
ZDownloadDocumentSuccessfulSchema,
|
|
||||||
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
|
||||||
ZGenerateDocumentFromTemplateMutationSchema,
|
|
||||||
ZGetDocumentsQuerySchema,
|
ZGetDocumentsQuerySchema,
|
||||||
ZSendDocumentForSigningMutationSchema,
|
ZSendDocumentForSigningMutationSchema,
|
||||||
ZSuccessfulDocumentResponseSchema,
|
ZSuccessfulDocumentResponseSchema,
|
||||||
@@ -54,17 +51,6 @@ export const ApiContractV1 = c.router(
|
|||||||
summary: 'Get a single document',
|
summary: 'Get a single document',
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadSignedDocument: {
|
|
||||||
method: 'GET',
|
|
||||||
path: '/api/v1/documents/:id/download',
|
|
||||||
responses: {
|
|
||||||
200: ZDownloadDocumentSuccessfulSchema,
|
|
||||||
401: ZUnsuccessfulResponseSchema,
|
|
||||||
404: ZUnsuccessfulResponseSchema,
|
|
||||||
},
|
|
||||||
summary: 'Download a signed document when the storage transport is S3',
|
|
||||||
},
|
|
||||||
|
|
||||||
createDocument: {
|
createDocument: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/v1/documents',
|
path: '/api/v1/documents',
|
||||||
@@ -87,24 +73,6 @@ export const ApiContractV1 = c.router(
|
|||||||
404: ZUnsuccessfulResponseSchema,
|
404: ZUnsuccessfulResponseSchema,
|
||||||
},
|
},
|
||||||
summary: 'Create a new document from an existing template',
|
summary: 'Create a new document from an existing template',
|
||||||
deprecated: true,
|
|
||||||
description: `This has been deprecated in favour of "/api/v1/templates/:templateId/generate-document". You may face unpredictable behavior using this endpoint as it is no longer maintained.`,
|
|
||||||
},
|
|
||||||
|
|
||||||
generateDocumentFromTemplate: {
|
|
||||||
method: 'POST',
|
|
||||||
path: '/api/v1/templates/:templateId/generate-document',
|
|
||||||
body: ZGenerateDocumentFromTemplateMutationSchema,
|
|
||||||
responses: {
|
|
||||||
200: ZGenerateDocumentFromTemplateMutationResponseSchema,
|
|
||||||
400: ZUnsuccessfulResponseSchema,
|
|
||||||
401: ZUnsuccessfulResponseSchema,
|
|
||||||
404: ZUnsuccessfulResponseSchema,
|
|
||||||
500: ZUnsuccessfulResponseSchema,
|
|
||||||
},
|
|
||||||
summary: 'Create a new document from an existing template',
|
|
||||||
description:
|
|
||||||
'Create a new document from an existing template. Passing in values for title and meta will override the original values defined in the template. If you do not pass in values for recipients, it will use the values defined in the template.',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sendDocument: {
|
sendDocument: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createNextRoute } from '@ts-rest/next';
|
import { createNextRoute } from '@ts-rest/next';
|
||||||
|
|
||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
|
||||||
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 { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||||
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
||||||
@@ -20,16 +19,11 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip
|
|||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||||
import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
|
|
||||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||||
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
|
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import {
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
getPresignGetUrl,
|
|
||||||
getPresignPostUrl,
|
|
||||||
} from '@documenso/lib/universal/upload/server-actions';
|
|
||||||
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { ApiContractV1 } from './contract';
|
import { ApiContractV1 } from './contract';
|
||||||
@@ -89,68 +83,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
|
|
||||||
const { id: documentId } = args.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: {
|
|
||||||
message: 'Please make sure the storage transport is set to S3.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = await getDocumentById({
|
|
||||||
id: Number(documentId),
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document || !document.documentDataId) {
|
|
||||||
return {
|
|
||||||
status: 404,
|
|
||||||
body: {
|
|
||||||
message: 'Document not found',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DocumentDataType.S3_PATH !== document.documentData.type) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
message: 'Invalid document data type',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.status !== DocumentStatus.COMPLETED) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
message: 'Document is not completed yet.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url } = await getPresignGetUrl(document.documentData.data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: { downloadUrl: url },
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: {
|
|
||||||
message: 'Error downloading the document. Please try again.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
|
|
||||||
@@ -232,13 +164,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
await upsertDocumentMeta({
|
|
||||||
documentId: document.id,
|
|
||||||
userId: user.id,
|
|
||||||
...body.meta,
|
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
|
||||||
});
|
|
||||||
|
|
||||||
const recipients = await setRecipientsForDocument({
|
const recipients = await setRecipientsForDocument({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
@@ -289,7 +214,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
|
|
||||||
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
||||||
|
|
||||||
const document = await createDocumentFromTemplateLegacy({
|
const document = await createDocumentFromTemplate({
|
||||||
templateId,
|
templateId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
@@ -306,7 +231,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
formValues: body.formValues,
|
formValues: body.formValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newDocumentData = await putPdfFile({
|
const newDocumentData = await putFile({
|
||||||
name: fileName,
|
name: fileName,
|
||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||||
@@ -334,7 +259,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
...body.meta,
|
subject: body.meta.subject,
|
||||||
|
message: body.meta.message,
|
||||||
|
dateFormat: body.meta.dateFormat,
|
||||||
|
timezone: body.meta.timezone,
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -354,85 +282,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
|
||||||
const { body, params } = args;
|
|
||||||
|
|
||||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
|
||||||
|
|
||||||
if (remaining.documents <= 0) {
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
body: {
|
|
||||||
message: 'You have reached the maximum number of documents allowed for this month',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateId = Number(params.templateId);
|
|
||||||
|
|
||||||
let document: CreateDocumentFromTemplateResponse | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
document = await createDocumentFromTemplate({
|
|
||||||
templateId,
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
|
||||||
recipients: body.recipients,
|
|
||||||
override: {
|
|
||||||
title: body.title,
|
|
||||||
...body.meta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return AppError.toRestAPIError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.formValues) {
|
|
||||||
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
|
|
||||||
|
|
||||||
const pdf = await getFile(document.documentData);
|
|
||||||
|
|
||||||
const prefilled = await insertFormValuesInPdf({
|
|
||||||
pdf: Buffer.from(pdf),
|
|
||||||
formValues: body.formValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newDocumentData = await putPdfFile({
|
|
||||||
name: fileName,
|
|
||||||
type: 'application/pdf',
|
|
||||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team?.id,
|
|
||||||
data: {
|
|
||||||
formValues: body.formValues,
|
|
||||||
documentData: {
|
|
||||||
connect: {
|
|
||||||
id: newDocumentData.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: {
|
|
||||||
documentId: document.id,
|
|
||||||
recipients: document.Recipient.map((recipient) => ({
|
|
||||||
recipientId: recipient.id,
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
token: recipient.token,
|
|
||||||
role: recipient.role,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id } = args.params;
|
const { id } = args.params;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { generateOpenApi } from '@ts-rest/open-api';
|
|||||||
|
|
||||||
import { ApiContractV1 } from './contract';
|
import { ApiContractV1 } from './contract';
|
||||||
|
|
||||||
export const OpenAPIV1 = Object.assign(
|
export const OpenAPIV1 = generateOpenApi(
|
||||||
generateOpenApi(
|
|
||||||
ApiContractV1,
|
ApiContractV1,
|
||||||
{
|
{
|
||||||
info: {
|
info: {
|
||||||
@@ -15,21 +14,4 @@ export const OpenAPIV1 = Object.assign(
|
|||||||
{
|
{
|
||||||
setOperationId: true,
|
setOperationId: true,
|
||||||
},
|
},
|
||||||
),
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
securitySchemes: {
|
|
||||||
authorization: {
|
|
||||||
type: 'apiKey',
|
|
||||||
in: 'header',
|
|
||||||
name: 'Authorization',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
authorization: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
ReadStatus,
|
ReadStatus,
|
||||||
@@ -54,10 +53,6 @@ export const ZUploadDocumentSuccessfulSchema = z.object({
|
|||||||
key: z.string(),
|
key: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDownloadDocumentSuccessfulSchema = z.object({
|
|
||||||
downloadUrl: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSuccessfulSchema>;
|
export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSuccessfulSchema>;
|
||||||
|
|
||||||
export const ZCreateDocumentMutationSchema = z.object({
|
export const ZCreateDocumentMutationSchema = z.object({
|
||||||
@@ -142,59 +137,6 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
|
|||||||
typeof ZCreateDocumentFromTemplateMutationResponseSchema
|
typeof ZCreateDocumentFromTemplateMutationResponseSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|
||||||
title: z.string().optional(),
|
|
||||||
recipients: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
email: z.string().email().min(1),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(schema) => {
|
|
||||||
const emails = schema.map((signer) => signer.email.toLowerCase());
|
|
||||||
const ids = schema.map((signer) => signer.id);
|
|
||||||
|
|
||||||
return new Set(emails).size === emails.length && new Set(ids).size === ids.length;
|
|
||||||
},
|
|
||||||
{ message: 'Recipient IDs and emails must be unique' },
|
|
||||||
),
|
|
||||||
meta: z
|
|
||||||
.object({
|
|
||||||
subject: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
timezone: z.string(),
|
|
||||||
dateFormat: z.string(),
|
|
||||||
redirectUrl: ZUrlSchema,
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.optional(),
|
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TGenerateDocumentFromTemplateMutationSchema = z.infer<
|
|
||||||
typeof ZGenerateDocumentFromTemplateMutationSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
|
|
||||||
documentId: z.number(),
|
|
||||||
recipients: z.array(
|
|
||||||
z.object({
|
|
||||||
recipientId: z.number(),
|
|
||||||
name: z.string(),
|
|
||||||
email: z.string().email().min(1),
|
|
||||||
token: z.string(),
|
|
||||||
role: z.nativeEnum(RecipientRole),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TGenerateDocumentFromTemplateMutationResponseSchema = z.infer<
|
|
||||||
typeof ZGenerateDocumentFromTemplateMutationResponseSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ZCreateRecipientMutationSchema = z.object({
|
export const ZCreateRecipientMutationSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByLabel('Require account').getByText('Require account').click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@@ -52,7 +52,11 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
// Todo: Verify that the values are correct once we fix the issue where going back
|
||||||
|
// does not show the updated values.
|
||||||
|
// await expect(page.getByLabel('Title')).toContainText('New Title');
|
||||||
|
// await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
// await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
await unseedUser(user.id);
|
await unseedUser(user.id);
|
||||||
});
|
});
|
||||||
@@ -85,8 +89,8 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByLabel('Require account').getByText('Require account').click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@@ -164,8 +168,11 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
// Todo: Verify that the values are correct once we fix the issue where going back
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
// does not show the updated values.
|
||||||
|
// await expect(page.getByLabel('Title')).toContainText('New Title');
|
||||||
|
// await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
// await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
await unseedUser(user.id);
|
await unseedUser(user.id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'Email', exact: true })
|
.getByRole('textbox', { name: 'Email', exact: true })
|
||||||
.fill('recipient2@documenso.com');
|
.fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2');
|
||||||
|
|
||||||
// Display advanced settings.
|
// Display advanced settings.
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').click();
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
// Navigate to the next step and back.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@@ -62,6 +62,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Note: Not complete yet due to issue with back button.
|
||||||
test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
const document = await seedBlankDocument(user);
|
const document = await seedBlankDocument(user);
|
||||||
@@ -81,7 +82,7 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2');
|
||||||
|
|
||||||
// Advanced settings should not be visible for non EE users.
|
// Advanced settings should not be visible for non EE users.
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
||||||
@@ -92,5 +93,26 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Todo: Fix stepper component back issue before finishing test.
|
||||||
|
|
||||||
|
// // Expect that the advanced settings is unchecked, since no advanced settings were applied.
|
||||||
|
// await expect(page.getByLabel('Show advanced settings')).toBeChecked({ checked: false });
|
||||||
|
|
||||||
|
// // Add advanced settings for a single recipient.
|
||||||
|
// await page.getByLabel('Show advanced settings').click();
|
||||||
|
// await page.getByRole('combobox').first().click();
|
||||||
|
// await page.getByLabel('Require account').click();
|
||||||
|
|
||||||
|
// // Navigate to the next step and back.
|
||||||
|
// await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
// await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
// await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
|
// await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Expect that the advanced settings is visible, and the checkbox is hidden. Since advanced
|
||||||
|
// settings were applied.
|
||||||
|
|
||||||
|
// Todo: Fix stepper component back issue before finishing test.
|
||||||
|
|
||||||
await unseedUser(user.id);
|
await unseedUser(user.id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
|||||||
await page.getByPlaceholder('Name').fill('User 1');
|
await page.getByPlaceholder('Name').fill('User 1');
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com');
|
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2');
|
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('User 2');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', asyn
|
|||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
await page.waitForURL(`/sign/${token}/complete`);
|
await page.waitForURL(`/sign/${token}/complete`);
|
||||||
await expect(page.getByText('Document Signed')).toBeVisible();
|
await expect(page.getByText('You have signed')).toBeVisible();
|
||||||
|
|
||||||
// Check if document has been signed
|
// Check if document has been signed
|
||||||
const { status: completedStatus } = await getDocumentByToken(token);
|
const { status: completedStatus } = await getDocumentByToken(token);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'serial' });
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ test('[DOCUMENTS]: deleting a completed document should not remove it from recip
|
|||||||
email: sender.email,
|
email: sender.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// open actions menu
|
||||||
await page
|
await page
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.getByRole('cell', { name: 'Download' })
|
.getByRole('cell', { name: 'Download' })
|
||||||
@@ -116,7 +115,7 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients'
|
|||||||
email: sender.email,
|
email: sender.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// open actions menu
|
||||||
await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click();
|
await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click();
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
@@ -136,11 +135,20 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients'
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible();
|
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.goto(`/sign/${recipient.token}`);
|
||||||
|
await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/documents');
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
await apiSignout({ page });
|
await apiSignout({ page });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[DOCUMENTS]: deleting draft documents should permanently remove it', async ({ page }) => {
|
test('[DOCUMENTS]: deleting a draft document should remove it without additional prompting', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const { sender } = await seedDeleteDocumentsTestRequirements();
|
const { sender } = await seedDeleteDocumentsTestRequirements();
|
||||||
|
|
||||||
await apiSignin({
|
await apiSignin({
|
||||||
@@ -148,10 +156,11 @@ test('[DOCUMENTS]: deleting draft documents should permanently remove it', async
|
|||||||
email: sender.email,
|
email: sender.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// open actions menu
|
||||||
await page
|
await page
|
||||||
.locator('tr', { hasText: 'Document 1 - Draft' })
|
.locator('tr', { hasText: 'Document 1 - Draft' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByRole('cell', { name: 'Edit' })
|
||||||
|
.getByRole('button')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
@@ -160,155 +169,4 @@ test('[DOCUMENTS]: deleting draft documents should permanently remove it', async
|
|||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 0);
|
|
||||||
await checkDocumentTabCount(page, 'All', 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[DOCUMENTS]: deleting pending documents should permanently remove it', async ({ page }) => {
|
|
||||||
const { sender } = await seedDeleteDocumentsTestRequirements();
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: sender.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open document action menu.
|
|
||||||
await page
|
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
|
||||||
.getByTestId('document-table-action-btn')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Delete document.
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible();
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 1);
|
|
||||||
await checkDocumentTabCount(page, 'All', 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[DOCUMENTS]: deleting completed documents as an owner should hide it from only the owner', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { sender, recipients } = await seedDeleteDocumentsTestRequirements();
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: sender.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open document action menu.
|
|
||||||
await page
|
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
|
||||||
.getByTestId('document-table-action-btn')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Delete document.
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible();
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 1);
|
|
||||||
await checkDocumentTabCount(page, 'All', 2);
|
|
||||||
|
|
||||||
// Sign into the recipient account.
|
|
||||||
await apiSignout({ page });
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: recipients[0].email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).toBeVisible();
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 0);
|
|
||||||
await checkDocumentTabCount(page, 'All', 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[DOCUMENTS]: deleting documents as a recipient should only hide it for them', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { sender, recipients } = await seedDeleteDocumentsTestRequirements();
|
|
||||||
const recipientA = recipients[0];
|
|
||||||
const recipientB = recipients[1];
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: recipientA.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open document action menu.
|
|
||||||
await page
|
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
|
||||||
.getByTestId('document-table-action-btn')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Delete document.
|
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Hide' }).click();
|
|
||||||
|
|
||||||
// Open document action menu.
|
|
||||||
await page
|
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
|
||||||
.getByTestId('document-table-action-btn')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Delete document.
|
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Hide' }).click();
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible();
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible();
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 0);
|
|
||||||
await checkDocumentTabCount(page, 'All', 0);
|
|
||||||
|
|
||||||
// Sign into the sender account.
|
|
||||||
await apiSignout({ page });
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: sender.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts for sender.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 1);
|
|
||||||
await checkDocumentTabCount(page, 'All', 3);
|
|
||||||
|
|
||||||
// Sign into the other recipient account.
|
|
||||||
await apiSignout({ page });
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: recipientB.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts for other recipient.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 0);
|
|
||||||
await checkDocumentTabCount(page, 'All', 2);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { Page } from '@playwright/test';
|
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
|
|
||||||
export const checkDocumentTabCount = async (page: Page, tabName: string, count: number) => {
|
|
||||||
await page.getByRole('tab', { name: tabName }).click();
|
|
||||||
|
|
||||||
if (tabName !== 'All') {
|
|
||||||
await expect(page.getByRole('tab', { name: tabName })).toContainText(count.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
await expect(page.getByTestId('empty-document-state')).toBeVisible();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(page.getByRole('main')).toContainText(`Showing ${count}`);
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
@@ -6,10 +7,24 @@ import { seedTeamEmail, unseedTeam, unseedTeamEmail } from '@documenso/prisma/se
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
const checkDocumentTabCount = async (page: Page, tabName: string, count: number) => {
|
||||||
|
await page.getByRole('tab', { name: tabName }).click();
|
||||||
|
|
||||||
|
if (tabName !== 'All') {
|
||||||
|
await expect(page.getByRole('tab', { name: tabName })).toContainText(count.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
await expect(page.getByRole('main')).toContainText(`Nothing to do`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(page.getByRole('main')).toContainText(`Showing ${count}`);
|
||||||
|
};
|
||||||
|
|
||||||
test('[TEAMS]: check team documents count', async ({ page }) => {
|
test('[TEAMS]: check team documents count', async ({ page }) => {
|
||||||
const { team, teamMember2 } = await seedTeamDocuments();
|
const { team, teamMember2 } = await seedTeamDocuments();
|
||||||
|
|
||||||
@@ -230,6 +245,24 @@ test('[TEAMS]: check team documents count with external team email', async ({ pa
|
|||||||
await unseedTeam(team.url);
|
await unseedTeam(team.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: delete pending team document', async ({ page }) => {
|
||||||
|
const { team, teamMember2: currentUser } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: currentUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('row').getByRole('button').nth(1).click();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'Pending', 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('[TEAMS]: resend pending team document', async ({ page }) => {
|
test('[TEAMS]: resend pending team document', async ({ page }) => {
|
||||||
const { team, teamMember2: currentUser } = await seedTeamDocuments();
|
const { team, teamMember2: currentUser } = await seedTeamDocuments();
|
||||||
|
|
||||||
@@ -247,125 +280,3 @@ test('[TEAMS]: resend pending team document', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.getByRole('status')).toContainText('Document re-sent');
|
await expect(page.getByRole('status')).toContainText('Document re-sent');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
|
||||||
const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments();
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: teamMember3.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 1);
|
|
||||||
|
|
||||||
// Should be hidden for all team members.
|
|
||||||
await apiSignout({ page });
|
|
||||||
|
|
||||||
// Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same.
|
|
||||||
for (const user of [team.owner, teamEmailMember]) {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 2);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 1);
|
|
||||||
await checkDocumentTabCount(page, 'All', 4);
|
|
||||||
|
|
||||||
await apiSignout({ page });
|
|
||||||
}
|
|
||||||
|
|
||||||
await unseedTeam(team.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEAMS]: delete pending team document', async ({ page }) => {
|
|
||||||
const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments();
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: teamMember3.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 1);
|
|
||||||
|
|
||||||
// Should be hidden for all team members.
|
|
||||||
await apiSignout({ page });
|
|
||||||
|
|
||||||
// Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same.
|
|
||||||
for (const user of [team.owner, teamEmailMember]) {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 1);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 2);
|
|
||||||
await checkDocumentTabCount(page, 'All', 4);
|
|
||||||
|
|
||||||
await apiSignout({ page });
|
|
||||||
}
|
|
||||||
|
|
||||||
await unseedTeam(team.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEAMS]: delete completed team document', async ({ page }) => {
|
|
||||||
const { team, teamMember2: teamEmailMember, teamMember3 } = await seedTeamDocuments();
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: teamMember3.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(2).click();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 0);
|
|
||||||
|
|
||||||
// Should be hidden for all team members.
|
|
||||||
await apiSignout({ page });
|
|
||||||
|
|
||||||
// Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same.
|
|
||||||
for (const user of [team.owner, teamEmailMember]) {
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/t/${team.url}/documents`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check document counts.
|
|
||||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Pending', 2);
|
|
||||||
await checkDocumentTabCount(page, 'Completed', 0);
|
|
||||||
await checkDocumentTabCount(page, 'Draft', 2);
|
|
||||||
await checkDocumentTabCount(page, 'All', 4);
|
|
||||||
|
|
||||||
await apiSignout({ page });
|
|
||||||
}
|
|
||||||
|
|
||||||
await unseedTeam(team.url);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
|
||||||
import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams';
|
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
|
||||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
|
||||||
|
|
||||||
test.describe('[EE_ONLY]', () => {
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
|
||||||
|
|
||||||
test.beforeEach(() => {
|
|
||||||
test.skip(
|
|
||||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId,
|
|
||||||
'Billing required for this test',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] add action auth settings', async ({ page }) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: user.id,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set EE action auth.
|
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Return to the settings step to check that the results are saved correctly.
|
|
||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
|
||||||
|
|
||||||
await unseedUser(user.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] enterprise team member can add action auth settings', async ({ page }) => {
|
|
||||||
const team = await seedTeam({
|
|
||||||
createTeamMembers: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const owner = team.owner;
|
|
||||||
const teamMemberUser = team.members[1].user;
|
|
||||||
|
|
||||||
// Make the team enterprise by giving the owner the enterprise subscription.
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: team.ownerUserId,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: teamMemberUser.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set EE action auth.
|
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Advanced settings should be visible.
|
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeVisible();
|
|
||||||
|
|
||||||
await unseedTeam(team.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] enterprise team member should not have access to enterprise on personal account', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const team = await seedTeam({
|
|
||||||
createTeamMembers: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamMemberUser = team.members[1].user;
|
|
||||||
|
|
||||||
// Make the team enterprise by giving the owner the enterprise subscription.
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: team.ownerUserId,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(teamMemberUser);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: teamMemberUser.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Global action auth should not be visible.
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
|
||||||
|
|
||||||
// Next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Advanced settings should not be visible.
|
|
||||||
await expect(page.getByLabel('Show advanced settings')).not.toBeVisible();
|
|
||||||
|
|
||||||
await unseedTeam(team.url);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set title.
|
|
||||||
await page.getByLabel('Title').fill('New Title');
|
|
||||||
|
|
||||||
// Set access auth.
|
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
|
||||||
|
|
||||||
// Action auth should NOT be visible.
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
|
||||||
|
|
||||||
// Return to the settings step to check that the results are saved correctly.
|
|
||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByLabel('Title')).toHaveValue('New Title');
|
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
|
||||||
|
|
||||||
await unseedUser(user.id);
|
|
||||||
});
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
|
||||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
|
||||||
|
|
||||||
test.describe('[EE_ONLY]', () => {
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
|
||||||
|
|
||||||
test.beforeEach(() => {
|
|
||||||
test.skip(
|
|
||||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId,
|
|
||||||
'Billing required for this test',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW] add EE settings', async ({ page }) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: user.id,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add 2 signers.
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
|
||||||
await page
|
|
||||||
.getByRole('textbox', { name: 'Email', exact: true })
|
|
||||||
.fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Display advanced settings.
|
|
||||||
await page.getByLabel('Show advanced settings').check();
|
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Expect that the advanced settings is unchecked, since no advanced settings were applied.
|
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeChecked({ checked: false });
|
|
||||||
|
|
||||||
// Add advanced settings for a single recipient.
|
|
||||||
await page.getByLabel('Show advanced settings').check();
|
|
||||||
await page.getByRole('combobox').first().click();
|
|
||||||
await page.getByLabel('Require passkey').click();
|
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Expect that the advanced settings is visible, and the checkbox is hidden. Since advanced
|
|
||||||
// settings were applied.
|
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
|
||||||
|
|
||||||
await unseedUser(user.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add 2 signers.
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Advanced settings should not be visible for non EE users.
|
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
|
||||||
|
|
||||||
await unseedUser(user.id);
|
|
||||||
});
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
|
||||||
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Create a template with all settings filled out
|
|
||||||
* 2. Create a document from the template
|
|
||||||
* 3. Ensure all values are correct
|
|
||||||
*
|
|
||||||
* Note: There is a direct copy paste of this test below for teams.
|
|
||||||
*
|
|
||||||
* If you update this test please update that test as well.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a document from a template', async ({ page }) => {
|
|
||||||
const user = await seedUser();
|
|
||||||
const template = await seedBlankTemplate(user);
|
|
||||||
|
|
||||||
const isBillingEnabled =
|
|
||||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId;
|
|
||||||
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: user.id,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: user.email,
|
|
||||||
redirectPath: `/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title.
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_TITLE');
|
|
||||||
|
|
||||||
// Set template document access.
|
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
|
||||||
|
|
||||||
// Set EE action auth.
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set email options.
|
|
||||||
await page.getByRole('button', { name: 'Email Options' }).click();
|
|
||||||
await page.getByLabel('Subject (Optional)').fill('SUBJECT');
|
|
||||||
await page.getByLabel('Message (Optional)').fill('MESSAGE');
|
|
||||||
|
|
||||||
// Set advanced options.
|
|
||||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
|
||||||
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click();
|
|
||||||
await page.getByLabel('DD/MM/YYYY').click();
|
|
||||||
|
|
||||||
await page.locator('.time-zone-field').click();
|
|
||||||
await page.getByRole('option', { name: 'Etc/UTC' }).click();
|
|
||||||
await page.getByLabel('Redirect URL').fill('https://documenso.com');
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add 2 signers.
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Apply require passkey for Recipient 1.
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
await page.getByLabel('Show advanced settings').check();
|
|
||||||
await page.getByRole('combobox').first().click();
|
|
||||||
await page.getByLabel('Require passkey').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template
|
|
||||||
await page.waitForURL('/templates');
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the correct values.
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
documentMeta: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
|
||||||
);
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
|
||||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
|
||||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
|
||||||
|
|
||||||
const recipientOne = document.Recipient[0];
|
|
||||||
const recipientTwo = document.Recipient[1];
|
|
||||||
|
|
||||||
const recipientOneAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
recipientAuth: recipientOne.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const recipientTwoAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
recipientAuth: recipientTwo.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a direct copy paste of the above test but for teams.
|
|
||||||
*/
|
|
||||||
test('[TEMPLATE]: should create a team document from a team template', async ({ page }) => {
|
|
||||||
const { owner, ...team } = await seedTeam({
|
|
||||||
createTeamMembers: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = await seedBlankTemplate(owner, {
|
|
||||||
createTemplateOptions: {
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isBillingEnabled =
|
|
||||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId;
|
|
||||||
|
|
||||||
await seedUserSubscription({
|
|
||||||
userId: owner.id,
|
|
||||||
priceId: enterprisePriceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await apiSignin({
|
|
||||||
page,
|
|
||||||
email: owner.email,
|
|
||||||
redirectPath: `/t/${team.url}/templates/${template.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set template title.
|
|
||||||
await page.getByLabel('Title').fill('TEMPLATE_TITLE');
|
|
||||||
|
|
||||||
// Set template document access.
|
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
|
||||||
|
|
||||||
// Set EE action auth.
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set email options.
|
|
||||||
await page.getByRole('button', { name: 'Email Options' }).click();
|
|
||||||
await page.getByLabel('Subject (Optional)').fill('SUBJECT');
|
|
||||||
await page.getByLabel('Message (Optional)').fill('MESSAGE');
|
|
||||||
|
|
||||||
// Set advanced options.
|
|
||||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
|
||||||
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click();
|
|
||||||
await page.getByLabel('DD/MM/YYYY').click();
|
|
||||||
|
|
||||||
await page.locator('.time-zone-field').click();
|
|
||||||
await page.getByRole('option', { name: 'Etc/UTC' }).click();
|
|
||||||
await page.getByLabel('Redirect URL').fill('https://documenso.com');
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
|
||||||
|
|
||||||
// Add 2 signers.
|
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Apply require passkey for Recipient 1.
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
await page.getByLabel('Show advanced settings').check();
|
|
||||||
await page.getByRole('combobox').first().click();
|
|
||||||
await page.getByLabel('Require passkey').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save template' }).click();
|
|
||||||
|
|
||||||
// Use template
|
|
||||||
await page.waitForURL(`/t/${team.url}/templates`);
|
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
|
|
||||||
// Review that the document was created with the correct values.
|
|
||||||
await page.waitForURL(/documents/);
|
|
||||||
|
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
documentMeta: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.teamId).toEqual(team.id);
|
|
||||||
|
|
||||||
const documentAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
|
||||||
);
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
|
||||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
|
||||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
|
||||||
|
|
||||||
const recipientOne = document.Recipient[0];
|
|
||||||
const recipientTwo = document.Recipient[1];
|
|
||||||
|
|
||||||
const recipientOneAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
recipientAuth: recipientOne.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const recipientTwoAuth = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
recipientAuth: recipientTwo.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
|
||||||
});
|
|
||||||
@@ -189,14 +189,7 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
|||||||
|
|
||||||
// Use personal template.
|
// Use personal template.
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Create Document' }).click();
|
||||||
// Enter template values.
|
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').click();
|
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
|
|
||||||
await page.getByPlaceholder('Recipient 1').click();
|
|
||||||
await page.getByPlaceholder('Recipient 1').fill('name');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/documents/);
|
||||||
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
||||||
await page.waitForURL('/documents');
|
await page.waitForURL('/documents');
|
||||||
@@ -207,14 +200,7 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
|||||||
|
|
||||||
// Use team template.
|
// Use team template.
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Create Document' }).click();
|
||||||
// Enter template values.
|
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').click();
|
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
|
|
||||||
await page.getByPlaceholder('Recipient 1').click();
|
|
||||||
await page.getByPlaceholder('Recipient 1').fill('name');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
|
||||||
await page.waitForURL(/\/t\/.+\/documents/);
|
await page.waitForURL(/\/t\/.+\/documents/);
|
||||||
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
||||||
await page.waitForURL(`/t/${team.url}/documents`);
|
await page.waitForURL(`/t/${team.url}/documents`);
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@ import { sealDocument } from '@documenso/lib/server-only/document/seal-document'
|
|||||||
import { redis } from '@documenso/lib/server-only/redis';
|
import { redis } from '@documenso/lib/server-only/redis';
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { alphaid, nanoid } from '@documenso/lib/universal/id';
|
import { alphaid, nanoid } from '@documenso/lib/universal/id';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
@@ -74,7 +74,7 @@ export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersChecko
|
|||||||
new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url),
|
new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url),
|
||||||
).then(async (res) => res.arrayBuffer());
|
).then(async (res) => res.arrayBuffer());
|
||||||
|
|
||||||
const { id: documentDataId } = await putPdfFile({
|
const { id: documentDataId } = await putFile({
|
||||||
name: 'Documenso Supporter Pledge.pdf',
|
name: 'Documenso Supporter Pledge.pdf',
|
||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
arrayBuffer: async () => Promise.resolve(documentBuffer),
|
arrayBuffer: async () => Promise.resolve(documentBuffer),
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ export const TemplateDocumentCancel = ({
|
|||||||
<br />"{documentName}"
|
<br />"{documentName}"
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
|
||||||
All signatures have been voided.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
You don't need to sign it anymore.
|
You don't need to sign it anymore.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export interface TemplateDocumentInviteProps {
|
|||||||
signDocumentLink: string;
|
signDocumentLink: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
selfSigner: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateDocumentInvite = ({
|
export const TemplateDocumentInvite = ({
|
||||||
@@ -20,7 +19,6 @@ export const TemplateDocumentInvite = ({
|
|||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
role,
|
role,
|
||||||
selfSigner,
|
|
||||||
}: TemplateDocumentInviteProps) => {
|
}: TemplateDocumentInviteProps) => {
|
||||||
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
|
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
|
||||||
|
|
||||||
@@ -30,19 +28,8 @@ export const TemplateDocumentInvite = ({
|
|||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
{selfSigner ? (
|
{inviterName} has invited you to {actionVerb.toLowerCase()}
|
||||||
<>
|
<br />"{documentName}"
|
||||||
{`Please ${actionVerb.toLowerCase()} your document`}
|
|
||||||
<br />
|
|
||||||
{`"${documentName}"`}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{`${inviterName} has invited you to ${actionVerb.toLowerCase()}`}
|
|
||||||
<br />
|
|
||||||
{`"${documentName}"`}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { TemplateFooter } from '../template-components/template-footer';
|
|||||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
customBody?: string;
|
customBody?: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
selfSigner?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentInviteEmailTemplate = ({
|
export const DocumentInviteEmailTemplate = ({
|
||||||
@@ -33,13 +32,10 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
customBody,
|
customBody,
|
||||||
role,
|
role,
|
||||||
selfSigner = false,
|
|
||||||
}: DocumentInviteEmailTemplateProps) => {
|
}: DocumentInviteEmailTemplateProps) => {
|
||||||
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
|
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
|
||||||
|
|
||||||
const previewText = selfSigner
|
const previewText = `${inviterName} has invited you to ${action} ${documentName}`;
|
||||||
? `Please ${action} your document ${documentName}`
|
|
||||||
: `${inviterName} has invited you to ${action} ${documentName}`;
|
|
||||||
|
|
||||||
const getAssetUrl = (path: string) => {
|
const getAssetUrl = (path: string) => {
|
||||||
return new URL(path, assetBaseUrl).toString();
|
return new URL(path, assetBaseUrl).toString();
|
||||||
@@ -75,7 +71,6 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
signDocumentLink={signDocumentLink}
|
signDocumentLink={signDocumentLink}
|
||||||
assetBaseUrl={assetBaseUrl}
|
assetBaseUrl={assetBaseUrl}
|
||||||
role={role}
|
role={role}
|
||||||
selfSigner={selfSigner}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
|
|||||||
* Does not take any person or group properties into account.
|
* Does not take any person or group properties into account.
|
||||||
*/
|
*/
|
||||||
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||||
app_allow_encrypted_documents: false,
|
|
||||||
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
|
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
|
||||||
app_document_page_view_history_sheet: false,
|
app_document_page_view_history_sheet: false,
|
||||||
app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag.
|
app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { APP_BASE_URL } from './app';
|
import { APP_BASE_URL } from './app';
|
||||||
|
|
||||||
export const DEFAULT_STANDARD_FONT_SIZE = 12;
|
export const DEFAULT_STANDARD_FONT_SIZE = 15;
|
||||||
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
|
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
|
||||||
|
|
||||||
export const MIN_STANDARD_FONT_SIZE = 8;
|
export const MIN_STANDARD_FONT_SIZE = 8;
|
||||||
|
|||||||
@@ -32,10 +32,3 @@ export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
|||||||
[RecipientRole.VIEWER]: 'VIEW_REQUEST',
|
[RecipientRole.VIEWER]: 'VIEW_REQUEST',
|
||||||
[RecipientRole.APPROVER]: 'APPROVE_REQUEST',
|
[RecipientRole.APPROVER]: 'APPROVE_REQUEST',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
|
||||||
[RecipientRole.SIGNER]: 'I am a signer of this document',
|
|
||||||
[RecipientRole.APPROVER]: 'I am an approver of this document',
|
|
||||||
[RecipientRole.CC]: 'I am required to recieve a copy of this document',
|
|
||||||
[RecipientRole.VIEWER]: 'I am a viewer of this document',
|
|
||||||
} satisfies Record<keyof typeof RecipientRole, string>;
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;
|
|
||||||
export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
@@ -150,24 +149,4 @@ export class AppError extends Error {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static toRestAPIError(err: unknown): {
|
|
||||||
status: 400 | 401 | 404 | 500;
|
|
||||||
body: { message: string };
|
|
||||||
} {
|
|
||||||
const error = AppError.parseError(err);
|
|
||||||
|
|
||||||
const status = match(error.code)
|
|
||||||
.with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => 400 as const)
|
|
||||||
.with(AppErrorCode.UNAUTHORIZED, () => 401 as const)
|
|
||||||
.with(AppErrorCode.NOT_FOUND, () => 404 as const)
|
|
||||||
.otherwise(() => 500 as const);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
body: {
|
|
||||||
message: status !== 500 ? error.message : 'Something went wrong',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user