Compare commits

..

30 Commits

Author SHA1 Message Date
a01385b15f exp: millionjs on marketing page 2024-01-20 14:22:24 +00:00
204388888d fix: fix bug for completed document shortcut (#839)
When you're in the `/documents` page in the dashboard, if you hover over
a draft and a completed document, you'll see different URLs.

At the moment, the shortcut tries to go to the following URL for a
completed document `/documents/{doc-id}`.

However, that's the wrong URL, since the URL for a completed doc is
`/sign/{token}` when the user is the recipient, not the one that sent
the document for signing.

If it's the document owner & the document is completed, the URL is fine
as `/documents/{doc-id}`.

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-01-18 09:38:42 +02:00
0d977e783e refactor: download function to be reusable (#740) 2024-01-18 15:24:36 +11:00
0d15b80c2d fix: simplify code 2024-01-18 04:23:22 +00:00
4e9cce0df0 chore: improve request template (#833)
Ref #622 
Consolidated the template(removed redundant fields), added appropriate
validation.
2024-01-18 12:13:20 +11:00
6f726565e8 Update README.md
We are nominated for a Product Hunt Gold Kitty 😺 and appreciate any support: https://documen.so/kitty
2024-01-17 14:36:28 +01:00
9ff44f10a6 chore: add incident blog post 2024-01-17 21:41:00 +11:00
16d97783f2 feat: improve the UX for password protected documents (#780) 2024-01-17 19:32:42 +11:00
91dd10ec9b fix: add symmetric encryption to document passwords 2024-01-17 17:28:28 +11:00
a94b829ee0 fix: tidy code 2024-01-17 17:17:08 +11:00
1bc885478d fix: display the number of documents in mobile view (#837)
This PR fixes #782.
It now displays the document count on mobile view.
2024-01-17 11:10:28 +11:00
b4b146ee49 Merge branch 'main' into Gautam-Hegde/issue#622 2024-01-16 23:27:34 +05:30
560352492d fix: keyboard shortcut ctrl+k fixed (#830) 2024-01-16 13:07:42 +11:00
67aebaac1a Update improvement.yml code quality 2024-01-16 01:14:48 +05:30
a593e045b5 Update improvement.yml 2024-01-16 00:08:04 +05:30
84b0c2756b fix: fixed the deleting signature block issue on touchscreens (#809)
Fixed the deleting signature block issue on touchscreens, for some
reason the `onClick` event isn't working on the touchscreens that's why
I've added `onTouchEnd` event to delete the signature block when the
user clicks on it.
2024-01-15 19:33:40 +11:00
58b3a127ea chore: fix color for light mode icon (#806) 2024-01-15 10:48:55 +11:00
7e71e06e04 fix: keyboard shortcut ctrl+k default behaviour fixed 2024-01-13 14:19:37 +05:30
68953d1253 feat add documentPassword to documenet meta and improve the ux
Signed-off-by: harkiratsm <multaniharry714@gmail.com>
2024-01-12 20:54:59 +05:30
d73ef57794 Merge branch 'main' into fix/bug-798-signatures-block 2024-01-11 16:50:43 +08:00
eeb6a072aa Merge branch 'main' into harkirat/Protect 2024-01-10 10:45:19 +05:30
a71078cbd5 fix: fixed the deleting signature block issue on touchscreens 2024-01-08 13:17:30 +08:00
d8cbe1d5ba Merge branch 'main' into harkirat/Protect 2024-01-03 11:34:42 +05:30
6a26ab4b2b fix: toast import errors 2024-01-02 04:52:15 +00:00
b76d2cea3b fix: changes from code review 2024-01-02 04:38:35 +00:00
53c570151f fix lint, description of dialog
Signed-off-by: harkiratsm <multaniharry714@gmail.com>
2023-12-29 22:11:44 +05:30
72a7dc6c05 fix the console error
Signed-off-by: harkiratsm <multaniharry714@gmail.com>
2023-12-29 17:26:33 +05:30
2ae9e29903 feat: improve the ux for password protected documents
Signed-off-by: harkiratsm <multaniharry714@gmail.com>
2023-12-22 17:24:05 +05:30
b9282f11b0 Merge branch 'main' into refactor/download-function 2023-12-08 11:26:02 +11:00
38ad3a1922 refactor: download function to be reusable 2023-12-07 14:52:12 +00:00
59 changed files with 1051 additions and 511 deletions

View File

@ -1,35 +1,39 @@
name: 'General Improvement'
description: Suggest a minor enhancement or improvement for this project
name: 'General Improvement Request'
description: 'Suggest a minor enhancement or improvement for this project'
title: '[Title for your improvement suggestion]'
body:
- type: markdown
attributes:
value: Please provide a clear and concise title for your improvement suggestion
- type: textarea
attributes:
label: Improvement Description
description: Describe the improvement you are suggesting in detail. Explain what specific aspect of the project it addresses or enhances.
label: 'Describe the improvement you are suggesting in detail'
description: 'Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.'
validations:
required: true
- type: textarea
id: description
attributes:
label: Rationale
description: Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.
- type: textarea
label: 'Additional Information & Alternatives (optional)'
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Proposed Solution
description: If you have a suggestion for how this improvement could be implemented, describe it here. Include any technical details, design suggestions, or other relevant information.
- type: textarea
attributes:
label: Alternatives (optional)
description: Are there any alternative approaches to achieve the same improvement? Describe other ways to address the issue or enhance the project.
- type: textarea
attributes:
label: Additional Context
description: Add any additional context or information that might be relevant to the improvement suggestion.
label: 'Do you want to work on this improvement?'
multiple: false
options:
- 'No'
- 'Yes'
default: 0
validations:
required: true
- type: checkboxes
attributes:
label: Please check the boxes that apply to this improvement suggestion.
label: 'Please check the boxes that apply to this improvement suggestion.'
options:
- label: I have searched the existing issues and improvement suggestions to avoid duplication.
- label: I have provided a clear description of the improvement being suggested.
- label: I have explained the rationale behind this improvement.
- label: I have included any relevant technical details or design suggestions.
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
- label: 'I have searched the existing issues and improvement suggestions to avoid duplication.'
- label: 'I have provided a clear description of the improvement being suggested.'
- label: 'I have explained the rationale behind this improvement.'
- label: 'I have included any relevant technical details or design suggestions.'
- label: 'I understand that this is a suggestion and that there is no guarantee of implementation.'
validations:
required: true

View File

@ -1,3 +1,5 @@
>We are nominated for a Product Hunt Gold Kitty 😺✨ and appreciate any support: https://documen.so/kitty
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<p align="center" style="margin-top: 20px">

View File

@ -0,0 +1,28 @@
---
title: Jan 10th Email Provider Security Incident
description: On January 10th, 2022, we were notified by our email provider that they had experienced a security incident.
authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder'
date: 2024-01-17
tags:
- Security
---
On January 10th, 2024 we were notified by our email provider that a security incident had occurred. This security incident which had started on January 7th led to a bad actor obtaining access to their database which contains ours and other customers data.
We understand that during this security incident the following has been accessed:
- Email addresses.
- Metadata on emails sent excluding the email body.
While the incident is unfortunate we are pleased with the remediation and the processes that our email provider has put in place to help avoid this kind of situation in the future. Since the incident, our provider has rectified the issue and has engaged a security company to conduct an exhaustive investigation and to help improve their security posture moving forward.
We remain steadfast in our commitment to our current email provider, and will not be taking any further action with relation to changing providers.
We are now working with our legal counsel to ensure that we provide the appropriate notice to all our customers in each jurisdiction. If you have any further questions on this incident please feel free to contact our support team at [support@documenso.com](mailto:support@documenso.com).
We appreciate your ongoing support in this matter.
You can read more on the incident on our providers blog post below:
[https://resend.com/blog/incident-report-for-january-10-2024](https://resend.com/blog/incident-report-for-january-10-2024)

View File

@ -1,6 +1,6 @@
---
title: Announcing Pre-Seed and Open Metrics
description: We are exicited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
description: We are excited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'

View File

@ -30,7 +30,7 @@ We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania)
## Documenso Merch Shop
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contributing to Documenso.
<figure>
<MdxNextImage

View File

@ -1,3 +1,4 @@
const million = require('million/compiler');
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const path = require('path');
@ -5,11 +6,11 @@ const { withContentlayer } = require('next-contentlayer');
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
ENV_FILES.forEach((file) => {
for (file of ENV_FILES) {
require('dotenv').config({
path: path.join(__dirname, `../../${file}`),
});
});
}
// !: This is a temp hack to get caveat working without placing it back in the public directory.
// !: By inlining this at build time we should be able to sign faster.
@ -94,4 +95,6 @@ const config = {
},
};
module.exports = withContentlayer(config);
module.exports = million.next(
withContentlayer(config), { auto: { rsc: true } }
);

View File

@ -24,6 +24,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"million": "^2.6.4",
"next": "14.0.3",
"next-auth": "4.24.5",
"next-contentlayer": "^0.3.4",

View File

@ -20,7 +20,7 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
<Image width={props.width} height={props.height} src={props.src} alt={props.alt ?? ''} />
),
};

View File

@ -25,7 +25,7 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
<Image width={props.width} height={props.height} src={props.src} alt={props.alt ?? ''} />
),
};

View File

@ -1,6 +1,6 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
@ -24,7 +24,6 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
label,
chartHeight = 400,
extraInfo,
...props
}: BarMetricProps<T>) => {
const formattedData = Object.keys(data)
.map((key) => ({
@ -34,7 +33,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
.reverse();
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>

View File

@ -1,6 +1,7 @@
'use client';
import { HTMLAttributes, useEffect, useState } from 'react';
import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts';
@ -41,14 +42,14 @@ const renderCustomizedLabel = ({
export type CapTableProps = HTMLAttributes<HTMLDivElement>;
export const CapTable = ({ className, ...props }: CapTableProps) => {
export const CapTable = ({ className }: CapTableProps) => {
const [isSSR, setIsSSR] = useState(true);
useEffect(() => {
setIsSSR(false);
}, []);
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Cap Table</h3>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border shadow-sm hover:shadow">
@ -76,7 +77,7 @@ export const CapTable = ({ className, ...props }: CapTableProps) => {
/>
<Tooltip
formatter={(percent: number, name, props) => {
return [`${percent}%`, name || props['name'] || props['payload']['name']];
return [`${percent}%`, name || props.name || props.payload.name];
}}
/>
</PieChart>

View File

@ -1,6 +1,6 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
@ -11,14 +11,14 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
};
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
export const FundingRaised = ({ className, data }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({
amount: Number(item.amount),
date: formatMonth(item.date as string),
}));
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Total Funding Raised</h3>
<div className="border-border mt-2.5 flex flex-1 flex-col items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
@ -7,9 +7,9 @@ export type MetricCardProps = HTMLAttributes<HTMLDivElement> & {
value: string;
};
export const MetricCard = ({ className, title, value, ...props }: MetricCardProps) => {
export const MetricCard = ({ className, title, value }: MetricCardProps) => {
return (
<div className={cn('rounded-md border p-4 shadow-sm hover:shadow', className)} {...props}>
<div className={cn('rounded-md border p-4 shadow-sm hover:shadow', className)}>
<h4 className="text-muted-foreground text-sm font-medium">{title}</h4>
<p className="mb-2 mt-6 text-4xl font-bold">{value}</p>

View File

@ -141,7 +141,12 @@ export default async function OpenPage() {
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
All our metrics, finances, and learnings are public. We believe in transparency and want
to share our journey with you. You can read more about why here:{' '}
<a className="font-bold" href="https://documenso.com/blog/pre-seed" target="_blank">
<a
className="font-bold"
href="https://documenso.com/blog/pre-seed"
target="_blank"
rel="noreferrer"
>
Announcing Open Metrics
</a>
</p>

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import {
@ -14,9 +14,9 @@ import { SALARY_BANDS } from '~/app/(marketing)/open/data';
export type SalaryBandsProps = HTMLAttributes<HTMLDivElement>;
export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
export const SalaryBands = ({ className }: SalaryBandsProps) => {
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Global Salary Bands</h3>
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
@ -30,7 +30,7 @@ export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
</TableHeader>
<TableBody>
{SALARY_BANDS.map((band, index) => (
<TableRow key={index}>
<TableRow key={band.title + index.toString()}>
<TableCell className="font-medium">{band.title}</TableCell>
<TableCell>{band.seniority}</TableCell>
<TableCell className="text-right">

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import {
@ -14,9 +14,9 @@ import { TEAM_MEMBERS } from './data';
export type TeamMembersProps = HTMLAttributes<HTMLDivElement>;
export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
export const TeamMembers = ({ className }: TeamMembersProps) => {
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h2 className="px-4 text-2xl font-semibold">Team</h2>
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">

View File

@ -34,13 +34,7 @@ export const ConfettiScreen = ({
}
return createPortal(
<Confetti
{...props}
className="w-full"
numberOfPieces={numberOfPieces}
width={width}
height={height}
/>,
<Confetti className="w-full" numberOfPieces={numberOfPieces} width={width} height={height} />,
document.body,
);
};

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -11,12 +11,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type FasterSmarterBeautifulBentoProps = HTMLAttributes<HTMLDivElement>;
export const FasterSmarterBeautifulBento = ({
className,
...props
}: FasterSmarterBeautifulBentoProps) => {
export const FasterSmarterBeautifulBento = ({ className }: FasterSmarterBeautifulBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -35,9 +35,9 @@ const FOOTER_LINKS = [
{ href: '/privacy', text: 'Privacy' },
];
export const Footer = ({ className, ...props }: FooterProps) => {
export const Footer = ({ className }: FooterProps) => {
return (
<div className={cn('border-t py-12', className)} {...props}>
<div className={cn('border-t py-12', className)}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div className="flex-shrink-0">
<Link href="/">
@ -53,7 +53,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4">
{SOCIAL_LINKS.map((link, index) => (
<Link
key={index}
key={link.href + index.toString()}
href={link.href}
target="_blank"
className="text-muted-foreground hover:text-muted-foreground/80"
@ -67,7 +67,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
{FOOTER_LINKS.map((link, index) => (
<Link
key={index}
key={link.href + index.toString()}
href={link.href}
target={link.target}
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm"

View File

@ -15,7 +15,7 @@ import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => {
export const Header = ({ className }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const { getFlag } = useFeatureFlags();
@ -23,7 +23,7 @@ export const Header = ({ className, ...props }: HeaderProps) => {
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
return (
<header className={cn('flex items-center justify-between', className)} {...props}>
<header className={cn('flex items-center justify-between', className)}>
<div className="flex items-center space-x-4">
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
<Image

View File

@ -3,7 +3,8 @@
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import type { Variants } from 'framer-motion';
import { motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { LuGithub } from 'react-icons/lu';
import { match } from 'ts-pattern';
@ -49,7 +50,7 @@ const HeroTitleVariants: Variants = {
},
};
export const Hero = ({ className, ...props }: HeroProps) => {
export const Hero = ({ className }: HeroProps) => {
const event = usePlausible();
const { getFlag } = useFeatureFlags();
@ -74,7 +75,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
};
return (
<motion.div className={cn('relative', className)} {...props}>
<motion.div className={cn('relative', className)}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full origin-top-right items-center justify-center"

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -11,9 +11,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type OpenBuildTemplateBentoProps = HTMLAttributes<HTMLDivElement>;
export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplateBentoProps) => {
export const OpenBuildTemplateBento = ({ className }: OpenBuildTemplateBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -15,13 +15,13 @@ export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
export const PricingTable = ({ className, ...props }: PricingTableProps) => {
export const PricingTable = ({ className }: PricingTableProps) => {
const event = usePlausible();
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>('MONTHLY');
return (
<div className={cn('', className)} {...props}>
<div className={cn('', className)}>
<div className="flex items-center justify-center gap-x-6">
<AnimatePresence>
<motion.button

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -12,12 +12,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type ShareConnectPaidWidgetBentoProps = HTMLAttributes<HTMLDivElement>;
export const ShareConnectPaidWidgetBento = ({
className,
...props
}: ShareConnectPaidWidgetBentoProps) => {
export const ShareConnectPaidWidgetBento = ({ className }: ShareConnectPaidWidgetBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -55,7 +55,7 @@ type StepValues = (typeof STEP)[StepKeys];
export type WidgetProps = HTMLAttributes<HTMLDivElement>;
export const Widget = ({ className, children, ...props }: WidgetProps) => {
export const Widget = ({ className, children }: WidgetProps) => {
const { toast } = useToast();
const event = usePlausible();
@ -148,19 +148,19 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
const claimPlanInput = signatureDataUrl
? {
name,
email,
planId,
signatureDataUrl: signatureDataUrl,
signatureText: null,
}
name,
email,
planId,
signatureDataUrl: signatureDataUrl,
signatureText: null,
}
: {
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText ?? '',
};
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText ?? '',
};
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);
@ -183,7 +183,6 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Card
className={cn('mx-auto w-full max-w-4xl rounded-3xl before:rounded-3xl', className)}
gradient
{...props}
>
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="text-muted-foreground col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed lg:col-span-7">

View File

@ -1,4 +1,4 @@
import { SVGAttributes } from 'react';
import type { SVGAttributes } from 'react';
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
import type { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@ -4,7 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
import type { DocumentData, DocumentMeta, Field, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc } from '@documenso/trpc/react';
@ -24,13 +24,12 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Comments } from '~/components/forms/comments';
export type EditDocumentFormProps = {
className?: string;
user: User;
document: DocumentWithData;
recipients: Recipient[];
documentMeta: DocumentMeta | null;
fields: Field[];
documentData: DocumentData;
};
@ -43,6 +42,7 @@ export const EditDocumentForm = ({
document,
recipients,
fields,
documentMeta,
user: _user,
documentData,
}: EditDocumentFormProps) => {
@ -58,6 +58,8 @@ export const EditDocumentForm = ({
const { mutateAsync: addFields } = trpc.field.addFields.useMutation();
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation();
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation();
const { mutateAsync: setPasswordForDocument } =
trpc.document.setPasswordForDocument.useMutation();
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
title: {
@ -178,73 +180,76 @@ export const EditDocumentForm = ({
}
};
const onPasswordSubmit = async (password: string) => {
await setPasswordForDocument({
documentId: document.id,
password,
});
};
const currentDocumentFlow = documentFlow[step];
return (
<div>
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer key={documentData.id} documentData={documentData} />
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer
className="lg:h-[calc(100vh-6rem)]"
onSubmit={(e) => e.preventDefault()}
>
<Stepper
currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
>
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
recipients={recipients}
fields={fields}
document={document}
onSubmit={onAddTitleFormSubmit}
/>
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSignersFormSubmit}
/>
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
onSubmit={onAddFieldsFormSubmit}
/>
<AddSubjectFormPartial
key={recipients.length}
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSubjectFormSubmit}
/>
</Stepper>
</DocumentFlowFormContainer>
</div>
</div>
<Card className="my-8" gradient={true} degrees={200}>
<CardContent className="mt-8 flex flex-col">
<h2 className="text-foreground text-2xl font-semibold">Comments</h2>
<hr className="border-border mb-4 mt-4" />
<Comments />
<hr className="border-border -mt-4 mb-4" />
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
onPasswordSubmit={onPasswordSubmit}
/>
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer
className="lg:h-[calc(100vh-6rem)]"
onSubmit={(e) => e.preventDefault()}
>
<Stepper
currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
>
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
recipients={recipients}
fields={fields}
document={document}
onSubmit={onAddTitleFormSubmit}
/>
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSignersFormSubmit}
/>
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
onSubmit={onAddFieldsFormSubmit}
/>
<AddSubjectFormPartial
key={recipients.length}
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSubjectFormSubmit}
/>
</Stepper>
</DocumentFlowFormContainer>
</div>
</div>
);
};

View File

@ -3,10 +3,12 @@ import { redirect } from 'next/navigation';
import { ChevronLeft, Users2 } from 'lucide-react';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -40,7 +42,24 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
redirect('/documents');
}
const { documentData } = document;
const { documentData, documentMeta } = document;
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
const securePassword = Buffer.from(
symmetricDecrypt({
key,
data: documentMeta.password,
}),
).toString('utf-8');
documentMeta.password = securePassword;
}
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
@ -83,6 +102,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
className="mt-8"
document={document}
user={user}
documentMeta={documentMeta}
recipients={recipients}
fields={fields}
documentData={documentData}
@ -91,7 +111,12 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
{document.status === InternalDocumentStatus.COMPLETED && (
<div className="mx-auto mt-12 max-w-2xl">
<LazyPDFViewer key={documentData.id} documentData={documentData} />
<LazyPDFViewer
document={document}
key={documentData.id}
documentMeta={documentMeta}
documentData={documentData}
/>
</div>
)}
</div>

View File

@ -6,7 +6,7 @@ import { Download, Edit, Pencil } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { Document, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
@ -55,28 +55,14 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const documentData = document?.documentData;
if (!documentData) {
return;
throw Error('No document available');
}
const documentBytes = await getFile(documentData);
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
link.href = window.URL.createObjectURL(blob);
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
} catch (error) {
await downloadPDF({ documentData, fileName: row.title });
} catch (err) {
toast({
title: 'Something went wrong',
description: 'An error occurred while trying to download file.',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
}

View File

@ -17,7 +17,7 @@ import {
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { Document, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
@ -30,6 +30,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { ResendDocumentActionItem } from './_action-items/resend-document';
import { DeleteDocumentDialog } from './delete-document-dialog';
@ -44,6 +45,7 @@ export type DataTableActionDropdownProps = {
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
@ -63,39 +65,33 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
const isDocumentDeletable = isOwner;
const onDownloadClick = async () => {
let document: DocumentWithData | null = null;
try {
let document: DocumentWithData | null = null;
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
}
const documentData = document?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: row.title });
} catch (err) {
toast({
title: 'Something went wrong',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
}
const documentData = document?.documentData;
if (!documentData) {
return;
}
const documentBytes = await getFile(documentData);
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
link.href = window.URL.createObjectURL(blob);
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
};
const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED');

View File

@ -88,7 +88,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
<span className="ml-1 inline-block opacity-50">
{Math.min(stats[value], 99)}
{stats[value] > 99 && '+'}
</span>

View File

@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { DEFAULT_DOCUMENT_DATE_FORMAT } 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';
@ -12,6 +13,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
@ -66,6 +68,23 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
redirect(`/sign/${token}/complete`);
}
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
const securePassword = Buffer.from(
symmetricDecrypt({
key,
data: documentMeta.password,
}),
).toString('utf-8');
documentMeta.password = securePassword;
}
const [recipientSignature] = await getRecipientSignatures({ recipientId: recipient.id });
if (document.deletedAt) {
@ -101,7 +120,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
gradient
>
<CardContent className="p-2">
<LazyPDFViewer key={documentData.id} documentData={documentData} />
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
/>
</CardContent>
</Card>

View File

@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook';
@ -13,6 +14,7 @@ import {
SETTINGS_PAGE_SHORTCUT,
TEMPLATES_PAGE_SHORTCUT,
} from '@documenso/lib/constants/keyboard-shortcuts';
import type { Document, Recipient } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import {
CommandDialog,
@ -65,6 +67,8 @@ export type CommandMenuProps = {
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const { setTheme } = useTheme();
const { data: session } = useSession();
const router = useRouter();
const [isOpen, setIsOpen] = useState(() => open ?? false);
@ -81,6 +85,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(() => {
if (!searchDocumentsData) {
return [];
@ -88,15 +103,14 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
return searchDocumentsData.map((document) => ({
label: document.title,
path: `/documents/${document.id}`,
path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient),
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
}));
}, [searchDocumentsData]);
}, [searchDocumentsData, isOwner, getSigningLink]);
const currentPage = pages[pages.length - 1];
const toggleOpen = (e: KeyboardEvent) => {
e.preventDefault();
const toggleOpen = () => {
setIsOpen((isOpen) => !isOpen);
onOpenChange?.(!isOpen);
@ -136,7 +150,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen);
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true });
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);

View File

@ -1,29 +0,0 @@
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date';
export type CommentCardProps = {
comment: any;
className?: string;
};
export const CommentCard = ({ comment, className }: CommentCardProps) => {
return (
<div className={cn('mb-8', className)} key={comment.id}>
<p className="font-semibold">{comment.User.name}</p>
<p className="text-sm">
<LocaleDate
date={comment.createdAt}
format={{
month: 'long',
day: 'numeric',
year: 'numeric',
}}
/>
</p>
<p className="mb-2 mt-2 text-base">{comment.comment}</p>
<Button>Reply</Button>
</div>
);
};

View File

@ -1,32 +0,0 @@
'use client';
import { CornerDownRight } from 'lucide-react';
import { trpc } from '@documenso/trpc/react';
import { CommentCard } from '~/components/comments/comment-card';
export const Comments = () => {
const { data: comments } = trpc.comment.getComments.useQuery();
console.log(comments);
return (
<div>
{comments?.map((comment) => (
<div key={comment.id}>
<CommentCard comment={comment} className="mb-8" />
{comment.replies && comment.replies.length > 0 ? (
<div>
{comment.replies.map((reply) => (
<div className="ml-6 flex" key={reply.id}>
<CornerDownRight className="flex shrink-0" />
<CommentCard comment={reply} className="ml-6" />
</div>
))}
</div>
) : null}
</div>
))}
</div>
);
};

View File

@ -124,7 +124,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
className="h-44 w-full"
containerClassName="rounded-lg border bg-background"
defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')}
/>
</FormControl>

469
package-lock.json generated
View File

@ -43,6 +43,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"million": "^2.6.4",
"next": "14.0.3",
"next-auth": "4.24.5",
"next-contentlayer": "^0.3.4",
@ -245,6 +246,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@aws-crypto/crc32": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
@ -1063,9 +1076,9 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
"integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
@ -1138,6 +1151,117 @@
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
"integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
"integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-module-transforms": "^7.23.3",
"@babel/helpers": "^7.23.7",
"@babel/parser": "^7.23.6",
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.7",
"@babel/types": "^7.23.6",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/traverse": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
"integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6",
"@babel/types": "^7.23.6",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/core/node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
@ -1159,6 +1283,42 @@
"node": ">=0.10.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
"integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dependencies": {
"@babel/compat-data": "^7.23.5",
"@babel/helper-validator-option": "^7.23.5",
"browserslist": "^4.22.2",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
@ -1216,6 +1376,80 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
"integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dependencies": {
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
"integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-module-imports": "^7.22.15",
"@babel/helper-simple-access": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/helper-validator-identifier": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
"integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
"integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
"dependencies": {
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
@ -1256,6 +1490,82 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
"integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz",
"integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==",
"dependencies": {
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.7",
"@babel/types": "^7.23.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/traverse": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
"integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6",
"@babel/types": "^7.23.6",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
@ -1334,9 +1644,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -1344,6 +1654,34 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
"integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
"integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
@ -7581,9 +7919,9 @@
}
},
"node_modules/browserslist": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"funding": [
{
"type": "opencollective",
@ -7599,9 +7937,9 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
"node-releases": "^2.0.13",
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@ -7763,9 +8101,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001564",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz",
"integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==",
"version": "1.0.30001579",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
"integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==",
"funding": [
{
"type": "opencollective",
@ -8546,6 +8884,11 @@
"node": ">=14"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@ -9347,9 +9690,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.593",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.593.tgz",
"integrity": "sha512-c7+Hhj87zWmdpmjDONbvNKNo24tvmD4mjal1+qqTYTrlF0/sNpAcDlU0Ki84ftA/5yj3BF2QhSGEC0Rky6larg=="
"version": "1.4.637",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.637.tgz",
"integrity": "sha512-G7j3UCOukFtxVO1vWrPQUoDk3kL70mtvjc/DC/k2o7lE0wAdq+Vwp1ipagOow+BH0uVztFysLWbkM/RTIrbK3w=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@ -11056,6 +11399,14 @@
"node": ">=8"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -14125,6 +14476,54 @@
"node": ">=8.6"
}
},
"node_modules/million": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/million/-/million-2.6.4.tgz",
"integrity": "sha512-voUkdd/jHWrG+7NS+mX49Pat+POKdgGW78V7pYMSrTaOjUitR6ySEcAci8hn17Rsx1IMI3+5w41dkADM1J1ZEg==",
"dependencies": {
"@babel/core": "^7.21.0",
"@babel/generator": "^7.22.7",
"@babel/plugin-syntax-jsx": "^7.21.4",
"@babel/plugin-syntax-typescript": "^7.21.4",
"@babel/types": "^7.21.3",
"kleur": "^4.1.5",
"rollup": "^3.28.0",
"unplugin": "^1.3.1"
},
"bin": {
"million": "packages/cli/dist/index.js"
},
"funding": {
"url": "https://github.com/sponsors/aidenybai"
}
},
"node_modules/million/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/million/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -14571,9 +14970,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/node-signpdf": {
"version": "2.0.0",
@ -16915,7 +17314,6 @@
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@ -18804,6 +19202,17 @@
"node": ">= 0.8"
}
},
"node_modules/unplugin": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.6.0.tgz",
"integrity": "sha512-BfJEpWBu3aE/AyHx8VaNE/WgouoQxgH9baAiH82JjX8cqVyi3uJQstqwD5J+SZxIK326SZIhsSZlALXVBCknTQ==",
"dependencies": {
"acorn": "^8.11.2",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.6.1"
}
},
"node_modules/unzipper": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
@ -19124,6 +19533,19 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
"integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@ -19869,7 +20291,8 @@
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5"
"ts-pattern": "^5.0.5",
"zod": "^3.22.4"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

@ -45,7 +45,6 @@
"name": "@documenso/root",
"workspaces": [
"apps/*",
"packages/*"
],
"dependencies": {},

View File

@ -0,0 +1,29 @@
import type { DocumentData } from '@documenso/prisma/client';
import { getFile } from '../universal/upload/get-file';
type DownloadPDFProps = {
documentData: DocumentData;
fileName?: string;
};
export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => {
const bytes = await getFile(documentData);
const blob = new Blob([bytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
const [baseTitle] = fileName?.includes('.pdf')
? fileName.split('.pdf')
: [fileName ?? 'document'];
link.href = window.URL.createObjectURL(blob);
link.download = `${baseTitle}_signed.pdf`;
link.click();
window.URL.revokeObjectURL(link.href);
};

View File

@ -1,33 +0,0 @@
import { getUserByApiToken } from '@documenso/lib/server-only/public-api/get-user-by-token';
export type Headers = {
headers: {
authorization: string;
};
};
export const authenticatedMiddleware = <T extends Headers>(fn: (args: T) => Promise<any>) => {
return async (args: T) => {
if (!args.headers.authorization) {
return {
status: 401,
body: {
message: 'Unauthorized access',
},
};
}
try {
await getUserByApiToken({ token: args.headers.authorization });
} catch (err) {
return {
status: 401,
body: {
message: 'Unauthorized access',
},
};
}
return fn(args);
};
};

View File

@ -1,25 +0,0 @@
import { prisma } from '@documenso/prisma';
export const findComments = async () => {
return await prisma.documentComment.findMany({
where: {
parentId: null,
},
include: {
User: {
select: {
name: true,
},
},
replies: {
include: {
User: {
select: {
name: true,
},
},
},
},
},
});
};

View File

@ -4,10 +4,11 @@ import { prisma } from '@documenso/prisma';
export type CreateDocumentMetaOptions = {
documentId: number;
subject: string;
message: string;
timezone: string;
dateFormat: string;
subject?: string;
message?: string;
timezone?: string;
password?: string;
dateFormat?: string;
userId: number;
};
@ -18,6 +19,7 @@ export const upsertDocumentMeta = async ({
dateFormat,
documentId,
userId,
password,
}: CreateDocumentMetaOptions) => {
await prisma.document.findFirstOrThrow({
where: {
@ -35,12 +37,14 @@ export const upsertDocumentMeta = async ({
message,
dateFormat,
timezone,
password,
documentId,
},
update: {
subject,
message,
dateFormat,
password,
timezone,
},
});

View File

@ -26,6 +26,7 @@ export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByI
message: true,
subject: true,
dateFormat: true,
password: true,
timezone: true,
},
},

View File

@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "DocumentComment" (
"id" SERIAL NOT NULL,
"documentId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"comment" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DocumentComment_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "DocumentComment" ADD COLUMN "parentId" INTEGER;
-- AddForeignKey
ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "DocumentComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "password" TEXT;

View File

@ -42,7 +42,6 @@ model User {
twoFactorBackupCodes String?
VerificationToken VerificationToken[]
Template Template[]
DocumentComment DocumentComment[]
@@index([email])
}
@ -122,43 +121,27 @@ enum DocumentStatus {
}
model Document {
id Int @id @default(autoincrement())
userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
title String
status DocumentStatus @default(DRAFT)
Recipient Recipient[]
Field Field[]
ShareLink DocumentShareLink[]
documentDataId String
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
documentMeta DocumentMeta?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
completedAt DateTime?
deletedAt DateTime?
DocumentComments DocumentComment[]
id Int @id @default(autoincrement())
userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
title String
status DocumentStatus @default(DRAFT)
Recipient Recipient[]
Field Field[]
ShareLink DocumentShareLink[]
documentDataId String
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
documentMeta DocumentMeta?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
completedAt DateTime?
deletedAt DateTime?
@@unique([documentDataId])
@@index([userId])
@@index([status])
}
model DocumentComment {
id Int @id @default(autoincrement())
documentId Int
userId Int
comment String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
parentId Int?
parent DocumentComment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies DocumentComment[] @relation("CommentReplies")
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum DocumentDataType {
S3_PATH
BYTES
@ -179,6 +162,7 @@ model DocumentMeta {
subject String?
message String?
timezone String? @default("Etc/UTC") @db.Text
password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)

View File

@ -1,9 +0,0 @@
import { findComments } from '@documenso/lib/server-only/comment/find-comments';
import { procedure, router } from '../trpc';
export const commentRouter = router({
getComments: procedure.query(async () => {
return await findComments();
}),
});

View File

@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
@ -13,6 +14,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
@ -24,6 +26,7 @@ import {
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
ZSetFieldsForDocumentMutationSchema,
ZSetPasswordForDocumentMutationSchema,
ZSetRecipientsForDocumentMutationSchema,
ZSetTitleForDocumentMutationSchema,
} from './schema';
@ -175,6 +178,38 @@ export const documentRouter = router({
}
}),
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, password } = input;
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing encryption key');
}
const securePassword = symmetricEncrypt({
data: password,
key,
});
await upsertDocumentMeta({
documentId,
password: securePassword,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set the password for this document. Please try again later.',
});
}
}),
sendDocument: authenticatedProcedure
.input(ZSendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -73,6 +73,15 @@ export const ZSendDocumentMutationSchema = z.object({
}),
});
export const ZSetPasswordForDocumentMutationSchema = z.object({
documentId: z.number(),
password: z.string(),
});
export type TSetPasswordForDocumentMutationSchema = z.infer<
typeof ZSetPasswordForDocumentMutationSchema
>;
export const ZResendDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z.array(z.number()).min(1),

View File

@ -1,6 +1,5 @@
import { adminRouter } from './admin-router/router';
import { authRouter } from './auth-router/router';
import { commentRouter } from './comment-router/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router';
@ -22,7 +21,6 @@ export const appRouter = router({
singleplayer: singleplayerRouter,
twoFactorAuthentication: twoFactorAuthenticationRouter,
template: templateRouter,
comment: commentRouter,
});
export type AppRouter = typeof appRouter;

View File

@ -5,11 +5,11 @@ import { useState } from 'react';
import { Download } from 'lucide-react';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { DocumentData } from '@documenso/prisma/client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Button } from '../../primitives/button';
import { useToast } from '../../primitives/use-toast';
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
disabled?: boolean;
@ -24,43 +24,29 @@ export const DocumentDownloadButton = ({
disabled,
...props
}: DownloadButtonProps) => {
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
const onDownloadClick = async () => {
try {
setIsLoading(true);
if (!documentData) {
setIsLoading(false);
return;
}
const bytes = await getFile(documentData);
const blob = new Blob([bytes], {
type: 'application/pdf',
await downloadPDF({ documentData, fileName }).then(() => {
setIsLoading(false);
});
const link = window.document.createElement('a');
const baseTitle = fileName?.includes('.pdf') ? fileName.split('.pdf')[0] : fileName;
link.href = window.URL.createObjectURL(blob);
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
} catch (err) {
console.error(err);
setIsLoading(false);
toast({
title: 'Error',
title: 'Something went wrong',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};

View File

@ -70,6 +70,7 @@
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5"
"ts-pattern": "^5.0.5",
"zod": "^3.22.4"
}
}

View File

@ -121,6 +121,7 @@ export const FieldItem = ({
<button
className="text-muted-foreground/50 hover:text-muted-foreground/80 bg-background absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border"
onClick={() => onRemove?.()}
onTouchEnd={() => onRemove?.()}
>
<Trash className="h-4 w-4" />
</button>

View File

@ -0,0 +1,96 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from './button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog';
import { Form, FormControl, FormField, FormItem, FormMessage } from './form/form';
import { Input } from './input';
const ZPasswordDialogFormSchema = z.object({
password: z.string(),
});
type TPasswordDialogFormSchema = z.infer<typeof ZPasswordDialogFormSchema>;
type PasswordDialogProps = {
open: boolean;
onOpenChange: (_open: boolean) => void;
defaultPassword?: string;
onPasswordSubmit?: (password: string) => void;
isError?: boolean;
};
export const PasswordDialog = ({
open,
onOpenChange,
defaultPassword,
onPasswordSubmit,
isError,
}: PasswordDialogProps) => {
const form = useForm<TPasswordDialogFormSchema>({
defaultValues: {
password: defaultPassword ?? '',
},
resolver: zodResolver(ZPasswordDialogFormSchema),
});
const onFormSubmit = ({ password }: TPasswordDialogFormSchema) => {
onPasswordSubmit?.(password);
};
useEffect(() => {
if (isError) {
form.setError('password', {
type: 'manual',
message: 'The password you have entered is incorrect. Please try again.',
});
}
}, [form, isError]);
return (
<Dialog open={open}>
<DialogContent className="w-full max-w-md">
<DialogHeader>
<DialogTitle>Password Required</DialogTitle>
<DialogDescription className="text-muted-foreground">
This document is password protected. Please enter the password to view the document.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex flex-wrap items-start justify-between gap-4">
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem className="relative flex-1">
<FormControl>
<Input
type="password"
className="bg-background"
placeholder="Enter password"
autoComplete="off"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Button>Submit</Button>
</div>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -3,16 +3,19 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Loader } from 'lucide-react';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import type { DocumentData } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { cn } from '../lib/utils';
import { PasswordDialog } from './document-password-dialog';
import { useToast } from './use-toast';
export type LoadedPDFDocument = PDFDocumentProxy;
@ -43,6 +46,9 @@ const PDFLoader = () => (
export type PDFViewerProps = {
className?: string;
documentData: DocumentData;
document?: DocumentWithData;
password?: string | null;
onPasswordSubmit?: (password: string) => void | Promise<void>;
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick;
[key: string]: unknown;
@ -51,6 +57,8 @@ export type PDFViewerProps = {
export const PDFViewer = ({
className,
documentData,
password: defaultPassword,
onPasswordSubmit,
onDocumentLoad,
onPageClick,
...props
@ -59,7 +67,11 @@ export const PDFViewer = ({
const $el = useRef<HTMLDivElement>(null);
const passwordCallbackRef = useRef<((password: string | null) => void) | null>(null);
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
const [isPasswordError, setIsPasswordError] = useState(false);
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(null);
const [width, setWidth] = useState(0);
@ -169,57 +181,87 @@ export const PDFViewer = ({
<PDFLoader />
</div>
) : (
<PDFDocument
file={documentBytes.buffer}
className={cn('w-full overflow-hidden rounded', {
'h-[80vh] max-h-[60rem]': numPages === 0,
})}
onLoadSuccess={(d) => onDocumentLoaded(d)}
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
// Therefore we add some additional custom error handling.
onSourceError={() => {
setPdfError(true);
}}
externalLinkTarget="_blank"
loading={
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
{pdfError ? (
<>
<PDFDocument
file={documentBytes.buffer}
className={cn('w-full overflow-hidden rounded', {
'h-[80vh] max-h-[60rem]': numPages === 0,
})}
onPassword={(callback, reason) => {
// If the document already has a password, we don't need to ask for it again.
if (defaultPassword && reason !== PasswordResponses.INCORRECT_PASSWORD) {
callback(defaultPassword);
return;
}
setIsPasswordModalOpen(true);
passwordCallbackRef.current = callback;
match(reason)
.with(PasswordResponses.NEED_PASSWORD, () => setIsPasswordError(false))
.with(PasswordResponses.INCORRECT_PASSWORD, () => setIsPasswordError(true));
}}
onLoadSuccess={(d) => onDocumentLoaded(d)}
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
// Therefore we add some additional custom error handling.
onSourceError={() => {
setPdfError(true);
}}
externalLinkTarget="_blank"
loading={
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
{pdfError ? (
<div className="text-muted-foreground text-center">
<p>Something went wrong while loading the document.</p>
<p className="mt-1 text-sm">Please try again or contact our support.</p>
</div>
) : (
<PDFLoader />
)}
</div>
}
error={
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<div className="text-muted-foreground text-center">
<p>Something went wrong while loading the document.</p>
<p className="mt-1 text-sm">Please try again or contact our support.</p>
</div>
) : (
<PDFLoader />
)}
</div>
}
error={
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<div className="text-muted-foreground text-center">
<p>Something went wrong while loading the document.</p>
<p className="mt-1 text-sm">Please try again or contact our support.</p>
</div>
</div>
}
>
{Array(numPages)
.fill(null)
.map((_, i) => (
<div
key={i}
className="border-border my-8 overflow-hidden rounded border will-change-transform first:mt-0 last:mb-0"
>
<PDFPage
pageNumber={i + 1}
width={width}
renderAnnotationLayer={false}
renderTextLayer={false}
loading={() => ''}
onClick={(e) => onDocumentPageClick(e, i + 1)}
/>
</div>
))}
</PDFDocument>
}
>
{Array(numPages)
.fill(null)
.map((_, i) => (
<div
key={i}
className="border-border my-8 overflow-hidden rounded border will-change-transform first:mt-0 last:mb-0"
>
<PDFPage
pageNumber={i + 1}
width={width}
renderAnnotationLayer={false}
renderTextLayer={false}
loading={() => ''}
onClick={(e) => onDocumentPageClick(e, i + 1)}
/>
</div>
))}
</PDFDocument>
<PasswordDialog
open={isPasswordModalOpen}
onOpenChange={setIsPasswordModalOpen}
onPasswordSubmit={(password) => {
passwordCallbackRef.current?.(password);
setIsPasswordModalOpen(false);
void onPasswordSubmit?.(password);
}}
isError={isPasswordError}
/>
</>
)}
</div>
);

View File

@ -18,7 +18,7 @@ export const ThemeSwitcher = () => {
>
{isMounted && theme === THEMES_TYPE.LIGHT && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
className="bg-background absolute inset-0 rounded-full mix-blend-color-burn"
layoutId="selected-theme"
/>
)}