Compare commits
27 Commits
v1.7.0
...
chore/sele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0ed8a658b | ||
|
|
ddee8a8272 | ||
|
|
efb2bc94ab | ||
|
|
97ee69e7a0 | ||
|
|
3da344fc5f | ||
|
|
404ca3202f | ||
|
|
c043fa9c06 | ||
|
|
9852e8971f | ||
|
|
5091112e4b | ||
|
|
e76f732990 | ||
|
|
b7c3deb6cd | ||
|
|
08114f7b97 | ||
|
|
6e368cc333 | ||
|
|
4ce4ca3f34 | ||
|
|
7644c0d855 | ||
|
|
fa6453e811 | ||
|
|
f7a20113e5 | ||
|
|
3d644db286 | ||
|
|
357bdd374f | ||
|
|
7b06b68572 | ||
|
|
9ee89346b1 | ||
|
|
77da7847d9 | ||
|
|
c36306d2c9 | ||
|
|
f6f893fbf7 | ||
|
|
e1b2206d28 | ||
|
|
ad135b72d8 | ||
|
|
e81023f8d4 |
48
.cursorrules
Normal file
48
.cursorrules
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
Code Style and Structure:
|
||||||
|
- Write concise, technical TypeScript code with accurate examples
|
||||||
|
- Use functional and declarative programming patterns; avoid classes
|
||||||
|
- Prefer iteration and modularization over code duplication
|
||||||
|
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
|
||||||
|
- Structure files: exported component, subcomponents, helpers, static content, types
|
||||||
|
|
||||||
|
Naming Conventions:
|
||||||
|
- Use lowercase with dashes for directories (e.g., components/auth-wizard)
|
||||||
|
- Favor named exports for components
|
||||||
|
|
||||||
|
TypeScript Usage:
|
||||||
|
- Use TypeScript for all code; prefer interfaces over types
|
||||||
|
- Avoid enums; use maps instead
|
||||||
|
- Use functional components with TypeScript interfaces
|
||||||
|
|
||||||
|
Syntax and Formatting:
|
||||||
|
- Use the "function" keyword for pure functions
|
||||||
|
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements
|
||||||
|
- Use declarative JSX
|
||||||
|
|
||||||
|
Error Handling and Validation:
|
||||||
|
- Prioritize error handling: handle errors and edge cases early
|
||||||
|
- Use early returns and guard clauses
|
||||||
|
- Implement proper error logging and user-friendly messages
|
||||||
|
- Use Zod for form validation
|
||||||
|
- Model expected errors as return values in Server Actions
|
||||||
|
- Use error boundaries for unexpected errors
|
||||||
|
|
||||||
|
UI and Styling:
|
||||||
|
- Use Shadcn UI, Radix, and Tailwind Aria for components and styling
|
||||||
|
- Implement responsive design with Tailwind CSS; use a mobile-first approach
|
||||||
|
|
||||||
|
Performance Optimization:
|
||||||
|
- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC)
|
||||||
|
- Wrap client components in Suspense with fallback
|
||||||
|
- Use dynamic loading for non-critical components
|
||||||
|
- Optimize images: use WebP format, include size data, implement lazy loading
|
||||||
|
|
||||||
|
Key Conventions:
|
||||||
|
- Use 'nuqs' for URL search parameter state management
|
||||||
|
- Optimize Web Vitals (LCP, CLS, FID)
|
||||||
|
- Limit 'use client':
|
||||||
|
- Favor server components and Next.js SSR
|
||||||
|
- Use only for Web API access in small components
|
||||||
|
- Avoid for data fetching or state management
|
||||||
|
|
||||||
|
Follow Next.js docs for Data Fetching, Rendering, and Routing
|
||||||
@@ -5,6 +5,8 @@ description: Learn how to self-host Documenso on your server or cloud infrastruc
|
|||||||
|
|
||||||
import { Callout, Steps } from 'nextra/components';
|
import { Callout, Steps } from 'nextra/components';
|
||||||
|
|
||||||
|
import { CallToAction } from '@documenso/ui/components/call-to-action';
|
||||||
|
|
||||||
# Self Hosting
|
# Self Hosting
|
||||||
|
|
||||||
We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind!
|
We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind!
|
||||||
@@ -273,3 +275,5 @@ We offer several alternative deployment methods for Documenso if you need more o
|
|||||||
## Koyeb
|
## Koyeb
|
||||||
|
|
||||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||||
|
|
||||||
|
<CallToAction className="mt-12" utmSource="self-hosting" />
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ title: Getting Started with Self-Hosting
|
|||||||
description: A step-by-step guide to setting up and hosting your own Documenso instance.
|
description: A step-by-step guide to setting up and hosting your own Documenso instance.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import { CallToAction } from '@documenso/ui/components/call-to-action';
|
||||||
|
|
||||||
# Getting Started with Self-Hosting
|
# Getting Started with Self-Hosting
|
||||||
|
|
||||||
This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses).
|
This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses).
|
||||||
|
|
||||||
|
<CallToAction className="mt-12" utmSource="self-hosting" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"signing-documents": "Signing Documents",
|
"signing-documents": "Signing Documents",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"direct-links": "Direct Signing Links",
|
"direct-links": "Direct Signing Links",
|
||||||
|
"document-visibility": "Document Visibility",
|
||||||
"-- Legal Overview": {
|
"-- Legal Overview": {
|
||||||
"type": "separator",
|
"type": "separator",
|
||||||
"title": "Legal Overview"
|
"title": "Legal Overview"
|
||||||
|
|||||||
18
apps/documentation/pages/users/document-visibility.mdx
Normal file
18
apps/documentation/pages/users/document-visibility.mdx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: Document Visibility
|
||||||
|
description: Learn how to control the visibility of your team documents.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Team's Document Visibility
|
||||||
|
|
||||||
|
By default, all documents created in a team are visible to all team members. However, you can control the visibility of your documents by changing the document's visibility settings.
|
||||||
|
|
||||||
|
To set the visibility of a document, click on the **Document visibility** dropdown in the document's settings panel.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The document visibility can be set to one of the following options:
|
||||||
|
|
||||||
|
- **Everyone** - The document is visible to all team members.
|
||||||
|
- **Managers and above** - The document is visible to people with the role of Manager or above.
|
||||||
|
- **Admin only** - The document is only visible to the team's admins.
|
||||||
BIN
apps/documentation/public/document-visibility-settings.webp
Normal file
BIN
apps/documentation/public/document-visibility-settings.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/marketing",
|
"name": "@documenso/marketing",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.1.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-contentlayer": "^0.3.4",
|
"next-contentlayer": "^0.3.4",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"perfect-freehand": "^1.2.0",
|
"perfect-freehand": "^1.2.0",
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export const SinglePlayerClient = () => {
|
|||||||
sendStatus: 'NOT_SENT',
|
sendStatus: 'NOT_SENT',
|
||||||
role: 'SIGNER',
|
role: 'SIGNER',
|
||||||
authOptions: null,
|
authOptions: null,
|
||||||
|
signingOrder: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { Caveat, Inter } from 'next/font/google';
|
import { Caveat, Inter } from 'next/font/google';
|
||||||
import { cookies, headers } from 'next/headers';
|
|
||||||
|
|
||||||
import { AxiomWebVitals } from 'next-axiom';
|
import { AxiomWebVitals } from 'next-axiom';
|
||||||
import { PublicEnvScript } from 'next-runtime-env';
|
import { PublicEnvScript } from 'next-runtime-env';
|
||||||
@@ -10,8 +9,6 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
|
|||||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
|
||||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
|
||||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
|
||||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -59,25 +56,7 @@ export function generateMetadata() {
|
|||||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const flags = await getAllAnonymousFlags();
|
const flags = await getAllAnonymousFlags();
|
||||||
|
|
||||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
const { lang, locales, i18n } = setupI18nSSR();
|
||||||
|
|
||||||
// Should be safe to remove when we upgrade NextJS.
|
|
||||||
// https://github.com/vercel/next.js/pull/65008
|
|
||||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
|
||||||
// during the same render.
|
|
||||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
|
||||||
if (!cookies().get('i18n')) {
|
|
||||||
const setCookieValue = headers().get('set-cookie');
|
|
||||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
|
||||||
|
|
||||||
if (i18nCookie) {
|
|
||||||
const i18n = i18nCookie.split('=')[1];
|
|
||||||
|
|
||||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -105,7 +84,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<PlausibleProvider>
|
<PlausibleProvider>
|
||||||
<TrpcProvider>
|
<TrpcProvider>
|
||||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
<I18nClientProvider
|
||||||
|
initialLocaleData={{ lang, locales }}
|
||||||
|
initialMessages={i18n.messages}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</I18nClientProvider>
|
</I18nClientProvider>
|
||||||
</TrpcProvider>
|
</TrpcProvider>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
import { type HTMLAttributes, useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -9,15 +9,15 @@ import { msg } from '@lingui/macro';
|
|||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { FaXTwitter } from 'react-icons/fa6';
|
import { FaXTwitter } from 'react-icons/fa6';
|
||||||
import { LiaDiscord } from 'react-icons/lia';
|
import { LiaDiscord } from 'react-icons/lia';
|
||||||
import { LuGithub } from 'react-icons/lu';
|
import { LuGithub, LuLanguages } from 'react-icons/lu';
|
||||||
|
|
||||||
import LogoImage from '@documenso/assets/logo.png';
|
import LogoImage from '@documenso/assets/logo.png';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
|
||||||
|
|
||||||
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
|
|
||||||
|
|
||||||
// import { StatusWidgetContainer } from './status-widget-container';
|
// import { StatusWidgetContainer } from './status-widget-container';
|
||||||
|
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -44,7 +44,9 @@ const FOOTER_LINKS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const Footer = ({ className, ...props }: FooterProps) => {
|
export const Footer = ({ className, ...props }: FooterProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
|
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('border-t py-12', className)} {...props}>
|
<div className={cn('border-t py-12', className)} {...props}>
|
||||||
@@ -97,13 +99,22 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-row-reverse items-center sm:flex-row">
|
<div className="flex flex-row-reverse items-center sm:flex-row">
|
||||||
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" />
|
<Button
|
||||||
|
className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setLanguageSwitcherOpen(true)}
|
||||||
|
>
|
||||||
|
<LuLanguages className="mr-1.5 h-4 w-4" />
|
||||||
|
{SUPPORTED_LANGUAGES[i18n.locale]?.full || i18n.locale}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { CheckIcon } from 'lucide-react';
|
|
||||||
import { LuLanguages } from 'react-icons/lu';
|
|
||||||
|
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
|
||||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from '@documenso/ui/primitives/command';
|
|
||||||
|
|
||||||
type I18nSwitcherProps = {
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
|
|
||||||
const { i18n, _ } = useLingui();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [value, setValue] = useState(i18n.locale);
|
|
||||||
|
|
||||||
const setLanguage = async (lang: string) => {
|
|
||||||
setValue(lang);
|
|
||||||
setOpen(false);
|
|
||||||
|
|
||||||
await dynamicActivate(i18n, lang);
|
|
||||||
await switchI18NLanguage(lang);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
|
|
||||||
<LuLanguages className="mr-1.5 h-4 w-4" />
|
|
||||||
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<CommandInput placeholder={_(msg`Search languages...`)} />
|
|
||||||
|
|
||||||
<CommandList>
|
|
||||||
<CommandGroup>
|
|
||||||
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
|
||||||
<CommandItem
|
|
||||||
key={language.short}
|
|
||||||
value={language.full}
|
|
||||||
onSelect={async () => setLanguage(language.short)}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
value === language.short ? 'opacity-100' : 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{SUPPORTED_LANGUAGES[language.short].full}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { cookies } from 'next/headers';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
|
||||||
|
|
||||||
export default function middleware(req: NextRequest) {
|
|
||||||
const lang = extractSupportedLanguage({
|
|
||||||
headers: req.headers,
|
|
||||||
cookies: cookies(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = NextResponse.next();
|
|
||||||
|
|
||||||
response.cookies.set('i18n', lang);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
/*
|
|
||||||
* Match all request paths except for the ones starting with:
|
|
||||||
* - api (API routes)
|
|
||||||
* - _next/static (static files)
|
|
||||||
* - _next/image (image optimization files)
|
|
||||||
* - favicon.ico (favicon file)
|
|
||||||
* - ingest (analytics)
|
|
||||||
* - site.webmanifest
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
|
|
||||||
missing: [
|
|
||||||
{ type: 'header', key: 'next-router-prefetch' },
|
|
||||||
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.1.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"remeda": "^1.27.1",
|
"remeda": "^2.12.1",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
|
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { AdminActions } from './admin-actions';
|
import { AdminActions } from './admin-actions';
|
||||||
import { RecipientItem } from './recipient-item';
|
import { RecipientItem } from './recipient-item';
|
||||||
@@ -25,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
|
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
|
||||||
setupI18nSSR();
|
const { i18n } = setupI18nSSR();
|
||||||
|
|
||||||
const document = await getEntireDocument({ id: Number(params.id) });
|
const document = await getEntireDocument({ id: Number(params.id) });
|
||||||
|
|
||||||
@@ -46,12 +45,11 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
|
|||||||
|
|
||||||
<div className="text-muted-foreground mt-4 text-sm">
|
<div className="text-muted-foreground mt-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Trans>Created on</Trans>:{' '}
|
<Trans>Created on</Trans>: {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
|
||||||
<LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Trans>Last updated at</Trans>:{' '}
|
<Trans>Last updated at</Trans>: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
|
||||||
<LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,11 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
// export type AdminDocumentResultsProps = {};
|
// export type AdminDocumentResultsProps = {};
|
||||||
|
|
||||||
export const AdminDocumentResults = () => {
|
export const AdminDocumentResults = () => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ export const AdminDocumentResults = () => {
|
|||||||
{
|
{
|
||||||
header: _(msg`Created`),
|
header: _(msg`Created`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Title`),
|
header: _(msg`Title`),
|
||||||
@@ -122,7 +121,7 @@ export const AdminDocumentResults = () => {
|
|||||||
{
|
{
|
||||||
header: 'Last updated',
|
header: 'Last updated',
|
||||||
accessorKey: 'updatedAt',
|
accessorKey: 'updatedAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
|
cell: ({ row }) => i18n.date(row.original.updatedAt),
|
||||||
},
|
},
|
||||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
|
||||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentPageViewInformationProps = {
|
export type DocumentPageViewInformationProps = {
|
||||||
@@ -24,21 +23,9 @@ export const DocumentPageViewInformation = ({
|
|||||||
}: DocumentPageViewInformationProps) => {
|
}: DocumentPageViewInformationProps) => {
|
||||||
const isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
|
|
||||||
const { locale } = useLocale();
|
const { _, i18n } = useLingui();
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const documentInformation = useMemo(() => {
|
const documentInformation = useMemo(() => {
|
||||||
let createdValue = DateTime.fromJSDate(document.createdAt).toFormat('MMMM d, yyyy');
|
|
||||||
let lastModifiedValue = DateTime.fromJSDate(document.updatedAt).toRelative();
|
|
||||||
|
|
||||||
if (!isMounted) {
|
|
||||||
createdValue = DateTime.fromJSDate(document.createdAt)
|
|
||||||
.setLocale(locale)
|
|
||||||
.toFormat('MMMM d, yyyy');
|
|
||||||
|
|
||||||
lastModifiedValue = DateTime.fromJSDate(document.updatedAt).setLocale(locale).toRelative();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
description: msg`Uploaded by`,
|
description: msg`Uploaded by`,
|
||||||
@@ -46,15 +33,19 @@ export const DocumentPageViewInformation = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: msg`Created`,
|
description: msg`Created`,
|
||||||
value: createdValue,
|
value: DateTime.fromJSDate(document.createdAt)
|
||||||
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
|
.toFormat('MMMM d, yyyy'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: msg`Last modified`,
|
description: msg`Last modified`,
|
||||||
value: lastModifiedValue,
|
value: DateTime.fromJSDate(document.updatedAt)
|
||||||
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
|
.toRelative(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, document, locale, userId]);
|
}, [isMounted, document, userId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
|
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
|
|||||||
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 { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
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 { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
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, TeamEmail } from '@documenso/prisma/client';
|
||||||
|
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
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';
|
||||||
@@ -39,7 +41,7 @@ export type DocumentPageViewProps = {
|
|||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
team?: Team & { teamEmail: TeamEmail | null };
|
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
||||||
@@ -62,11 +64,35 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (document?.teamId && !team?.url) {
|
||||||
|
redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentVisibility = document?.visibility;
|
||||||
|
const currentTeamMemberRole = team?.currentTeamMember?.role;
|
||||||
|
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
|
||||||
|
let canAccessDocument = true;
|
||||||
|
|
||||||
|
if (team && !isRecipient) {
|
||||||
|
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||||
|
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
|
||||||
|
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.otherwise(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
const isDocumentHistoryEnabled = await getServerComponentFlag(
|
const isDocumentHistoryEnabled = await getServerComponentFlag(
|
||||||
'app_document_page_view_history_sheet',
|
'app_document_page_view_history_sheet',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!document || !document.documentData) {
|
if (!document || !document.documentData || (team && !canAccessDocument)) {
|
||||||
|
redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (team && !canAccessDocument) {
|
||||||
redirect(documentRootPath);
|
redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,20 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSigningOrderForDocument } =
|
||||||
|
trpc.document.setSigningOrderForDocument.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
id: initialDocument.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newFields) => {
|
onSuccess: (newFields) => {
|
||||||
@@ -177,6 +191,7 @@ export const EditDocumentForm = ({
|
|||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
|
visibility: data.visibility,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? null,
|
||||||
},
|
},
|
||||||
@@ -204,15 +219,22 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await addSigners({
|
await Promise.all([
|
||||||
documentId: document.id,
|
setSigningOrderForDocument({
|
||||||
teamId: team?.id,
|
documentId: document.id,
|
||||||
signers: data.signers.map((signer) => ({
|
signingOrder: data.signingOrder,
|
||||||
...signer,
|
}),
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
|
||||||
actionAuth: signer.actionAuth || null,
|
addSigners({
|
||||||
})),
|
documentId: document.id,
|
||||||
});
|
teamId: team?.id,
|
||||||
|
signers: data.signers.map((signer) => ({
|
||||||
|
...signer,
|
||||||
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
|
actionAuth: signer.actionAuth || null,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
// Router refresh is here to clear the router cache for when navigating to /documents.
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -339,6 +361,7 @@ export const EditDocumentForm = ({
|
|||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.settings}
|
documentFlow={documentFlow.settings}
|
||||||
document={document}
|
document={document}
|
||||||
|
currentTeamMemberRole={team?.currentTeamMember?.role}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
isDocumentEnterprise={isDocumentEnterprise}
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
@@ -350,6 +373,7 @@ export const EditDocumentForm = ({
|
|||||||
key={recipients.length}
|
key={recipients.length}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
|
signingOrder={document.documentMeta?.signingOrder}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
isDocumentEnterprise={isDocumentEnterprise}
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { Plural, Trans } from '@lingui/macro';
|
import { Plural, Trans } from '@lingui/macro';
|
||||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
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 { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||||
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
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 type { Team } from '@documenso/prisma/client';
|
import type { Team } from '@documenso/prisma/client';
|
||||||
|
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
||||||
@@ -21,7 +24,7 @@ export type DocumentEditPageViewProps = {
|
|||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
team?: Team;
|
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
|
export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
|
||||||
@@ -43,10 +46,34 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (document?.teamId && !team?.url) {
|
||||||
|
redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentVisibility = document?.visibility;
|
||||||
|
const currentTeamMemberRole = team?.currentTeamMember?.role;
|
||||||
|
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
|
||||||
|
let canAccessDocument = true;
|
||||||
|
|
||||||
|
if (!isRecipient) {
|
||||||
|
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||||
|
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||||
|
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
|
||||||
|
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
|
||||||
|
.otherwise(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
redirect(documentRootPath);
|
redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (team && !canAccessDocument) {
|
||||||
|
redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (document.status === InternalDocumentStatus.COMPLETED) {
|
if (document.status === InternalDocumentStatus.COMPLETED) {
|
||||||
redirect(`${documentRootPath}/${documentId}`);
|
redirect(`${documentRootPath}/${documentId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
export type DocumentLogsDataTableProps = {
|
export type DocumentLogsDataTableProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
@@ -32,7 +30,7 @@ const dateFormat: DateTimeFormatOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
|
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@@ -78,7 +76,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
|||||||
{
|
{
|
||||||
header: _(msg`Time`),
|
header: _(msg`Time`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`User`),
|
header: _(msg`User`),
|
||||||
@@ -106,9 +104,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
|||||||
header: _(msg`Action`),
|
header: _(msg`Action`),
|
||||||
accessorKey: 'type',
|
accessorKey: 'type',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
|
||||||
{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}
|
|
||||||
</span>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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';
|
||||||
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 { 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';
|
||||||
@@ -32,9 +31,7 @@ export type DocumentLogsPageViewProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
|
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const locale = getLocale();
|
|
||||||
|
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
@@ -87,13 +84,13 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
{
|
{
|
||||||
description: msg`Date created`,
|
description: msg`Date created`,
|
||||||
value: DateTime.fromJSDate(document.createdAt)
|
value: DateTime.fromJSDate(document.createdAt)
|
||||||
.setLocale(locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: msg`Last updated`,
|
description: msg`Last updated`,
|
||||||
value: DateTime.fromJSDate(document.updatedAt)
|
value: DateTime.fromJSDate(document.updatedAt)
|
||||||
.setLocale(locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
|
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { DataTableActionButton } from './data-table-action-button';
|
import { DataTableActionButton } from './data-table-action-button';
|
||||||
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||||
@@ -41,8 +40,9 @@ export const DocumentsDataTable = ({
|
|||||||
showSenderColumn,
|
showSenderColumn,
|
||||||
team,
|
team,
|
||||||
}: DocumentsDataTableProps) => {
|
}: DocumentsDataTableProps) => {
|
||||||
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -53,12 +53,8 @@ export const DocumentsDataTable = ({
|
|||||||
{
|
{
|
||||||
header: _(msg`Created`),
|
header: _(msg`Created`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) =>
|
||||||
<LocaleDate
|
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
|
||||||
date={row.original.createdAt}
|
|
||||||
format={{ ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Title`),
|
header: _(msg`Title`),
|
||||||
@@ -88,8 +84,7 @@ export const DocumentsDataTable = ({
|
|||||||
{
|
{
|
||||||
header: _(msg`Actions`),
|
header: _(msg`Actions`),
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
(!row.original.deletedAt ||
|
(!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
||||||
row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<DataTableActionButton team={team} row={row.original} />
|
<DataTableActionButton team={team} row={row.original} />
|
||||||
<DataTableActionDropdown team={team} row={row.original} />
|
<DataTableActionDropdown team={team} row={row.original} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stat
|
|||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Team, TeamEmail } from '@documenso/prisma/client';
|
import type { Team, TeamEmail, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
@@ -33,7 +33,7 @@ export type DocumentsPageViewProps = {
|
|||||||
perPage?: string;
|
perPage?: string;
|
||||||
senderIds?: string;
|
senderIds?: string;
|
||||||
};
|
};
|
||||||
team?: Team & { teamEmail?: TeamEmail | null };
|
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||||
@@ -47,6 +47,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
const currentTeam = team
|
const currentTeam = team
|
||||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const currentTeamMemberRole = team?.currentTeamMember?.role;
|
||||||
|
|
||||||
const getStatOptions: GetStatsInput = {
|
const getStatOptions: GetStatsInput = {
|
||||||
user,
|
user,
|
||||||
@@ -58,6 +59,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
teamEmail: team.teamEmail?.email,
|
teamEmail: team.teamEmail?.email,
|
||||||
senderIds,
|
senderIds,
|
||||||
|
currentTeamMemberRole,
|
||||||
|
currentUserEmail: user.email,
|
||||||
|
userId: user.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import { type Stripe } from '@documenso/lib/server-only/stripe';
|
|||||||
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
|
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { BillingPlans } from './billing-plans';
|
import { BillingPlans } from './billing-plans';
|
||||||
import { BillingPortalButton } from './billing-portal-button';
|
import { BillingPortalButton } from './billing-portal-button';
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
setupI18nSSR();
|
const { i18n } = setupI18nSSR();
|
||||||
|
|
||||||
let { user } = await getRequiredServerComponentSession();
|
let { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
@@ -104,12 +102,12 @@ export default async function BillingSettingsPage() {
|
|||||||
{subscription.cancelAtPeriodEnd ? (
|
{subscription.cancelAtPeriodEnd ? (
|
||||||
<span>
|
<span>
|
||||||
end on{' '}
|
end on{' '}
|
||||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
automatically renew on{' '}
|
automatically renew on{' '}
|
||||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -20,15 +20,13 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
const dateFormat: DateTimeFormatOptions = {
|
const dateFormat: DateTimeFormatOptions = {
|
||||||
...DateTime.DATETIME_SHORT,
|
...DateTime.DATETIME_SHORT,
|
||||||
hourCycle: 'h12',
|
hourCycle: 'h12',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserSecurityActivityDataTable = () => {
|
export const UserSecurityActivityDataTable = () => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -71,7 +69,7 @@ export const UserSecurityActivityDataTable = () => {
|
|||||||
{
|
{
|
||||||
header: _(msg`Date`),
|
header: _(msg`Date`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Device`),
|
header: _(msg`Device`),
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const UserPasskeysDataTable = () => {
|
|||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.lastUsedAt
|
row.original.lastUsedAt
|
||||||
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
|
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
|
||||||
: msg`Never`,
|
: _(msg`Never`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-use
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
import { ApiTokenForm } from '~/components/forms/token';
|
import { ApiTokenForm } from '~/components/forms/token';
|
||||||
|
|
||||||
export default async function ApiTokensPage() {
|
export default async function ApiTokensPage() {
|
||||||
setupI18nSSR();
|
const { i18n } = setupI18nSSR();
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
@@ -65,13 +64,11 @@ export default async function ApiTokensPage() {
|
|||||||
<h5 className="text-base">{token.name}</h5>
|
<h5 className="text-base">{token.name}</h5>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
<p className="text-muted-foreground mt-2 text-xs">
|
||||||
<Trans>Created on</Trans>{' '}
|
<Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||||
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
|
||||||
</p>
|
</p>
|
||||||
{token.expires ? (
|
{token.expires ? (
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
<Trans>Expires on</Trans>{' '}
|
<Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
|
||||||
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
||||||
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
export default function WebhookPage() {
|
export default function WebhookPage() {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
|
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
|
||||||
|
|
||||||
@@ -86,10 +85,7 @@ export default function WebhookPage() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
<p className="text-muted-foreground mt-2 text-xs">
|
||||||
<Trans>
|
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||||
Created on{' '}
|
|
||||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
|
||||||
</Trans>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ export const EditTemplateForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSigningOrderForTemplate } =
|
||||||
|
trpc.template.setSigningOrderForTemplate.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.template.getTemplateWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
id: initialTemplate.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
@@ -160,11 +173,19 @@ export const EditTemplateForm = ({
|
|||||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await addTemplateSigners({
|
await Promise.all([
|
||||||
templateId: template.id,
|
setSigningOrderForTemplate({
|
||||||
teamId: team?.id,
|
templateId: template.id,
|
||||||
signers: data.signers,
|
teamId: team?.id,
|
||||||
});
|
signingOrder: data.signingOrder,
|
||||||
|
}),
|
||||||
|
|
||||||
|
addTemplateSigners({
|
||||||
|
templateId: template.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
signers: data.signers,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
// Router refresh is here to clear the router cache for when navigating to /documents.
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -262,6 +283,7 @@ export const EditTemplateForm = ({
|
|||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
signingOrder={template.templateMeta?.signingOrder}
|
||||||
templateDirectLink={template.directLink}
|
templateDirectLink={template.directLink}
|
||||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||||
isEnterprise={isEnterprise}
|
isEnterprise={isEnterprise}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
|
|||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
import { TemplateType } from '~/components/formatter/template-type';
|
import { TemplateType } from '~/components/formatter/template-type';
|
||||||
|
|
||||||
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||||
@@ -48,7 +47,7 @@ export const TemplatesDataTable = ({
|
|||||||
|
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
const { remaining } = useLimits();
|
const { remaining } = useLimits();
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
@@ -56,7 +55,7 @@ export const TemplatesDataTable = ({
|
|||||||
{
|
{
|
||||||
header: _(msg`Created`),
|
header: _(msg`Created`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Title`),
|
header: _(msg`Title`),
|
||||||
@@ -81,8 +80,8 @@ export const TemplatesDataTable = ({
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Public templates are connected to your public profile. Any modifications
|
Public templates are connected to your public profile. Any modifications to
|
||||||
to public templates will also appear in your public profile.
|
public templates will also appear in your public profile.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@@ -94,9 +93,9 @@ export const TemplatesDataTable = ({
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Direct link templates contain one dynamic recipient placeholder. Anyone
|
Direct link templates contain one dynamic recipient placeholder. Anyone with
|
||||||
with access to this link can sign the document, and it will then appear
|
access to this link can sign the document, and it will then appear on your
|
||||||
on your documents page.
|
documents page.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@@ -109,8 +108,8 @@ export const TemplatesDataTable = ({
|
|||||||
<p>
|
<p>
|
||||||
{teamId ? (
|
{teamId ? (
|
||||||
<Trans>
|
<Trans>
|
||||||
Team only templates are not linked anywhere and are visible only to
|
Team only templates are not linked anywhere and are visible only to your
|
||||||
your team.
|
team.
|
||||||
</Trans>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans>Private templates can only be modified and viewed by you.</Trans>
|
<Trans>Private templates can only be modified and viewed by you.</Trans>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
|
|||||||
import type { DateTimeFormatOptions } from 'luxon';
|
import type { DateTimeFormatOptions } from 'luxon';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import {
|
import {
|
||||||
@@ -15,8 +16,6 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@documenso/ui/primitives/table';
|
} from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
export type AuditLogDataTableProps = {
|
export type AuditLogDataTableProps = {
|
||||||
logs: TDocumentAuditLog[];
|
logs: TDocumentAuditLog[];
|
||||||
};
|
};
|
||||||
@@ -49,7 +48,9 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
|
|||||||
{logs.map((log, i) => (
|
{logs.map((log, i) => (
|
||||||
<TableRow className="break-inside-avoid" key={i}>
|
<TableRow className="break-inside-avoid" key={i}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<LocaleDate format={dateFormat} date={log.createdAt} />
|
{DateTime.fromJSDate(log.createdAt)
|
||||||
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
|
.toLocaleString(dateFormat)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||||
@@ -10,7 +12,6 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
|||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { AuditLogDataTable } from './data-table';
|
import { AuditLogDataTable } from './data-table';
|
||||||
|
|
||||||
@@ -21,8 +22,6 @@ type AuditLogProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function AuditLog({ searchParams }: AuditLogProps) {
|
export default async function AuditLog({ searchParams }: AuditLogProps) {
|
||||||
setupI18nSSR();
|
|
||||||
|
|
||||||
const { d } = searchParams;
|
const { d } = searchParams;
|
||||||
|
|
||||||
if (typeof d !== 'string' || !d) {
|
if (typeof d !== 'string' || !d) {
|
||||||
@@ -89,7 +88,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
|||||||
<span className="font-medium">Created At</span>
|
<span className="font-medium">Created At</span>
|
||||||
|
|
||||||
<span className="mt-1 block">
|
<span className="mt-1 block">
|
||||||
<LocaleDate date={document.createdAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
{DateTime.fromJSDate(document.createdAt)
|
||||||
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
|
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -97,7 +98,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
|||||||
<span className="font-medium">Last Updated</span>
|
<span className="font-medium">Last Updated</span>
|
||||||
|
|
||||||
<span className="mt-1 block">
|
<span className="mt-1 block">
|
||||||
<LocaleDate date={document.updatedAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
{DateTime.fromJSDate(document.updatedAt)
|
||||||
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
|
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React from 'react';
|
|||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
RECIPIENT_ROLES_DESCRIPTION_ENG,
|
RECIPIENT_ROLES_DESCRIPTION_ENG,
|
||||||
RECIPIENT_ROLE_SIGNING_REASONS_ENG,
|
RECIPIENT_ROLE_SIGNING_REASONS_ENG,
|
||||||
@@ -27,7 +28,6 @@ import {
|
|||||||
} from '@documenso/ui/primitives/table';
|
} from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
type SigningCertificateProps = {
|
type SigningCertificateProps = {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
@@ -41,8 +41,6 @@ const FRIENDLY_SIGNING_REASONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
|
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
|
||||||
setupI18nSSR();
|
|
||||||
|
|
||||||
const { d } = searchParams;
|
const { d } = searchParams;
|
||||||
|
|
||||||
if (typeof d !== 'string' || !d) {
|
if (typeof d !== 'string' || !d) {
|
||||||
@@ -231,42 +229,33 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
|
|||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-muted-foreground text-sm print:text-xs">
|
||||||
<span className="font-medium">Sent:</span>{' '}
|
<span className="font-medium">Sent:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.EMAIL_SENT[0] ? (
|
{logs.EMAIL_SENT[0]
|
||||||
<LocaleDate
|
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||||
date={logs.EMAIL_SENT[0].createdAt}
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
/>
|
: 'Unknown'}
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-muted-foreground text-sm print:text-xs">
|
||||||
<span className="font-medium">Viewed:</span>{' '}
|
<span className="font-medium">Viewed:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_OPENED[0] ? (
|
{logs.DOCUMENT_OPENED[0]
|
||||||
<LocaleDate
|
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||||
date={logs.DOCUMENT_OPENED[0].createdAt}
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
/>
|
: 'Unknown'}
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm print:text-xs">
|
<p className="text-muted-foreground text-sm print:text-xs">
|
||||||
<span className="font-medium">Signed:</span>{' '}
|
<span className="font-medium">Signed:</span>{' '}
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] ? (
|
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||||
<LocaleDate
|
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||||
date={logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt}
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||||
/>
|
: 'Unknown'}
|
||||||
) : (
|
|
||||||
'Unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ export const CheckboxField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-1">
|
||||||
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
|
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
|
||||||
const itemValue = item.value || `empty-value-${item.id}`;
|
const itemValue = item.value || `empty-value-${item.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center gap-x-1.5">
|
<div key={index} className="flex items-center gap-x-1.5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="h-4 w-4"
|
className="h-3 w-3"
|
||||||
checkClassName="text-white"
|
checkClassName="text-white"
|
||||||
id={`checkbox-${index}`}
|
id={`checkbox-${index}`}
|
||||||
checked={field.customText
|
checked={field.customText
|
||||||
@@ -283,7 +283,7 @@ export const CheckboxField = ({
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={`checkbox-${index}`}>
|
<Label htmlFor={`checkbox-${index}`} className="text-xs">
|
||||||
{item.value.includes('empty-value-') ? '' : item.value}
|
{item.value.includes('empty-value-') ? '' : item.value}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -204,25 +204,29 @@ export default async function CompletedSigningPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canSignUp && (
|
<div className="flex flex-col items-center">
|
||||||
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
|
{canSignUp && (
|
||||||
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
|
<div className="flex max-w-xl flex-col items-center justify-center p-4 md:p-12">
|
||||||
<Trans>Need to sign documents?</Trans>
|
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
|
||||||
</h2>
|
<Trans>Need to sign documents?</Trans>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
|
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
|
||||||
<Trans>Create your account and start using state-of-the-art document signing.</Trans>
|
<Trans>
|
||||||
</p>
|
Create your account and start using state-of-the-art document signing.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoggedIn && (
|
{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">
|
||||||
<Trans>Go Back Home</Trans>
|
<Trans>Go Back Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PollUntilDocumentCompleted document={document} />
|
<PollUntilDocumentCompleted document={document} />
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export const DateField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 text-sm duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{localDateString}
|
{localDateString}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
@@ -25,22 +25,19 @@ export const DocumentActionAuthAccount = ({
|
|||||||
}: DocumentActionAuthAccountProps) => {
|
}: DocumentActionAuthAccountProps) => {
|
||||||
const { recipient } = useRequiredDocumentAuthContext();
|
const { recipient } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
const handleChangeAccount = async (email: string) => {
|
const handleChangeAccount = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
const encryptedEmail = await encryptSecondaryData({
|
await signOut({
|
||||||
data: email,
|
redirect: false,
|
||||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await signOut({
|
router.push(`/signin#email=${email}`);
|
||||||
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const DropdownField = ({
|
|||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalChoice(parsedFieldMeta.defaultValue ?? '');
|
setLocalChoice('');
|
||||||
startTransition(() => router.refresh());
|
startTransition(() => router.refresh());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -179,7 +179,7 @@ export const DropdownField = ({
|
|||||||
|
|
||||||
{!field.inserted && (
|
{!field.inserted && (
|
||||||
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
|
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
|
||||||
<Select value={parsedFieldMeta.defaultValue} onValueChange={handleSelectItem}>
|
<Select value={localChoice} onValueChange={handleSelectItem}>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0',
|
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0',
|
||||||
@@ -189,7 +189,10 @@ export const DropdownField = ({
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectValue placeholder={`-- ${_(msg`Select`)} --`} />
|
<SelectValue
|
||||||
|
className="text-[clamp(0.625rem,1cqw,0.825rem)]"
|
||||||
|
placeholder={`${_(msg`Select`)}`}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
|
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
|
||||||
{parsedFieldMeta?.values?.map((item, index) => (
|
{parsedFieldMeta?.values?.map((item, index) => (
|
||||||
@@ -203,7 +206,7 @@ export const DropdownField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -29,9 +29,16 @@ export type SigningFormProps = {
|
|||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
redirectUrl?: string | null;
|
redirectUrl?: string | null;
|
||||||
|
isRecipientsTurn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
|
export const SigningForm = ({
|
||||||
|
document,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
redirectUrl,
|
||||||
|
isRecipientsTurn,
|
||||||
|
}: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -150,6 +157,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
|
disabled={!isRecipientsTurn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +221,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
|
disabled={!isRecipientsTurn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const InitialsField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 duration-200">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
|
||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -267,7 +267,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add number</Trans>}
|
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
|||||||
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 { 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 { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||||
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';
|
||||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
@@ -42,6 +43,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
||||||
|
|
||||||
|
if (!isRecipientsTurn) {
|
||||||
|
return redirect(`/sign/${token}/waiting`);
|
||||||
|
}
|
||||||
|
|
||||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
@@ -146,6 +153,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
document={document}
|
document={document}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
completedFields={completedFields}
|
completedFields={completedFields}
|
||||||
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
|
|||||||
@@ -173,16 +173,16 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<RadioGroup>
|
<RadioGroup className="gap-y-1">
|
||||||
{values?.map((item, index) => (
|
{values?.map((item, index) => (
|
||||||
<div key={index} className="flex items-center gap-x-1.5">
|
<div key={index} className="flex items-center gap-x-1.5">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
className=""
|
className="h-3 w-3"
|
||||||
value={item.value}
|
value={item.value}
|
||||||
id={`option-${index}`}
|
id={`option-${index}`}
|
||||||
checked={item.value === field.customText}
|
checked={item.value === field.customText}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={`option-${index}`}>
|
<Label htmlFor={`option-${index}`} className="text-xs">
|
||||||
{item.value.includes('empty-value-') ? '' : item.value}
|
{item.value.includes('empty-value-') ? '' : item.value}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type SignDialogProps = {
|
|||||||
fieldsValidated: () => void | Promise<void>;
|
fieldsValidated: () => void | Promise<void>;
|
||||||
onSignatureComplete: () => void | Promise<void>;
|
onSignatureComplete: () => void | Promise<void>;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDialog = ({
|
export const SignDialog = ({
|
||||||
@@ -32,6 +33,7 @@ export const SignDialog = ({
|
|||||||
fieldsValidated,
|
fieldsValidated,
|
||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
role,
|
role,
|
||||||
|
disabled = false,
|
||||||
}: SignDialogProps) => {
|
}: SignDialogProps) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const truncatedTitle = truncateTitle(documentTitle);
|
const truncatedTitle = truncateTitle(documentTitle);
|
||||||
@@ -54,6 +56,7 @@ export const SignDialog = ({
|
|||||||
size="lg"
|
size="lg"
|
||||||
onClick={fieldsValidated}
|
onClick={fieldsValidated}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -20,24 +20,19 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
const handleChangeAccount = async (email: string) => {
|
const handleChangeAccount = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
const encryptedEmail = await encryptSecondaryData({
|
await signOut({
|
||||||
data: email,
|
redirect: false,
|
||||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await signOut({
|
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
||||||
callbackUrl: emailHasAccount
|
|
||||||
? `/signin?email=${encodeURIComponent(encryptedEmail)}`
|
|
||||||
: `/signup?email=${encodeURIComponent(encryptedEmail)}`,
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const SigningFieldContainer = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(type === 'Checkbox' ? 'group' : '')}>
|
<div className={cn('[container-type:size]', type === 'Checkbox' ? 'group' : '')}>
|
||||||
<FieldRootContainer field={field}>
|
<FieldRootContainer field={field}>
|
||||||
{!field.inserted && !loading && !readOnlyField && (
|
{!field.inserted && !loading && !readOnlyField && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type SigningPageViewProps = {
|
|||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
completedFields: CompletedField[];
|
completedFields: CompletedField[];
|
||||||
|
isRecipientsTurn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningPageView = ({
|
export const SigningPageView = ({
|
||||||
@@ -46,6 +47,7 @@ export const SigningPageView = ({
|
|||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
completedFields,
|
completedFields,
|
||||||
|
isRecipientsTurn,
|
||||||
}: SigningPageViewProps) => {
|
}: SigningPageViewProps) => {
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ export const SigningPageView = ({
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
redirectUrl={documentMeta?.redirectUrl}
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
|||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-x-1">
|
<span className="flex items-center justify-center gap-x-1">
|
||||||
<Type />
|
<Type />
|
||||||
{fieldDisplayName || <Trans>Add text</Trans>}
|
{fieldDisplayName || <Trans>Text</Trans>}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -269,7 +269,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
|||||||
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
|
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add Text</Trans>}
|
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Text</Trans>}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
100
apps/web/src/app/(signing)/sign/[token]/waiting/page.tsx
Normal file
100
apps/web/src/app/(signing)/sign/[token]/waiting/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||||
|
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
import type { Team } from '@documenso/prisma/client';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
type WaitingForTurnToSignPageProps = {
|
||||||
|
params: { token?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function WaitingForTurnToSignPage({
|
||||||
|
params: { token },
|
||||||
|
}: WaitingForTurnToSignPageProps) {
|
||||||
|
setupI18nSSR();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = await getServerComponentSession();
|
||||||
|
|
||||||
|
const [document, recipient] = await Promise.all([
|
||||||
|
getDocumentAndSenderByToken({ token }).catch(() => null),
|
||||||
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!document || !recipient) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status === DocumentStatus.COMPLETED) {
|
||||||
|
return redirect(`/sign/${token}/complete`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isOwnerOrTeamMember = false;
|
||||||
|
|
||||||
|
let team: Team | null = null;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
isOwnerOrTeamMember = await getDocumentById({
|
||||||
|
id: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: document.teamId ?? undefined,
|
||||||
|
})
|
||||||
|
.then((document) => !!document)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (document.teamId) {
|
||||||
|
team = await getTeamById({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: document.teamId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-full max-w-md text-center">
|
||||||
|
<h2 className="tracking-tigh text-3xl font-bold">
|
||||||
|
<Trans>Waiting for Your Turn</Trans>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>
|
||||||
|
It's currently not your turn to sign. You will receive an email with instructions once
|
||||||
|
it's your turn to sign the document.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-4 text-sm">
|
||||||
|
<Trans>Please check your email for updates.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
{isOwnerOrTeamMember ? (
|
||||||
|
<Button variant="link" asChild>
|
||||||
|
<Link href={`${formatDocumentsPath(team?.url)}/${document.id}`}>
|
||||||
|
<Trans>Were you trying to edit this document instead?</Trans>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="link" asChild>
|
||||||
|
<Link href="/documents">Return Home</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
import { ApiTokenForm } from '~/components/forms/token';
|
import { ApiTokenForm } from '~/components/forms/token';
|
||||||
|
|
||||||
type ApiTokensPageProps = {
|
type ApiTokensPageProps = {
|
||||||
@@ -22,7 +21,7 @@ type ApiTokensPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||||
setupI18nSSR();
|
const { i18n } = setupI18nSSR();
|
||||||
|
|
||||||
const { teamUrl } = params;
|
const { teamUrl } = params;
|
||||||
|
|
||||||
@@ -98,13 +97,17 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
|||||||
<h5 className="text-base">{token.name}</h5>
|
<h5 className="text-base">{token.name}</h5>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
<p className="text-muted-foreground mt-2 text-xs">
|
||||||
<Trans>Created on</Trans>{' '}
|
<Trans>
|
||||||
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
Created on
|
||||||
|
{i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
{token.expires ? (
|
{token.expires ? (
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
<Trans>Expires on</Trans>{' '}
|
<Trans>
|
||||||
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
Expires on
|
||||||
|
{i18n.date(token.expires, DateTime.DATETIME_FULL)}
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
||||||
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export default function WebhookPage() {
|
export default function WebhookPage() {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
@@ -91,10 +90,7 @@ export default function WebhookPage() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
<p className="text-muted-foreground mt-2 text-xs">
|
||||||
<Trans>
|
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||||
Created on{' '}
|
|
||||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
|
||||||
</Trans>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
IS_OIDC_SSO_ENABLED,
|
IS_OIDC_SSO_ENABLED,
|
||||||
OIDC_PROVIDER_LABEL,
|
OIDC_PROVIDER_LABEL,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
|
||||||
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
|
|
||||||
@@ -19,24 +17,11 @@ export const metadata: Metadata = {
|
|||||||
title: 'Sign In',
|
title: 'Sign In',
|
||||||
};
|
};
|
||||||
|
|
||||||
type SignInPageProps = {
|
export default function SignInPage() {
|
||||||
searchParams: {
|
|
||||||
email?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SignInPage({ searchParams }: SignInPageProps) {
|
|
||||||
setupI18nSSR();
|
setupI18nSSR();
|
||||||
|
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||||
|
|
||||||
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
|
|
||||||
const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
|
|
||||||
|
|
||||||
if (!email && rawEmail) {
|
|
||||||
redirect('/signin');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
|
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
|
||||||
@@ -50,7 +35,6 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
|||||||
<hr className="-mx-6 my-4" />
|
<hr className="-mx-6 my-4" />
|
||||||
|
|
||||||
<SignInForm
|
<SignInForm
|
||||||
initialEmail={email || undefined}
|
|
||||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||||
oidcProviderLabel={OIDC_PROVIDER_LABEL}
|
oidcProviderLabel={OIDC_PROVIDER_LABEL}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { env } from 'next-runtime-env';
|
|||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
|
||||||
|
|
||||||
import { SignUpFormV2 } from '~/components/forms/v2/signup';
|
import { SignUpFormV2 } from '~/components/forms/v2/signup';
|
||||||
|
|
||||||
@@ -13,13 +12,7 @@ export const metadata: Metadata = {
|
|||||||
title: 'Sign Up',
|
title: 'Sign Up',
|
||||||
};
|
};
|
||||||
|
|
||||||
type SignUpPageProps = {
|
export default function SignUpPage() {
|
||||||
searchParams: {
|
|
||||||
email?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
|
||||||
setupI18nSSR();
|
setupI18nSSR();
|
||||||
|
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||||
@@ -28,17 +21,9 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||||||
redirect('/signin');
|
redirect('/signin');
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
|
|
||||||
const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
|
|
||||||
|
|
||||||
if (!email && rawEmail) {
|
|
||||||
redirect('/signup');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignUpFormV2
|
<SignUpFormV2
|
||||||
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
|
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
|
||||||
initialEmail={email || undefined}
|
|
||||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ZBaseEmbedDataSchema = z.object({
|
export const ZBaseEmbedDataSchema = z.object({
|
||||||
css: z.string().optional().transform(value => value || undefined),
|
css: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value || undefined),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,15 +18,18 @@ export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentComplet
|
|||||||
|
|
||||||
<div className="mt-8 w-full max-w-md">
|
<div className="mt-8 w-full max-w-md">
|
||||||
<SigningCard3D
|
<SigningCard3D
|
||||||
className='w-full mx-auto'
|
className="mx-auto w-full"
|
||||||
name={name || 'Documenso'}
|
name={name || 'Documenso'}
|
||||||
signature={signature}
|
signature={signature}
|
||||||
signingCelebrationImage={signingCelebration}
|
signingCelebrationImage={signingCelebration}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
|
<p className="text-muted-foreground mt-8 max-w-[50ch] text-center text-sm">
|
||||||
<Trans>The document is now completed, please follow any instructions provided within the parent application.</Trans>
|
<Trans>
|
||||||
|
The document is now completed, please follow any instructions provided within the parent
|
||||||
|
application.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation';
|
|||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
@@ -14,7 +15,7 @@ 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 { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { FieldType, type DocumentData, type Field } from '@documenso/prisma/client';
|
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
@@ -34,7 +35,6 @@ import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-
|
|||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
@@ -307,7 +307,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
|
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<LazyPDFViewer
|
<LazyPDFViewer
|
||||||
@@ -318,26 +318,26 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
|
|
||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="w-full border-border bg-widget flex md:min-h-[min(calc(100dvh-2rem),48rem)] h-fit flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-x-2">
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<LucideChevronDown
|
<LucideChevronDown
|
||||||
className="h-5 w-5 text-muted-foreground"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LucideChevronUp
|
<LucideChevronUp
|
||||||
className="h-5 w-5 text-muted-foreground"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(true)}
|
onClick={() => setIsExpanded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -354,7 +354,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
|
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">
|
<Label htmlFor="full-name">
|
||||||
@@ -408,9 +408,9 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
|
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||||
|
|
||||||
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
|
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||||
{pendingFields.length > 0 ? (
|
{pendingFields.length > 0 ? (
|
||||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||||
<Trans>Next</Trans>
|
<Trans>Next</Trans>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function EmbedDirectTemplateNotFound() {
|
export default function EmbedDirectTemplateNotFound() {
|
||||||
return <div>Not Found</div>
|
return <div>Not Found</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,11 +73,7 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
|
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningProvider
|
<SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
|
||||||
email={user?.email}
|
|
||||||
fullName={user?.name}
|
|
||||||
signature={user?.signature}
|
|
||||||
>
|
|
||||||
<DocumentAuthProvider
|
<DocumentAuthProvider
|
||||||
documentAuthOptions={template.authOptions}
|
documentAuthOptions={template.authOptions}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { FieldType, type Field } from '@documenso/prisma/client';
|
import { type Field, FieldType } from '@documenso/prisma/client';
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const EmbedPaywall = () => {
|
export const EmbedPaywall = () => {
|
||||||
return <div>
|
return (
|
||||||
<h1>Paywall</h1>
|
<div>
|
||||||
</div>
|
<h1>Paywall</h1>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans, msg } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { type DocumentData, type Field } from '@documenso/prisma/client';
|
import { type DocumentData, type Field } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
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';
|
||||||
@@ -20,9 +22,9 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
@@ -185,7 +187,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
|
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<LazyPDFViewer
|
<LazyPDFViewer
|
||||||
@@ -196,26 +198,26 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="w-full border-border bg-widget flex flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-x-2">
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<LucideChevronDown
|
<LucideChevronDown
|
||||||
className="h-5 w-5 text-muted-foreground"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LucideChevronUp
|
<LucideChevronUp
|
||||||
className="h-5 w-5 text-muted-foreground"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(true)}
|
onClick={() => setIsExpanded(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -232,7 +234,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
|
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">
|
<Label htmlFor="full-name">
|
||||||
@@ -285,9 +287,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
|
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||||
|
|
||||||
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
|
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||||
{pendingFields.length > 0 ? (
|
{pendingFields.length > 0 ? (
|
||||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||||
<Trans>Next</Trans>
|
<Trans>Next</Trans>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function EmbedDirectTemplateNotFound() {
|
export default function EmbedDirectTemplateNotFound() {
|
||||||
return <div>Not Found</div>
|
return <div>Not Found</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-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 { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||||
@@ -13,10 +17,6 @@ import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
|||||||
import { EmbedAuthenticateView } from '../../authenticate';
|
import { EmbedAuthenticateView } from '../../authenticate';
|
||||||
import { EmbedPaywall } from '../../paywall';
|
import { EmbedPaywall } from '../../paywall';
|
||||||
import { EmbedSignDocumentClientPage } from './client';
|
import { EmbedSignDocumentClientPage } from './client';
|
||||||
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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export type EmbedSignDocumentPageProps = {
|
export type EmbedSignDocumentPageProps = {
|
||||||
params: {
|
params: {
|
||||||
@@ -66,7 +66,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
return <EmbedAuthenticateView email={user?.email || recipient.email} returnTo={`/embed/direct/${token}`} />;
|
return (
|
||||||
|
<EmbedAuthenticateView
|
||||||
|
email={user?.email || recipient.email}
|
||||||
|
returnTo={`/embed/direct/${token}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { Caveat, Inter } from 'next/font/google';
|
import { Caveat, Inter } from 'next/font/google';
|
||||||
import { cookies, headers } from 'next/headers';
|
|
||||||
|
|
||||||
import { AxiomWebVitals } from 'next-axiom';
|
import { AxiomWebVitals } from 'next-axiom';
|
||||||
import { PublicEnvScript } from 'next-runtime-env';
|
import { PublicEnvScript } from 'next-runtime-env';
|
||||||
@@ -9,12 +8,8 @@ import { PublicEnvScript } from 'next-runtime-env';
|
|||||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||||
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
|
|
||||||
import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
|
||||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
|
||||||
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||||
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||||
@@ -61,32 +56,7 @@ export function generateMetadata() {
|
|||||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const flags = await getServerComponentAllFlags();
|
const flags = await getServerComponentAllFlags();
|
||||||
|
|
||||||
const locale = getLocale();
|
const { i18n, lang, locales } = setupI18nSSR();
|
||||||
|
|
||||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
|
||||||
|
|
||||||
// Should be safe to remove when we upgrade NextJS.
|
|
||||||
// https://github.com/vercel/next.js/pull/65008
|
|
||||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
|
||||||
// during the same render.
|
|
||||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
|
||||||
if (!cookies().get('i18n')) {
|
|
||||||
const setCookieValue = headers().get('set-cookie');
|
|
||||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
|
||||||
|
|
||||||
if (i18nCookie) {
|
|
||||||
const i18n = i18nCookie.split('=')[1];
|
|
||||||
|
|
||||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable i18n for now until we get translations.
|
|
||||||
if (!IS_APP_WEB_I18N_ENABLED) {
|
|
||||||
overrideLang = 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -110,21 +80,22 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<LocaleProvider locale={locale}>
|
<FeatureFlagProvider initialFlags={flags}>
|
||||||
<FeatureFlagProvider initialFlags={flags}>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<TooltipProvider>
|
||||||
<TooltipProvider>
|
<TrpcProvider>
|
||||||
<TrpcProvider>
|
<I18nClientProvider
|
||||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
initialLocaleData={{ lang, locales }}
|
||||||
{children}
|
initialMessages={i18n.messages}
|
||||||
</I18nClientProvider>
|
>
|
||||||
</TrpcProvider>
|
{children}
|
||||||
</TooltipProvider>
|
</I18nClientProvider>
|
||||||
</ThemeProvider>
|
</TrpcProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
</LocaleProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { useRouter } from 'next/navigation';
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
DOCUMENTS_PAGE_SHORTCUT,
|
DOCUMENTS_PAGE_SHORTCUT,
|
||||||
SETTINGS_PAGE_SHORTCUT,
|
SETTINGS_PAGE_SHORTCUT,
|
||||||
@@ -20,7 +21,10 @@ 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 { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||||
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -31,6 +35,7 @@ import {
|
|||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from '@documenso/ui/primitives/command';
|
} from '@documenso/ui/primitives/command';
|
||||||
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const DOCUMENTS_PAGES = [
|
const DOCUMENTS_PAGES = [
|
||||||
{
|
{
|
||||||
@@ -207,6 +212,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
<Commands push={push} pages={SETTINGS_PAGES} />
|
<Commands push={push} pages={SETTINGS_PAGES} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
|
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
|
||||||
|
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
|
||||||
|
Change language
|
||||||
|
</CommandItem>
|
||||||
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
|
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
|
||||||
Change theme
|
Change theme
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
@@ -218,7 +226,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
||||||
|
{currentPage === 'language' && <LanguageCommands />}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
);
|
);
|
||||||
@@ -269,3 +279,46 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LanguageCommands = () => {
|
||||||
|
const { i18n, _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const setLanguage = async (lang: string) => {
|
||||||
|
if (isLoading || lang === i18n.locale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dynamicActivate(i18n, lang);
|
||||||
|
await switchI18NLanguage(lang);
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`An unknown error occurred`),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: _(msg`Unable to change the language at this time. Please try again later.`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
||||||
|
<CommandItem
|
||||||
|
disabled={isLoading}
|
||||||
|
key={language.full}
|
||||||
|
onSelect={async () => setLanguage(language.short)}
|
||||||
|
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn('mr-2 h-4 w-4', i18n.locale === language.short ? 'opacity-100' : 'opacity-0')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{language.full}
|
||||||
|
</CommandItem>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
|||||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||||
import type { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -41,6 +44,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
|
||||||
|
|
||||||
const isUserAdmin = isAdmin(user);
|
const isUserAdmin = isAdmin(user);
|
||||||
|
|
||||||
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
|
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
|
||||||
@@ -274,6 +279,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-muted-foreground px-4 py-2"
|
||||||
|
onClick={() => setLanguageSwitcherOpen(true)}
|
||||||
|
>
|
||||||
|
<Trans>Language</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive/90 hover:!text-destructive px-4 py-2"
|
className="text-destructive/90 hover:!text-destructive px-4 py-2"
|
||||||
onSelect={async () =>
|
onSelect={async () =>
|
||||||
@@ -285,6 +297,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
<Trans>Sign Out</Trans>
|
<Trans>Sign Out</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
|
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,12 +22,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
|
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
|
||||||
|
|
||||||
export const CurrentUserTeamsDataTable = () => {
|
export const CurrentUserTeamsDataTable = () => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@@ -91,7 +89,7 @@ export const CurrentUserTeamsDataTable = () => {
|
|||||||
{
|
{
|
||||||
header: _(msg`Member Since`),
|
header: _(msg`Member Since`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
|
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
|
||||||
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
|
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
|
||||||
|
|
||||||
export const PendingUserTeamsDataTable = () => {
|
export const PendingUserTeamsDataTable = () => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@@ -79,7 +77,7 @@ export const PendingUserTeamsDataTable = () => {
|
|||||||
{
|
{
|
||||||
header: _(msg`Created on`),
|
header: _(msg`Created on`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
|||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
export type TeamMemberInvitesDataTableProps = {
|
export type TeamMemberInvitesDataTableProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
};
|
};
|
||||||
@@ -37,7 +35,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||||
@@ -129,7 +127,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
|
|||||||
{
|
{
|
||||||
header: _(msg`Invited At`),
|
header: _(msg`Invited At`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Actions`),
|
header: _(msg`Actions`),
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ import {
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
|
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
|
||||||
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
|
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ export const TeamMembersDataTable = ({
|
|||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
}: TeamMembersDataTableProps) => {
|
}: TeamMembersDataTableProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@@ -114,7 +112,7 @@ export const TeamMembersDataTable = ({
|
|||||||
{
|
{
|
||||||
header: _(msg`Member Since`),
|
header: _(msg`Member Since`),
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: _(msg`Actions`),
|
header: _(msg`Actions`),
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { ArrowRightIcon, Loader } from 'lucide-react';
|
import { ArrowRightIcon, Loader } from 'lucide-react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
@@ -18,8 +20,6 @@ import { Badge } from '@documenso/ui/primitives/badge';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
|
||||||
|
|
||||||
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
|
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
|
||||||
|
|
||||||
export type DocumentHistorySheetProps = {
|
export type DocumentHistorySheetProps = {
|
||||||
@@ -37,6 +37,8 @@ export const DocumentHistorySheet = ({
|
|||||||
onMenuOpenChange,
|
onMenuOpenChange,
|
||||||
children,
|
children,
|
||||||
}: DocumentHistorySheetProps) => {
|
}: DocumentHistorySheetProps) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
|
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -153,7 +155,9 @@ export const DocumentHistorySheet = ({
|
|||||||
{formatDocumentAuditLogActionString(auditLog, userId)}
|
{formatDocumentAuditLogActionString(auditLog, userId)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-foreground/50 text-xs">
|
<p className="text-foreground/50 text-xs">
|
||||||
<LocaleDate date={auditLog.createdAt} format="d MMM, yyyy HH:MM a" />
|
{DateTime.fromJSDate(auditLog.createdAt)
|
||||||
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
|
.toFormat('d MMM, yyyy HH:MM a')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,6 +335,23 @@ export const DocumentHistorySheet = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
.with(
|
||||||
|
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
|
||||||
|
({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Old',
|
||||||
|
value: data.from,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'New',
|
||||||
|
value: data.to,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
|
|
||||||
{isUserDetailsVisible && (
|
{isUserDetailsVisible && (
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import type { DateTimeFormatOptions } from 'luxon';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
|
||||||
|
|
||||||
export type LocaleDateProps = HTMLAttributes<HTMLSpanElement> & {
|
|
||||||
date: string | number | Date;
|
|
||||||
format?: DateTimeFormatOptions | string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the date based on the user locale.
|
|
||||||
*
|
|
||||||
* Will use the estimated locale from the user headers on SSR, then will use
|
|
||||||
* the client browser locale once mounted.
|
|
||||||
*/
|
|
||||||
export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
|
|
||||||
const { locale } = useLocale();
|
|
||||||
|
|
||||||
const formatDateTime = useCallback(
|
|
||||||
(date: DateTime) => {
|
|
||||||
if (typeof format === 'string') {
|
|
||||||
return date.toFormat(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toLocaleString(format);
|
|
||||||
},
|
|
||||||
[format],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [localeDate, setLocaleDate] = useState(() =>
|
|
||||||
formatDateTime(DateTime.fromJSDate(new Date(date)).setLocale(locale)),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocaleDate(formatDateTime(DateTime.fromJSDate(new Date(date))));
|
|
||||||
}, [date, format, formatDateTime]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={className} {...props}>
|
|
||||||
{localeDate}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -307,6 +307,18 @@ export const SignInForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
|
||||||
|
const email = params.get('email');
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
form.setValue('email', email);
|
||||||
|
}
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -203,6 +203,18 @@ export const SignUpFormV2 = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
|
||||||
|
const email = params.get('email');
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
form.setValue('email', email);
|
||||||
|
}
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex justify-center gap-x-12', className)}>
|
<div className={cn('flex justify-center gap-x-12', className)}>
|
||||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
import { LocaleDate } from '../formatter/locale-date';
|
|
||||||
|
|
||||||
export type ManagePublicTemplateDialogProps = {
|
export type ManagePublicTemplateDialogProps = {
|
||||||
directTemplates: (Template & {
|
directTemplates: (Template & {
|
||||||
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
||||||
@@ -93,7 +91,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
onIsOpenChange,
|
onIsOpenChange,
|
||||||
...props
|
...props
|
||||||
}: ManagePublicTemplateDialogProps) => {
|
}: ManagePublicTemplateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [open, onOpenChange] = useState(isOpen);
|
const [open, onOpenChange] = useState(isOpen);
|
||||||
@@ -300,7 +298,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-muted-foreground text-sm">
|
<TableCell className="text-muted-foreground text-sm">
|
||||||
<LocaleDate date={row.createdAt} />
|
{i18n.date(row.createdAt)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { NextResponse } from 'next/server';
|
|||||||
import { getToken } from 'next-auth/jwt';
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
|
||||||
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
||||||
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
|
||||||
async function middleware(req: NextRequest): Promise<NextResponse> {
|
async function middleware(req: NextRequest): Promise<NextResponse> {
|
||||||
@@ -82,7 +81,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
|
|||||||
// Allow third parties to iframe the document.
|
// Allow third parties to iframe the document.
|
||||||
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
res.headers.set('Access-Control-Allow-Origin', '*');
|
res.headers.set('Access-Control-Allow-Origin', '*');
|
||||||
res.headers.set('Content-Security-Policy', "frame-ancestors *");
|
res.headers.set('Content-Security-Policy', 'frame-ancestors *');
|
||||||
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
res.headers.set('X-Content-Type-Options', 'nosniff');
|
res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
res.headers.set('X-Frame-Options', 'ALLOW-ALL');
|
res.headers.set('X-Frame-Options', 'ALLOW-ALL');
|
||||||
@@ -96,12 +95,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
|
|||||||
export default async function middlewareWrapper(req: NextRequest) {
|
export default async function middlewareWrapper(req: NextRequest) {
|
||||||
const response = await middleware(req);
|
const response = await middleware(req);
|
||||||
|
|
||||||
const lang = extractSupportedLanguage({
|
// Can place anything that needs to be set on the response here.
|
||||||
headers: req.headers,
|
|
||||||
cookies: cookies(),
|
|
||||||
});
|
|
||||||
|
|
||||||
response.cookies.set('i18n', lang);
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { Team } from '@documenso/prisma/client';
|
import type { GetTeamResponse } from '@documenso/lib/server-only/team/get-team';
|
||||||
|
|
||||||
interface TeamProviderProps {
|
interface TeamProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
team: Team;
|
team: GetTeamResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamContext = createContext<Team | null>(null);
|
const TeamContext = createContext<GetTeamResponse | null>(null);
|
||||||
|
|
||||||
export const useCurrentTeam = () => {
|
export const useCurrentTeam = () => {
|
||||||
const context = useContext(TeamContext);
|
const context = useContext(TeamContext);
|
||||||
|
|||||||
230
package-lock.json
generated
230
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
"apps/marketing": {
|
"apps/marketing": {
|
||||||
"name": "@documenso/marketing",
|
"name": "@documenso/marketing",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.1.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-contentlayer": "^0.3.4",
|
"next-contentlayer": "^0.3.4",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"perfect-freehand": "^1.2.0",
|
"perfect-freehand": "^1.2.0",
|
||||||
@@ -410,6 +410,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"apps/marketing/node_modules/next-axiom": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-axiom/-/next-axiom-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-sWxIzuJOex48ugMDlXWzvGvDGv5YHZ3w8gLZbUQ/Yml7oy5jcCItJNws9D0qmASirp2e5/BnvHxs44+9CO0GAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-deep-compare": "^1.2.1",
|
||||||
|
"whatwg-fetch": "^3.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">=14.0",
|
||||||
|
"react": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"apps/marketing/node_modules/typescript": {
|
"apps/marketing/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",
|
||||||
@@ -424,7 +441,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -449,7 +466,7 @@
|
|||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"next-axiom": "^1.1.1",
|
"next-axiom": "^1.5.1",
|
||||||
"next-plausible": "^3.10.1",
|
"next-plausible": "^3.10.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -465,7 +482,7 @@
|
|||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"remeda": "^1.27.1",
|
"remeda": "^2.12.1",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
@@ -493,6 +510,23 @@
|
|||||||
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
|
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"apps/web/node_modules/next-axiom": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-axiom/-/next-axiom-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-sWxIzuJOex48ugMDlXWzvGvDGv5YHZ3w8gLZbUQ/Yml7oy5jcCItJNws9D0qmASirp2e5/BnvHxs44+9CO0GAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-deep-compare": "^1.2.1",
|
||||||
|
"whatwg-fetch": "^3.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">=14.0",
|
||||||
|
"react": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"apps/web/node_modules/typescript": {
|
"apps/web/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",
|
||||||
@@ -3433,6 +3467,34 @@
|
|||||||
"react-dom": "^16 || ^17 || ^18"
|
"react-dom": "^16 || ^17 || ^18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hello-pangea/dnd": {
|
||||||
|
"version": "16.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz",
|
||||||
|
"integrity": "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.24.1",
|
||||||
|
"css-box-model": "^1.2.1",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"raf-schd": "^4.0.3",
|
||||||
|
"react-redux": "^8.1.3",
|
||||||
|
"redux": "^4.2.1",
|
||||||
|
"use-memo-one": "^1.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@hello-pangea/dnd/node_modules/redux": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@hexagon/base64": {
|
"node_modules/@hexagon/base64": {
|
||||||
"version": "1.1.28",
|
"version": "1.1.28",
|
||||||
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
|
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
|
||||||
@@ -11048,6 +11110,16 @@
|
|||||||
"@types/unist": "^2"
|
"@types/unist": "^2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||||
|
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
||||||
@@ -14663,6 +14735,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css.escape": {
|
"node_modules/css.escape": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
@@ -19256,6 +19337,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hosted-git-info": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||||
@@ -22639,6 +22729,12 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/meow": {
|
"node_modules/meow": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz",
|
||||||
@@ -24088,22 +24184,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-axiom": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/next-axiom/-/next-axiom-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-0r/TJ+/zetD+uDc7B+2E7WpC86hEtQ1U+DuWYrP/JNmUz+ZdPFbrZgzOSqaZ6TwYbXP56VVlPfYwq1YsKHTHYQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"remeda": "^1.29.0",
|
|
||||||
"whatwg-fetch": "^3.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"next": ">=13.4",
|
|
||||||
"react": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/next-contentlayer": {
|
"node_modules/next-contentlayer": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/next-contentlayer/-/next-contentlayer-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-contentlayer/-/next-contentlayer-0.3.4.tgz",
|
||||||
@@ -27686,6 +27766,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/railroad-diagrams": {
|
"node_modules/railroad-diagrams": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
|
||||||
@@ -28523,6 +28609,51 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw=="
|
"integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "8.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
|
||||||
|
"integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/use-sync-external-store": "^0.0.3",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
|
"use-sync-external-store": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-native": ">=0.59",
|
||||||
|
"redux": "^4 || ^5.0.0-beta.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-redux/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||||
@@ -30017,9 +30148,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/remeda": {
|
"node_modules/remeda": {
|
||||||
"version": "1.29.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/remeda/-/remeda-1.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.12.1.tgz",
|
||||||
"integrity": "sha512-M3LQ14KtMdQ1879lj/kKji3zBk158s7Rwg963mEkTfQFMxnKrIEAMxJfo/+0sp/+uGgN/KMVU2MBA4LNjqf8YQ=="
|
"integrity": "sha512-hKFAbxbQe8PMd4+CYO1DYCrCbcZsUSa7e21g7+4co91GBy7BD+Ub6JdaLy76yPOp7PCPTAXRz/9NXtZ9w15jbg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^4.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/remeda/node_modules/type-fest": {
|
||||||
|
"version": "4.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
|
||||||
|
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/remote-git-tags": {
|
"node_modules/remote-git-tags": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -34082,6 +34229,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-deep-compare": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-94iG+dEdEP/Sl3WWde+w9StIunlV8Dgj+vkt5wTwMoFQLaijiEZSXXy8KtcStpmEDtIptRJiNeD4ACTtVvnIKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "2.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/use-memo-one": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
@@ -36629,7 +36797,7 @@
|
|||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"playwright": "1.43.0",
|
"playwright": "1.43.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"remeda": "^1.27.1",
|
"remeda": "^2.12.1",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
@@ -36828,6 +36996,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@hookform/resolvers": "^3.3.0",
|
"@hookform/resolvers": "^3.3.0",
|
||||||
"@lingui/macro": "^4.11.3",
|
"@lingui/macro": "^4.11.3",
|
||||||
"@lingui/react": "^4.11.3",
|
"@lingui/react": "^4.11.3",
|
||||||
@@ -36874,6 +37043,7 @@
|
|||||||
"react-hook-form": "^7.45.4",
|
"react-hook-form": "^7.45.4",
|
||||||
"react-pdf": "7.7.3",
|
"react-pdf": "7.7.3",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
|
"remeda": "^1.27.1",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
@@ -36926,6 +37096,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/ui/node_modules/remeda": {
|
||||||
|
"version": "1.61.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/remeda/-/remeda-1.61.0.tgz",
|
||||||
|
"integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1-rc.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
|
|||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
|
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||||
import { updateField } from '@documenso/lib/server-only/field/update-field';
|
import { updateField } from '@documenso/lib/server-only/field/update-field';
|
||||||
@@ -292,9 +293,22 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
timezone,
|
timezone,
|
||||||
dateFormat: dateFormat?.value,
|
dateFormat: dateFormat?.value,
|
||||||
redirectUrl: body.meta.redirectUrl,
|
redirectUrl: body.meta.redirectUrl,
|
||||||
|
signingOrder: body.meta.signingOrder,
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (body.authOptions) {
|
||||||
|
await updateDocumentSettings({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
data: {
|
||||||
|
...body.authOptions,
|
||||||
|
},
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const recipients = await setRecipientsForDocument({
|
const recipients = await setRecipientsForDocument({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
@@ -314,6 +328,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
|
||||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||||
})),
|
})),
|
||||||
@@ -465,6 +480,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.authOptions) {
|
||||||
|
await updateDocumentSettings({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
data: body.authOptions,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
@@ -475,6 +500,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
|
||||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||||
})),
|
})),
|
||||||
@@ -547,6 +573,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.authOptions) {
|
||||||
|
await updateDocumentSettings({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
data: body.authOptions,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
@@ -557,6 +593,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
|
||||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||||
})),
|
})),
|
||||||
@@ -682,7 +719,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
|
|
||||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { name, email, role } = args.body;
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
id: Number(documentId),
|
id: Number(documentId),
|
||||||
@@ -731,11 +768,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: [
|
recipients: [
|
||||||
...recipients,
|
...recipients.map(({ email, name }) => ({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
signingOrder,
|
||||||
|
actionAuth: authOptions?.actionAuth ?? null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
@@ -767,7 +810,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
|
|
||||||
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId, recipientId } = args.params;
|
const { id: documentId, recipientId } = args.params;
|
||||||
const { name, email, role } = args.body;
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
id: Number(documentId),
|
id: Number(documentId),
|
||||||
@@ -801,6 +844,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
signingOrder,
|
||||||
|
actionAuth: authOptions?.actionAuth,
|
||||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/const
|
|||||||
import '@documenso/lib/constants/time-zones';
|
import '@documenso/lib/constants/time-zones';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
||||||
|
import {
|
||||||
|
ZDocumentAccessAuthTypesSchema,
|
||||||
|
ZDocumentActionAuthTypesSchema,
|
||||||
|
ZRecipientActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
|
DocumentSigningOrder,
|
||||||
FieldType,
|
FieldType,
|
||||||
ReadStatus,
|
ReadStatus,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
@@ -98,6 +104,7 @@ export const ZCreateDocumentMutationSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
meta: z
|
meta: z
|
||||||
@@ -118,8 +125,15 @@ export const ZCreateDocumentMutationSchema = z.object({
|
|||||||
enum: DATE_FORMATS.map((format) => format.value),
|
enum: DATE_FORMATS.map((format) => format.value),
|
||||||
}),
|
}),
|
||||||
redirectUrl: z.string(),
|
redirectUrl: z.string(),
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
|
authOptions: z
|
||||||
|
.object({
|
||||||
|
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||||
|
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,6 +150,7 @@ export const ZCreateDocumentMutationResponseSchema = z.object({
|
|||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
|
|
||||||
signingUrl: z.string(),
|
signingUrl: z.string(),
|
||||||
}),
|
}),
|
||||||
@@ -154,6 +169,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
meta: z
|
meta: z
|
||||||
@@ -163,9 +179,16 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
dateFormat: z.string(),
|
dateFormat: z.string(),
|
||||||
redirectUrl: z.string(),
|
redirectUrl: z.string(),
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
authOptions: z
|
||||||
|
.object({
|
||||||
|
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||||
|
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,6 +206,7 @@ export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
|
|||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
|
|
||||||
signingUrl: z.string(),
|
signingUrl: z.string(),
|
||||||
}),
|
}),
|
||||||
@@ -202,6 +226,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -220,9 +245,16 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
dateFormat: z.string(),
|
dateFormat: z.string(),
|
||||||
redirectUrl: ZUrlSchema,
|
redirectUrl: ZUrlSchema,
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
authOptions: z
|
||||||
|
.object({
|
||||||
|
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||||
|
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,6 +272,7 @@ export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
|
|||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
|
|
||||||
signingUrl: z.string(),
|
signingUrl: z.string(),
|
||||||
}),
|
}),
|
||||||
@@ -254,6 +287,12 @@ 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),
|
||||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
|
authOptions: z
|
||||||
|
.object({
|
||||||
|
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,6 +316,7 @@ export const ZSuccessfulRecipientResponseSchema = z.object({
|
|||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
// !: Not used for now
|
// !: Not used for now
|
||||||
// expired: z.string(),
|
// expired: z.string(),
|
||||||
@@ -394,6 +434,7 @@ export const ZTemplateMetaSchema = z.object({
|
|||||||
dateFormat: z.string().nullish(),
|
dateFormat: z.string().nullish(),
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
redirectUrl: z.string().nullish(),
|
redirectUrl: z.string().nullish(),
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).nullish().default(DocumentSigningOrder.PARALLEL),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZTemplateSchema = z.object({
|
export const ZTemplateSchema = z.object({
|
||||||
@@ -415,6 +456,7 @@ export const ZRecipientSchema = z.object({
|
|||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
|
signingOrder: z.number().nullish(),
|
||||||
documentDeletedAt: z.date().nullish(),
|
documentDeletedAt: z.date().nullish(),
|
||||||
expired: z.date().nullish(),
|
expired: z.date().nullish(),
|
||||||
signedAt: z.date().nullish(),
|
signedAt: z.date().nullish(),
|
||||||
@@ -468,6 +510,7 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
|||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
signingOrder: true,
|
||||||
authOptions: true,
|
authOptions: true,
|
||||||
role: true,
|
role: true,
|
||||||
}).array(),
|
}).array(),
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
// Add 2 signers.
|
// Add 2 signers.
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
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
|
await page.getByLabel('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
.getByRole('textbox', { name: 'Email', exact: true })
|
await page.getByLabel('Name').nth(1).fill('Recipient 2');
|
||||||
.fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Display advanced settings.
|
// Display advanced settings.
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
@@ -77,9 +76,11 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
|||||||
// Add 2 signers.
|
// Add 2 signers.
|
||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
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: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByLabel('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
|
await page.getByLabel('Name').nth(1).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();
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import {
|
||||||
|
DocumentSigningOrder,
|
||||||
|
DocumentStatus,
|
||||||
|
FieldType,
|
||||||
|
RecipientRole,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import {
|
import {
|
||||||
seedBlankDocument,
|
seedBlankDocument,
|
||||||
seedPendingDocumentWithFullFields,
|
seedPendingDocumentWithFullFields,
|
||||||
@@ -137,8 +143,9 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
|||||||
await page.getByPlaceholder('Email').fill('user1@example.com');
|
await page.getByPlaceholder('Email').fill('user1@example.com');
|
||||||
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: 'Name', exact: true }).nth(1).fill('User 2');
|
await page.getByLabel('Email').nth(1).fill('user2@example.com');
|
||||||
|
await page.getByLabel('Name').nth(1).fill('User 2');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
@@ -217,20 +224,20 @@ 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.getByLabel('Email').nth(1).fill('user2@example.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2');
|
await page.getByLabel('Name').nth(1).fill('User 2');
|
||||||
await page.locator('button[role="combobox"]').nth(1).click();
|
await page.locator('button[role="combobox"]').nth(1).click();
|
||||||
await page.getByLabel('Receives copy').click();
|
await page.getByLabel('Receives copy').click();
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(1).fill('user3@example.com');
|
await page.getByLabel('Email').nth(2).fill('user3@example.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(2).fill('User 3');
|
await page.getByLabel('Name').nth(2).fill('User 3');
|
||||||
await page.locator('button[role="combobox"]').nth(2).click();
|
await page.locator('button[role="combobox"]').nth(2).click();
|
||||||
await page.getByLabel('Needs to approve').click();
|
await page.getByLabel('Needs to approve').click();
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(2).fill('user4@example.com');
|
await page.getByLabel('Email').nth(3).fill('user4@example.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(3).fill('User 4');
|
await page.getByLabel('Name').nth(3).fill('User 4');
|
||||||
await page.locator('button[role="combobox"]').nth(3).click();
|
await page.locator('button[role="combobox"]').nth(3).click();
|
||||||
await page.getByLabel('Needs to view').click();
|
await page.getByLabel('Needs to view').click();
|
||||||
|
|
||||||
@@ -503,3 +510,163 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
|
|||||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
||||||
}).toPass();
|
}).toPass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recipients in sequential order', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentTitle = `Sequential-Signing-${Date.now()}.pdf`;
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
await page.getByLabel('Title').fill(documentTitle);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
await page.getByLabel('Enable signing order').check();
|
||||||
|
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
if (i > 1) {
|
||||||
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
|
}
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Email')
|
||||||
|
.nth(i - 1)
|
||||||
|
.fill(`user${i}@example.com`);
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Name')
|
||||||
|
.nth(i - 1)
|
||||||
|
.fill(`User ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
if (i > 1) {
|
||||||
|
await page.getByText(`User ${i} (user${i}@example.com)`).click();
|
||||||
|
}
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100 * i,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await page.getByText(`User ${i} (user${i}@example.com)`).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/documents');
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
|
||||||
|
|
||||||
|
const createdDocument = await prisma.document.findFirst({
|
||||||
|
where: { title: documentTitle },
|
||||||
|
include: { Recipient: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createdDocument).not.toBeNull();
|
||||||
|
expect(createdDocument?.Recipient.length).toBe(3);
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const recipient = createdDocument?.Recipient.find(
|
||||||
|
(r) => r.email === `user${i + 1}@example.com`,
|
||||||
|
);
|
||||||
|
expect(recipient).not.toBeNull();
|
||||||
|
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: { recipientId: recipient?.id, documentId: createdDocument?.id },
|
||||||
|
});
|
||||||
|
const recipientField = fields[0];
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
const previousRecipient = await prisma.recipient.findFirst({
|
||||||
|
where: { email: `user${i}@example.com`, documentId: createdDocument?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto(`/sign/${recipient?.token}`);
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas#signature');
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
if (box) {
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(`/sign/${recipient?.token}/complete`);
|
||||||
|
await expect(page.getByText('Document Signed')).toBeVisible();
|
||||||
|
|
||||||
|
const updatedRecipient = await prisma.recipient.findFirst({
|
||||||
|
where: { id: recipient?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the document to be signed.
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const finalDocument = await prisma.document.findFirst({
|
||||||
|
where: { id: createdDocument?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(finalDocument?.status).toBe(DocumentStatus.COMPLETED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
|
||||||
|
const { document, recipients } = await seedPendingDocumentWithFullFields({
|
||||||
|
owner: user,
|
||||||
|
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
||||||
|
fields: [FieldType.SIGNATURE],
|
||||||
|
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }],
|
||||||
|
updateDocumentOptions: {
|
||||||
|
documentMeta: {
|
||||||
|
create: {
|
||||||
|
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendingRecipient = recipients.find((r) => r.signingOrder === 2);
|
||||||
|
|
||||||
|
await page.goto(`/sign/${pendingRecipient?.token}`);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(`/sign/${pendingRecipient?.token}/waiting`);
|
||||||
|
|
||||||
|
const activeRecipient = recipients.find((r) => r.signingOrder === 1);
|
||||||
|
|
||||||
|
await page.goto(`/sign/${activeRecipient?.token}`);
|
||||||
|
|
||||||
|
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
|
||||||
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||||
import { seedTeamEmail } from '@documenso/prisma/seed/teams';
|
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||||
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';
|
||||||
@@ -355,3 +355,354 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
|
|||||||
await apiSignout({ page });
|
await apiSignout({ page });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: check document visibility based on team member role', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
|
||||||
|
// Seed users with different roles
|
||||||
|
const adminUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.ADMIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const managerUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MANAGER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MEMBER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outsideUser = await seedUser();
|
||||||
|
|
||||||
|
// Seed documents with different visibility levels
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Document Visible to Everyone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'MANAGER_AND_ABOVE',
|
||||||
|
title: 'Document Visible to Manager and Above',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Document Visible to Admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [outsideUser],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Document Visible to Admin with Recipient',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test cases for each role
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
user: adminUser,
|
||||||
|
expectedDocuments: [
|
||||||
|
'Document Visible to Everyone',
|
||||||
|
'Document Visible to Manager and Above',
|
||||||
|
'Document Visible to Admin',
|
||||||
|
'Document Visible to Admin with Recipient',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: managerUser,
|
||||||
|
expectedDocuments: ['Document Visible to Everyone', 'Document Visible to Manager and Above'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: memberUser,
|
||||||
|
expectedDocuments: ['Document Visible to Everyone'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: outsideUser,
|
||||||
|
expectedDocuments: ['Document Visible to Admin with Recipient'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: testCase.user.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the user sees the expected documents
|
||||||
|
for (const documentTitle of testCase.expectedDocuments) {
|
||||||
|
await expect(page.getByRole('link', { name: documentTitle, exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: ensure recipient can see document regardless of visibility', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
|
||||||
|
// Seed a member user
|
||||||
|
const memberUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MEMBER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed a document with ADMIN visibility but make the member user a recipient
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [memberUser],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Admin Document with Member Recipient',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: memberUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the member user can see the document
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Document with Member Recipient', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: check that members cannot see ADMIN-only documents', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
|
||||||
|
// Seed a member user
|
||||||
|
const memberUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MEMBER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed an ADMIN-only document
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Admin Only Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: memberUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the member user cannot see the ADMIN-only document
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: check that managers cannot see ADMIN-only documents', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
|
||||||
|
// Seed a manager user
|
||||||
|
const managerUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MANAGER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed an ADMIN-only document
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Admin Only Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: managerUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the manager user cannot see the ADMIN-only document
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: check that admin can see MANAGER_AND_ABOVE documents', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
|
||||||
|
// Seed an admin user
|
||||||
|
const adminUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.ADMIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed a MANAGER_AND_ABOVE document
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'MANAGER_AND_ABOVE',
|
||||||
|
title: 'Manager and Above Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: adminUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the admin user can see the MANAGER_AND_ABOVE document
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Manager and Above Document', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: users cannot see documents from other teams', async ({ page }) => {
|
||||||
|
// Seed two teams with documents
|
||||||
|
const { team: teamA, teamMember2: teamAMember } = await seedTeamDocuments();
|
||||||
|
const { team: teamB, teamMember2: teamBMember } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
// Seed a document in team B
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: teamB.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: teamB.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Team B Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sign in as a member of team A
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamAMember.email,
|
||||||
|
redirectPath: `/t/${teamA.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the user cannot see the document from team B
|
||||||
|
await expect(page.getByRole('link', { name: 'Team B Document', exact: true })).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: personal documents are not visible in team context', async ({ page }) => {
|
||||||
|
// Seed a team and a user with personal documents
|
||||||
|
const { team, teamMember2 } = await seedTeamDocuments();
|
||||||
|
const personalUser = await seedUser();
|
||||||
|
|
||||||
|
// Seed a personal document for teamMember2
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: teamMember2,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: null, // Indicates a personal document
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Personal Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sign in as teamMember2 in the team context
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamMember2.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the personal document is not visible in the team context
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Personal Document', exact: true }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PERSONAL]: team documents are not visible in personal account', async ({ page }) => {
|
||||||
|
// Seed a team and a user with personal documents
|
||||||
|
const { team, teamMember2 } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
// Seed a team document
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: teamMember2,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Team Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sign in as teamMember2 in the personal context
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamMember2.email,
|
||||||
|
redirectPath: `/documents?status=COMPLETED`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the team document is not visible in the personal context
|
||||||
|
await expect(page.getByRole('link', { name: 'Team Document', exact: true })).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,10 +42,8 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||||
await page
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
.getByRole('textbox', { name: 'Email', exact: true })
|
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||||
.fill('recipient2@documenso.com');
|
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
|
||||||
|
|
||||||
// Display advanced settings.
|
// Display advanced settings.
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
@@ -94,8 +92,8 @@ test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByPlaceholder('Name').nth(1).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();
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
@@ -211,8 +211,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { equals } from 'remeda';
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { getLimits } from '../client';
|
import { getLimits } from '../client';
|
||||||
import { FREE_PLAN_LIMITS } from '../constants';
|
import { FREE_PLAN_LIMITS } from '../constants';
|
||||||
@@ -42,7 +42,7 @@ export const LimitsProvider = ({
|
|||||||
const newLimits = await getLimits({ teamId });
|
const newLimits = await getLimits({ teamId });
|
||||||
|
|
||||||
setLimits((oldLimits) => {
|
setLimits((oldLimits) => {
|
||||||
if (equals(oldLimits, newLimits)) {
|
if (isDeepEqual(oldLimits, newLimits)) {
|
||||||
return oldLimits;
|
return oldLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
packages/email/templates/recipient-removed-from-document.tsx
Normal file
78
packages/email/templates/recipient-removed-from-document.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from '../components';
|
||||||
|
import type { TemplateDocumentCancelProps } from '../template-components/template-document-cancel';
|
||||||
|
import TemplateDocumentImage from '../template-components/template-document-image';
|
||||||
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
|
export type DocumentCancelEmailTemplateProps = Partial<TemplateDocumentCancelProps>;
|
||||||
|
|
||||||
|
export const RecipientRemovedFromDocumentTemplate = ({
|
||||||
|
inviterName = 'Lucas Smith',
|
||||||
|
documentName = 'Open Source Pledge.pdf',
|
||||||
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
|
}: DocumentCancelEmailTemplateProps) => {
|
||||||
|
const previewText = `${inviterName} has removed you from the document ${documentName}.`;
|
||||||
|
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: config.theme.extend.colors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="mx-auto my-auto bg-white font-sans">
|
||||||
|
<Section>
|
||||||
|
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||||
|
<Section>
|
||||||
|
<Img
|
||||||
|
src={getAssetUrl('/static/logo.png')}
|
||||||
|
alt="Documenso Logo"
|
||||||
|
className="mb-4 h-6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
|
{inviterName} has removed you from the document
|
||||||
|
<br />"{documentName}"
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||||
|
|
||||||
|
<Container className="mx-auto max-w-xl">
|
||||||
|
<TemplateFooter />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecipientRemovedFromDocumentTemplate;
|
||||||
@@ -3,12 +3,12 @@ import { useCallback, useRef, useState } from 'react';
|
|||||||
type ThrottleOptions = {
|
type ThrottleOptions = {
|
||||||
leading?: boolean;
|
leading?: boolean;
|
||||||
trailing?: boolean;
|
trailing?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
||||||
fn: T,
|
fn: T,
|
||||||
ms = 500,
|
ms = 500,
|
||||||
options: ThrottleOptions = {}
|
options: ThrottleOptions = {},
|
||||||
): [(...args: Parameters<T>) => void, boolean, () => void] {
|
): [(...args: Parameters<T>) => void, boolean, () => void] {
|
||||||
const [isThrottling, setIsThrottling] = useState(false);
|
const [isThrottling, setIsThrottling] = useState(false);
|
||||||
const $isThrottling = useRef(false);
|
const $isThrottling = useRef(false);
|
||||||
@@ -44,7 +44,7 @@ export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
|||||||
$lastArgs.current = args;
|
$lastArgs.current = args;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[fn, ms, leading, trailing, $setIsThrottling]
|
[fn, ms, leading, trailing, $setIsThrottling],
|
||||||
);
|
);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
|
|||||||
@@ -5,19 +5,24 @@ import { useState } from 'react';
|
|||||||
import { type Messages, setupI18n } from '@lingui/core';
|
import { type Messages, setupI18n } from '@lingui/core';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
|
||||||
|
import type { I18nLocaleData } from '../../constants/i18n';
|
||||||
|
|
||||||
export function I18nClientProvider({
|
export function I18nClientProvider({
|
||||||
children,
|
children,
|
||||||
initialLocale,
|
initialLocaleData,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
initialLocale: string;
|
initialLocaleData: I18nLocaleData;
|
||||||
initialMessages: Messages;
|
initialMessages: Messages;
|
||||||
}) {
|
}) {
|
||||||
|
const { lang, locales } = initialLocaleData;
|
||||||
|
|
||||||
const [i18n] = useState(() => {
|
const [i18n] = useState(() => {
|
||||||
return setupI18n({
|
return setupI18n({
|
||||||
locale: initialLocale,
|
locale: lang,
|
||||||
messages: { [initialLocale]: initialMessages },
|
locales: locales,
|
||||||
|
messages: { [lang]: initialMessages },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
|
||||||
import { cookies } from 'next/headers';
|
import { cookies, headers } from 'next/headers';
|
||||||
|
|
||||||
import type { I18n, Messages } from '@lingui/core';
|
import type { I18n, Messages } from '@lingui/core';
|
||||||
import { setupI18n } from '@lingui/core';
|
import { setupI18n } from '@lingui/core';
|
||||||
import { setI18n } from '@lingui/react/server';
|
import { setI18n } from '@lingui/react/server';
|
||||||
|
|
||||||
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../../constants/app';
|
import { IS_APP_WEB } from '../../constants/app';
|
||||||
import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
|
import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
|
||||||
import { extractSupportedLanguage } from '../../utils/i18n';
|
import { extractLocaleData } from '../../utils/i18n';
|
||||||
|
|
||||||
type SupportedLocales = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||||
|
|
||||||
async function loadCatalog(locale: SupportedLocales): Promise<{
|
async function loadCatalog(lang: SupportedLanguages): Promise<{
|
||||||
[k: string]: Messages;
|
[k: string]: Messages;
|
||||||
}> {
|
}> {
|
||||||
const { messages } = await import(
|
const { messages } = await import(
|
||||||
`../../translations/${locale}/${IS_APP_WEB ? 'web' : 'marketing'}.js`
|
`../../translations/${lang}/${IS_APP_WEB ? 'web' : 'marketing'}.js`
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[locale]: messages,
|
[lang]: messages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,18 +31,18 @@ export const allMessages = catalogs.reduce((acc, oneCatalog) => {
|
|||||||
return { ...acc, ...oneCatalog };
|
return { ...acc, ...oneCatalog };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
type AllI18nInstances = { [K in SupportedLocales]: I18n };
|
type AllI18nInstances = { [K in SupportedLanguages]: I18n };
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) => {
|
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, lang) => {
|
||||||
const messages = allMessages[locale] ?? {};
|
const messages = allMessages[lang] ?? {};
|
||||||
|
|
||||||
const i18n = setupI18n({
|
const i18n = setupI18n({
|
||||||
locale,
|
locale: lang,
|
||||||
messages: { [locale]: messages },
|
messages: { [lang]: messages },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...acc, [locale]: i18n };
|
return { ...acc, [lang]: i18n };
|
||||||
}, {}) as AllI18nInstances;
|
}, {}) as AllI18nInstances;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,24 +50,23 @@ export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) =>
|
|||||||
*
|
*
|
||||||
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
|
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
|
||||||
*/
|
*/
|
||||||
export const setupI18nSSR = (overrideLang?: SupportedLocales) => {
|
export const setupI18nSSR = () => {
|
||||||
let lang =
|
const { lang, locales } = extractLocaleData({
|
||||||
overrideLang ||
|
cookies: cookies(),
|
||||||
extractSupportedLanguage({
|
headers: headers(),
|
||||||
cookies: cookies(),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Override web app to be English.
|
|
||||||
if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
|
|
||||||
lang = 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and set a ready-made i18n instance for the given language.
|
// Get and set a ready-made i18n instance for the given language.
|
||||||
const i18n = allI18nInstances[lang];
|
const i18n = allI18nInstances[lang];
|
||||||
|
|
||||||
|
// Reactivate the i18n instance with the locale for date and number formatting.
|
||||||
|
i18n.activate(lang, locales);
|
||||||
|
|
||||||
setI18n(i18n);
|
setI18n(i18n);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lang,
|
lang,
|
||||||
|
locales,
|
||||||
i18n,
|
i18n,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useContext } from 'react';
|
|
||||||
|
|
||||||
export type LocaleContextValue = {
|
|
||||||
locale: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
||||||
|
|
||||||
export const useLocale = () => {
|
|
||||||
const context = useContext(LocaleContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useLocale must be used within a LocaleProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LocaleProvider({
|
|
||||||
children,
|
|
||||||
locale,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
locale: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<LocaleContext.Provider
|
|
||||||
value={{
|
|
||||||
locale: locale,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</LocaleContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
export const IS_APP_WEB_I18N_ENABLED = false;
|
|
||||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||||
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
||||||
|
|
||||||
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
||||||
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
||||||
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL = process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL();
|
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
|
||||||
|
process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL();
|
||||||
|
|
||||||
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
|
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
|
||||||
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
||||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||||
|
export const IS_APP_WEB_I18N_ENABLED = true;
|
||||||
|
|
||||||
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
|
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
|
||||||
|
|
||||||
|
|||||||
23
packages/lib/constants/document-visibility.ts
Normal file
23
packages/lib/constants/document-visibility.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
|
|
||||||
|
import type { TDocumentVisibility } from '../types/document-visibility';
|
||||||
|
|
||||||
|
type DocumentVisibilityTypeData = {
|
||||||
|
key: TDocumentVisibility;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DOCUMENT_VISIBILITY: Record<string, DocumentVisibilityTypeData> = {
|
||||||
|
[DocumentVisibility.ADMIN]: {
|
||||||
|
key: DocumentVisibility.ADMIN,
|
||||||
|
value: 'Admins only',
|
||||||
|
},
|
||||||
|
[DocumentVisibility.EVERYONE]: {
|
||||||
|
key: DocumentVisibility.EVERYONE,
|
||||||
|
value: 'Everyone',
|
||||||
|
},
|
||||||
|
[DocumentVisibility.MANAGER_AND_ABOVE]: {
|
||||||
|
key: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||||
|
value: 'Managers and above',
|
||||||
|
},
|
||||||
|
} satisfies Record<TDocumentVisibility, DocumentVisibilityTypeData>;
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en'] as const;
|
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr'] as const;
|
||||||
|
|
||||||
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
|
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
|
||||||
|
|
||||||
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||||
|
|
||||||
|
export type I18nLocaleData = {
|
||||||
|
/**
|
||||||
|
* The supported language extracted from the locale.
|
||||||
|
*/
|
||||||
|
lang: SupportedLanguageCodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferred locales.
|
||||||
|
*/
|
||||||
|
locales: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export const APP_I18N_OPTIONS = {
|
export const APP_I18N_OPTIONS = {
|
||||||
supportedLangs: SUPPORTED_LANGUAGE_CODES,
|
supportedLangs: SUPPORTED_LANGUAGE_CODES,
|
||||||
sourceLang: 'en',
|
sourceLang: 'en',
|
||||||
|
defaultLocale: 'en-US',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type SupportedLanguage = {
|
type SupportedLanguage = {
|
||||||
@@ -25,4 +38,8 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
|
|||||||
full: 'English',
|
full: 'English',
|
||||||
short: 'en',
|
short: 'en',
|
||||||
},
|
},
|
||||||
|
fr: {
|
||||||
|
full: 'French',
|
||||||
|
short: 'fr',
|
||||||
|
},
|
||||||
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
|
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"playwright": "1.43.0",
|
"playwright": "1.43.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"remeda": "^1.27.1",
|
"remeda": "^2.12.1",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
diffDocumentMetaChanges,
|
diffDocumentMetaChanges,
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
} from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type CreateDocumentMetaOptions = {
|
export type CreateDocumentMetaOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@@ -16,6 +17,7 @@ export type CreateDocumentMetaOptions = {
|
|||||||
password?: string;
|
password?: string;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
|
signingOrder?: DocumentSigningOrder;
|
||||||
userId: number;
|
userId: number;
|
||||||
requestMetadata: RequestMetadata;
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
@@ -29,6 +31,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
password,
|
password,
|
||||||
userId,
|
userId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
|
signingOrder,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@@ -78,6 +81,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
timezone,
|
timezone,
|
||||||
documentId,
|
documentId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
|
signingOrder,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
subject,
|
subject,
|
||||||
@@ -86,6 +90,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
dateFormat,
|
dateFormat,
|
||||||
timezone,
|
timezone,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
|
signingOrder,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
|||||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import {
|
||||||
|
DocumentSigningOrder,
|
||||||
|
DocumentStatus,
|
||||||
|
RecipientRole,
|
||||||
|
SendStatus,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
|
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
import { sendPendingEmail } from './send-pending-email';
|
import { sendPendingEmail } from './send-pending-email';
|
||||||
|
|
||||||
@@ -29,6 +36,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
Recipient: {
|
Recipient: {
|
||||||
where: {
|
where: {
|
||||||
token,
|
token,
|
||||||
@@ -59,6 +67,16 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||||
|
|
||||||
|
if (!isRecipientsTurn) {
|
||||||
|
throw new Error(
|
||||||
|
`Recipient ${recipient.id} attempted to complete the document before it was their turn`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fields = await prisma.field.findMany({
|
const fields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
@@ -120,17 +138,48 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const pendingRecipients = await prisma.recipient.count({
|
const pendingRecipients = await prisma.recipient.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
signingOrder: true,
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
signingStatus: {
|
signingStatus: {
|
||||||
not: SigningStatus.SIGNED,
|
not: SigningStatus.SIGNED,
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
// Composite sort so our next recipient is always the one with the lowest signing order or id
|
||||||
|
// if there is a tie.
|
||||||
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pendingRecipients > 0) {
|
if (pendingRecipients.length > 0) {
|
||||||
await sendPendingEmail({ documentId, recipientId: recipient.id });
|
await sendPendingEmail({ documentId, recipientId: recipient.id });
|
||||||
|
|
||||||
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
|
const [nextRecipient] = pendingRecipients;
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.recipient.update({
|
||||||
|
where: { id: nextRecipient.id },
|
||||||
|
data: { sendStatus: SendStatus.SENT },
|
||||||
|
});
|
||||||
|
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'send.signing.requested.email',
|
||||||
|
payload: {
|
||||||
|
userId: document.userId,
|
||||||
|
documentId: document.id,
|
||||||
|
recipientId: nextRecipient.id,
|
||||||
|
requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
||||||
@@ -138,7 +187,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
id: document.id,
|
id: document.id,
|
||||||
Recipient: {
|
Recipient: {
|
||||||
every: {
|
every: {
|
||||||
signingStatus: SigningStatus.SIGNED,
|
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user