mirror of
https://github.com/documenso/documenso.git
synced 2026-07-07 11:35:01 +10:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5650007302 | |||
| ddee8a8272 | |||
| efb2bc94ab | |||
| 97ee69e7a0 | |||
| 3da344fc5f | |||
| 404ca3202f | |||
| c043fa9c06 | |||
| 9852e8971f | |||
| 5091112e4b | |||
| e76f732990 | |||
| b7c3deb6cd | |||
| 08114f7b97 | |||
| 6e368cc333 | |||
| 4ce4ca3f34 | |||
| 7644c0d855 | |||
| fa6453e811 | |||
| f7a20113e5 | |||
| 3d644db286 | |||
| 357bdd374f |
@@ -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
|
||||
@@ -16,12 +16,12 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "14.2.6",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next-plausible": "^3.12.0",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"signing-documents": "Signing Documents",
|
||||
"templates": "Templates",
|
||||
"direct-links": "Direct Signing Links",
|
||||
"document-visibility": "Document Visibility",
|
||||
"-- Legal Overview": {
|
||||
"type": "separator",
|
||||
"title": "Legal Overview"
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.1-rc.0",
|
||||
"version": "1.7.1-rc.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -32,16 +32,16 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-plausible": "^3.10.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.77.3",
|
||||
"react": "^18",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.11.0",
|
||||
"recharts": "^2.7.2",
|
||||
@@ -51,9 +51,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,7 @@ export const SinglePlayerClient = () => {
|
||||
sendStatus: 'NOT_SENT',
|
||||
role: 'SIGNER',
|
||||
authOptions: null,
|
||||
signingOrder: null,
|
||||
};
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { type HTMLAttributes, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@@ -9,15 +9,15 @@ import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||
|
||||
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
// 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>;
|
||||
|
||||
@@ -44,7 +44,9 @@ const FOOTER_LINKS = [
|
||||
];
|
||||
|
||||
export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={cn('border-t py-12', className)} {...props}>
|
||||
@@ -97,13 +99,22 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { extractLocaleData } from '@documenso/lib/utils/i18n';
|
||||
|
||||
export default function middleware(req: NextRequest) {
|
||||
const { lang } = extractLocaleData({
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Vendored
+1
-1
@@ -3,4 +3,4 @@
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
+14
-22
@@ -25,13 +25,20 @@ const FONT_NOTO_SANS_BYTES = fs.readFileSync(
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
serverExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
swcPlugins: [['@lingui/swc-plugin', {}]],
|
||||
turbo: {
|
||||
rules: {
|
||||
'*.po': {
|
||||
loaders: ['@lingui/loader'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
@@ -49,26 +56,11 @@ const config = {
|
||||
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
||||
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
|
||||
},
|
||||
modularizeImports: {
|
||||
'lucide-react': {
|
||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||
},
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// fixes: Module not found: Can’t resolve ‘../build/Release/canvas.node’
|
||||
if (isServer) {
|
||||
config.resolve.alias.canvas = false;
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.po$/,
|
||||
use: {
|
||||
loader: '@lingui/loader',
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
// modularizeImports: {
|
||||
// 'lucide-react': {
|
||||
// transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||
// },
|
||||
// },
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.1-rc.0",
|
||||
"version": "1.7.1-rc.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -35,25 +35,25 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-plausible": "^3.10.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.75.3",
|
||||
"posthog-node": "^3.1.1",
|
||||
"react": "^18",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-call": "^1.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-rnd": "^10.4.1",
|
||||
"recharts": "^2.7.2",
|
||||
"remeda": "^1.27.1",
|
||||
"remeda": "^2.12.1",
|
||||
"sharp": "0.32.6",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
@@ -63,7 +63,7 @@
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
@@ -74,4 +74,4 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-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 { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { DocumentStatus } 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 { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -39,7 +41,7 @@ export type DocumentPageViewProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
team?: Team & { teamEmail: TeamEmail | null };
|
||||
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
|
||||
};
|
||||
|
||||
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
||||
@@ -62,11 +64,35 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
teamId: team?.id,
|
||||
}).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(
|
||||
'app_document_page_view_history_sheet',
|
||||
);
|
||||
|
||||
if (!document || !document.documentData) {
|
||||
if (!document || !document.documentData || (team && !canAccessDocument)) {
|
||||
redirect(documentRootPath);
|
||||
}
|
||||
|
||||
if (team && !canAccessDocument) {
|
||||
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({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newFields) => {
|
||||
@@ -177,6 +191,7 @@ export const EditDocumentForm = ({
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
@@ -204,15 +219,22 @@ export const EditDocumentForm = ({
|
||||
|
||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||
try {
|
||||
await 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,
|
||||
})),
|
||||
});
|
||||
await Promise.all([
|
||||
setSigningOrderForDocument({
|
||||
documentId: document.id,
|
||||
signingOrder: data.signingOrder,
|
||||
}),
|
||||
|
||||
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();
|
||||
@@ -339,6 +361,7 @@ export const EditDocumentForm = ({
|
||||
key={recipients.length}
|
||||
documentFlow={documentFlow.settings}
|
||||
document={document}
|
||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
isDocumentEnterprise={isDocumentEnterprise}
|
||||
@@ -350,6 +373,7 @@ export const EditDocumentForm = ({
|
||||
key={recipients.length}
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
signingOrder={document.documentMeta?.signingOrder}
|
||||
fields={fields}
|
||||
isDocumentEnterprise={isDocumentEnterprise}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
|
||||
@@ -3,14 +3,17 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { Plural, Trans } from '@lingui/macro';
|
||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||
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 { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
||||
@@ -21,7 +24,7 @@ export type DocumentEditPageViewProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
team?: Team;
|
||||
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
|
||||
};
|
||||
|
||||
export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
|
||||
@@ -43,10 +46,34 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
||||
teamId: team?.id,
|
||||
}).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) {
|
||||
redirect(documentRootPath);
|
||||
}
|
||||
|
||||
if (team && !canAccessDocument) {
|
||||
redirect(documentRootPath);
|
||||
}
|
||||
|
||||
if (document.status === InternalDocumentStatus.COMPLETED) {
|
||||
redirect(`${documentRootPath}/${documentId}`);
|
||||
}
|
||||
|
||||
@@ -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 { parseToIntegerArray } from '@documenso/lib/utils/params';
|
||||
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 { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
@@ -33,7 +33,7 @@ export type DocumentsPageViewProps = {
|
||||
perPage?: string;
|
||||
senderIds?: string;
|
||||
};
|
||||
team?: Team & { teamEmail?: TeamEmail | null };
|
||||
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
||||
};
|
||||
|
||||
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||
@@ -47,6 +47,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
const currentTeam = team
|
||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
||||
: undefined;
|
||||
const currentTeamMemberRole = team?.currentTeamMember?.role;
|
||||
|
||||
const getStatOptions: GetStatsInput = {
|
||||
user,
|
||||
@@ -58,6 +59,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
teamId: team.id,
|
||||
teamEmail: team.teamEmail?.email,
|
||||
senderIds,
|
||||
currentTeamMemberRole,
|
||||
currentUserEmail: user.email,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export const UserPasskeysDataTable = () => {
|
||||
cell: ({ row }) =>
|
||||
row.original.lastUsedAt
|
||||
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
|
||||
: msg`Never`,
|
||||
: _(msg`Never`),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@@ -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({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
@@ -160,11 +173,19 @@ export const EditTemplateForm = ({
|
||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||
) => {
|
||||
try {
|
||||
await addTemplateSigners({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
signers: data.signers,
|
||||
});
|
||||
await Promise.all([
|
||||
setSigningOrderForTemplate({
|
||||
templateId: template.id,
|
||||
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();
|
||||
@@ -262,6 +283,7 @@ export const EditTemplateForm = ({
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
signingOrder={template.templateMeta?.signingOrder}
|
||||
templateDirectLink={template.directLink}
|
||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||
isEnterprise={isEnterprise}
|
||||
|
||||
@@ -267,14 +267,14 @@ export const CheckboxField = ({
|
||||
)}
|
||||
|
||||
{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) => {
|
||||
const itemValue = item.value || `empty-value-${item.id}`;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<Checkbox
|
||||
className="h-4 w-4"
|
||||
className="h-3 w-3"
|
||||
checkClassName="text-white"
|
||||
id={`checkbox-${index}`}
|
||||
checked={field.customText
|
||||
@@ -283,7 +283,7 @@ export const CheckboxField = ({
|
||||
disabled={isLoading}
|
||||
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${index}`}>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -204,25 +204,29 @@ export default async function CompletedSigningPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canSignUp && (
|
||||
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
|
||||
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
|
||||
<Trans>Need to sign documents?</Trans>
|
||||
</h2>
|
||||
<div className="flex flex-col items-center">
|
||||
{canSignUp && (
|
||||
<div className="flex max-w-xl flex-col items-center justify-center p-4 md:p-12">
|
||||
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
|
||||
<Trans>Need to sign documents?</Trans>
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
||||
</div>
|
||||
)}
|
||||
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoggedIn && (
|
||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
|
||||
<Trans>Go Back Home</Trans>
|
||||
</Link>
|
||||
)}
|
||||
{isLoggedIn && (
|
||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600">
|
||||
<Trans>Go Back Home</Trans>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PollUntilDocumentCompleted document={document} />
|
||||
|
||||
@@ -150,7 +150,7 @@ export const DateField = ({
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { signOut } from 'next-auth/react';
|
||||
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||
@@ -25,22 +25,19 @@ export const DocumentActionAuthAccount = ({
|
||||
}: DocumentActionAuthAccountProps) => {
|
||||
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) => {
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
|
||||
const encryptedEmail = await encryptSecondaryData({
|
||||
data: email,
|
||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
||||
await signOut({
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
await signOut({
|
||||
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
|
||||
});
|
||||
router.push(`/signin#email=${email}`);
|
||||
} catch {
|
||||
setIsSigningOut(false);
|
||||
|
||||
|
||||
@@ -189,7 +189,10 @@ export const DropdownField = ({
|
||||
},
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder={`${_(msg`Select`)}`} />
|
||||
<SelectValue
|
||||
className="text-[clamp(0.625rem,1cqw,0.825rem)]"
|
||||
placeholder={`${_(msg`Select`)}`}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
|
||||
{parsedFieldMeta?.values?.map((item, index) => (
|
||||
@@ -203,7 +206,7 @@ export const DropdownField = ({
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -128,7 +128,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -29,9 +29,16 @@ export type SigningFormProps = {
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
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 analytics = useAnalytics();
|
||||
const { data: session } = useSession();
|
||||
@@ -150,6 +157,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,6 +221,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const InitialsField = ({
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -259,7 +259,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
)}
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -267,7 +267,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add number</Trans>}
|
||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
|
||||
</DialogTitle>
|
||||
|
||||
<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 { 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 { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
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 isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
||||
|
||||
if (!isRecipientsTurn) {
|
||||
return redirect(`/sign/${token}/waiting`);
|
||||
}
|
||||
|
||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
@@ -146,6 +153,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
document={document}
|
||||
fields={fields}
|
||||
completedFields={completedFields}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
|
||||
@@ -173,16 +173,16 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<RadioGroup>
|
||||
<RadioGroup className="gap-y-1">
|
||||
{values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
className=""
|
||||
className="h-3 w-3"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`}>
|
||||
<Label htmlFor={`option-${index}`} className="text-xs">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ export type SignDialogProps = {
|
||||
fieldsValidated: () => void | Promise<void>;
|
||||
onSignatureComplete: () => void | Promise<void>;
|
||||
role: RecipientRole;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignDialog = ({
|
||||
@@ -32,6 +33,7 @@ export const SignDialog = ({
|
||||
fieldsValidated,
|
||||
onSignatureComplete,
|
||||
role,
|
||||
disabled = false,
|
||||
}: SignDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const truncatedTitle = truncateTitle(documentTitle);
|
||||
@@ -54,6 +56,7 @@ export const SignDialog = ({
|
||||
size="lg"
|
||||
onClick={fieldsValidated}
|
||||
loading={isSubmitting}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||
</Button>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { signOut } from 'next-auth/react';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@@ -20,24 +20,19 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
|
||||
const { _ } = useLingui();
|
||||
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) => {
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
|
||||
const encryptedEmail = await encryptSecondaryData({
|
||||
data: email,
|
||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
||||
await signOut({
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
await signOut({
|
||||
callbackUrl: emailHasAccount
|
||||
? `/signin?email=${encodeURIComponent(encryptedEmail)}`
|
||||
: `/signup?email=${encodeURIComponent(encryptedEmail)}`,
|
||||
});
|
||||
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
|
||||
@@ -128,7 +128,7 @@ export const SigningFieldContainer = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(type === 'Checkbox' ? 'group' : '')}>
|
||||
<div className={cn('[container-type:size]', type === 'Checkbox' ? 'group' : '')}>
|
||||
<FieldRootContainer field={field}>
|
||||
{!field.inserted && !loading && !readOnlyField && (
|
||||
<button
|
||||
|
||||
@@ -39,6 +39,7 @@ export type SigningPageViewProps = {
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
};
|
||||
|
||||
export const SigningPageView = ({
|
||||
@@ -46,6 +47,7 @@ export const SigningPageView = ({
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
}: SigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
@@ -99,6 +101,7 @@ export const SigningPageView = ({
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
redirectUrl={documentMeta?.redirectUrl}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
>
|
||||
<span className="flex items-center justify-center gap-x-1">
|
||||
<Type />
|
||||
{fieldDisplayName || <Trans>Add text</Trans>}
|
||||
{fieldDisplayName || <Trans>Text</Trans>}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
@@ -269,7 +269,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add Text</Trans>}
|
||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Text</Trans>}
|
||||
</DialogTitle>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { env } from 'next-runtime-env';
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
IS_OIDC_SSO_ENABLED,
|
||||
OIDC_PROVIDER_LABEL,
|
||||
} from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
|
||||
@@ -19,24 +17,11 @@ export const metadata: Metadata = {
|
||||
title: 'Sign In',
|
||||
};
|
||||
|
||||
type SignInPageProps = {
|
||||
searchParams: {
|
||||
email?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
export default function SignInPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
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 (
|
||||
<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">
|
||||
@@ -50,7 +35,6 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
<hr className="-mx-6 my-4" />
|
||||
|
||||
<SignInForm
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||
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 { 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';
|
||||
|
||||
@@ -13,13 +12,7 @@ export const metadata: Metadata = {
|
||||
title: 'Sign Up',
|
||||
};
|
||||
|
||||
type SignUpPageProps = {
|
||||
searchParams: {
|
||||
email?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
export default function SignUpPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||
@@ -28,17 +21,9 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
redirect('/signin');
|
||||
}
|
||||
|
||||
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
|
||||
const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
|
||||
|
||||
if (!email && rawEmail) {
|
||||
redirect('/signup');
|
||||
}
|
||||
|
||||
return (
|
||||
<SignUpFormV2
|
||||
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
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">
|
||||
<SigningCard3D
|
||||
className='w-full mx-auto'
|
||||
className="mx-auto w-full"
|
||||
name={name || 'Documenso'}
|
||||
signature={signature}
|
||||
signingCelebrationImage={signingCelebration}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
|
||||
<Trans>The document is now completed, please follow any instructions provided within the parent application.</Trans>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
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 { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
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 type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
@@ -34,7 +35,6 @@ import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
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">
|
||||
{(!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 */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
@@ -318,26 +318,26 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
|
||||
{/* Widget */}
|
||||
<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}
|
||||
>
|
||||
<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 */}
|
||||
<div>
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
@@ -354,7 +354,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<Label htmlFor="full-name">
|
||||
@@ -408,9 +408,9 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
</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 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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);
|
||||
|
||||
return (
|
||||
<SigningProvider
|
||||
email={user?.email}
|
||||
fullName={user?.name}
|
||||
signature={user?.signature}
|
||||
>
|
||||
<SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
|
||||
<DocumentAuthProvider
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={recipient}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
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 {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const EmbedPaywall = () => {
|
||||
return <div>
|
||||
<h1>Paywall</h1>
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Paywall</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'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 { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { type DocumentData, type Field } from '@documenso/prisma/client';
|
||||
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 { Button } from '@documenso/ui/primitives/button';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
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">
|
||||
{(!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 */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
@@ -196,26 +198,26 @@ export const EmbedSignDocumentClientPage = ({
|
||||
|
||||
{/* Widget */}
|
||||
<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}
|
||||
>
|
||||
<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 */}
|
||||
<div>
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
@@ -232,7 +234,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<Label htmlFor="full-name">
|
||||
@@ -285,9 +287,9 @@ export const EmbedSignDocumentClientPage = ({
|
||||
</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 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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 { 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 { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-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 { EmbedPaywall } from '../../paywall';
|
||||
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 = {
|
||||
params: {
|
||||
@@ -66,7 +66,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
||||
.exhaustive();
|
||||
|
||||
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 (
|
||||
|
||||
@@ -7,10 +7,11 @@ import { useRouter } from 'next/navigation';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
DOCUMENTS_PAGE_SHORTCUT,
|
||||
SETTINGS_PAGE_SHORTCUT,
|
||||
@@ -20,7 +21,10 @@ import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} 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 { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -31,6 +35,7 @@ import {
|
||||
CommandShortcut,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const DOCUMENTS_PAGES = [
|
||||
{
|
||||
@@ -207,6 +212,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
<Commands push={push} pages={SETTINGS_PAGES} />
|
||||
</CommandGroup>
|
||||
<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')}>
|
||||
Change theme
|
||||
</CommandItem>
|
||||
@@ -218,7 +226,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
||||
{currentPage === 'language' && <LanguageCommands />}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
@@ -269,3 +279,46 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
|
||||
</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';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
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 type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -41,6 +44,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
|
||||
|
||||
const isUserAdmin = isAdmin(user);
|
||||
|
||||
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
|
||||
@@ -274,6 +279,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground px-4 py-2"
|
||||
onClick={() => setLanguageSwitcherOpen(true)}
|
||||
>
|
||||
<Trans>Language</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-destructive/90 hover:!text-destructive px-4 py-2"
|
||||
onSelect={async () =>
|
||||
@@ -285,6 +297,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
<Trans>Sign Out</Trans>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -335,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()}
|
||||
|
||||
{isUserDetailsVisible && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
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 (
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
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 (
|
||||
<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">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,6 @@ import { NextResponse } from 'next/server';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
|
||||
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
||||
import { extractLocaleData } from '@documenso/lib/utils/i18n';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
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.
|
||||
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
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('X-Content-Type-Options', 'nosniff');
|
||||
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) {
|
||||
const response = await middleware(req);
|
||||
|
||||
const { lang } = extractLocaleData({
|
||||
headers: req.headers,
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
response.cookies.set('i18n', lang);
|
||||
// Can place anything that needs to be set on the response here.
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { createContext, useContext } 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 {
|
||||
children: React.ReactNode;
|
||||
team: Team;
|
||||
team: GetTeamResponse;
|
||||
}
|
||||
|
||||
const TeamContext = createContext<Team | null>(null);
|
||||
const TeamContext = createContext<GetTeamResponse | null>(null);
|
||||
|
||||
export const useCurrentTeam = () => {
|
||||
const context = useContext(TeamContext);
|
||||
|
||||
Generated
+765
-685
File diff suppressed because it is too large
Load Diff
+9
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.7.1-rc.0",
|
||||
"version": "1.7.1-rc.3",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
@@ -64,14 +64,19 @@
|
||||
"dependencies": {
|
||||
"@documenso/pdf-sign": "^0.1.0",
|
||||
"@lingui/core": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"inngest-cli": "^0.29.1",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"react": "^18"
|
||||
"next": "15.0.0-canary.165",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
},
|
||||
"overrides": {
|
||||
"next": "14.2.6"
|
||||
"next": "15.0.0-canary.165",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
timezone,
|
||||
dateFormat: dateFormat?.value,
|
||||
redirectUrl: body.meta.redirectUrl,
|
||||
signingOrder: body.meta.signingOrder,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
@@ -301,7 +302,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
data: {
|
||||
...body.authOptions,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
@@ -325,6 +328,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
@@ -496,6 +500,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
@@ -588,6 +593,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email: recipient.email,
|
||||
token: recipient.token,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
|
||||
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
||||
})),
|
||||
@@ -713,7 +719,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role, authOptions } = args.body;
|
||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@@ -762,11 +768,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
recipients: [
|
||||
...recipients,
|
||||
...recipients.map(({ email, name }) => ({
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
})),
|
||||
{
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
signingOrder,
|
||||
actionAuth: authOptions?.actionAuth ?? null,
|
||||
},
|
||||
],
|
||||
@@ -799,7 +810,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, recipientId } = args.params;
|
||||
const { name, email, role, authOptions } = args.body;
|
||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@@ -833,6 +844,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
signingOrder,
|
||||
actionAuth: authOptions?.actionAuth,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}).catch(() => null);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
DocumentDataType,
|
||||
DocumentSigningOrder,
|
||||
FieldType,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
@@ -103,6 +104,7 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
signingOrder: z.number().nullish(),
|
||||
}),
|
||||
),
|
||||
meta: z
|
||||
@@ -123,6 +125,7 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
enum: DATE_FORMATS.map((format) => format.value),
|
||||
}),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
})
|
||||
.partial(),
|
||||
authOptions: z
|
||||
@@ -147,6 +150,7 @@ export const ZCreateDocumentMutationResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().nullish(),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
@@ -165,6 +169,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
signingOrder: z.number().nullish(),
|
||||
}),
|
||||
),
|
||||
meta: z
|
||||
@@ -174,6 +179,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
@@ -200,6 +206,7 @@ export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
signingOrder: z.number().nullish(),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
@@ -219,6 +226,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().min(1),
|
||||
signingOrder: z.number().nullish(),
|
||||
}),
|
||||
)
|
||||
.refine(
|
||||
@@ -237,6 +245,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
timezone: z.string(),
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: ZUrlSchema,
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
@@ -263,6 +272,7 @@ export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
token: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().nullish(),
|
||||
|
||||
signingUrl: z.string(),
|
||||
}),
|
||||
@@ -277,6 +287,7 @@ export const ZCreateRecipientMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
signingOrder: z.number().nullish(),
|
||||
authOptions: z
|
||||
.object({
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
||||
@@ -305,6 +316,7 @@ export const ZSuccessfulRecipientResponseSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().nullish(),
|
||||
token: z.string(),
|
||||
// !: Not used for now
|
||||
// expired: z.string(),
|
||||
@@ -422,6 +434,7 @@ export const ZTemplateMetaSchema = z.object({
|
||||
dateFormat: z.string().nullish(),
|
||||
templateId: z.number(),
|
||||
redirectUrl: z.string().nullish(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).nullish().default(DocumentSigningOrder.PARALLEL),
|
||||
});
|
||||
|
||||
export const ZTemplateSchema = z.object({
|
||||
@@ -443,6 +456,7 @@ export const ZRecipientSchema = z.object({
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
token: z.string(),
|
||||
signingOrder: z.number().nullish(),
|
||||
documentDeletedAt: z.date().nullish(),
|
||||
expired: z.date().nullish(),
|
||||
signedAt: z.date().nullish(),
|
||||
@@ -496,6 +510,7 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
signingOrder: true,
|
||||
authOptions: true,
|
||||
role: true,
|
||||
}).array(),
|
||||
|
||||
@@ -41,11 +41,10 @@ test.describe('[EE_ONLY]', () => {
|
||||
// Add 2 signers.
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
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');
|
||||
|
||||
// Display advanced settings.
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
@@ -77,9 +76,11 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
||||
// Add 2 signers.
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
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.
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
import {
|
||||
seedBlankDocument,
|
||||
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('Name').fill('User 1');
|
||||
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();
|
||||
|
||||
@@ -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.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.locator('button[role="combobox"]').nth(1).click();
|
||||
await page.getByLabel('Receives copy').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.getByRole('textbox', { name: 'Name', exact: true }).nth(2).fill('User 3');
|
||||
await page.getByLabel('Email').nth(2).fill('user3@example.com');
|
||||
await page.getByLabel('Name').nth(2).fill('User 3');
|
||||
await page.locator('button[role="combobox"]').nth(2).click();
|
||||
await page.getByLabel('Needs to approve').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.getByRole('textbox', { name: 'Name', exact: true }).nth(3).fill('User 4');
|
||||
await page.getByLabel('Email').nth(3).fill('user4@example.com');
|
||||
await page.getByLabel('Name').nth(3).fill('User 4');
|
||||
await page.locator('button[role="combobox"]').nth(3).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);
|
||||
}).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 { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
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 { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
@@ -355,3 +355,354 @@ test('[TEAMS]: delete completed team document', async ({ 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('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Email', exact: true })
|
||||
.fill('recipient2@documenso.com');
|
||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Display advanced settings.
|
||||
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('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Advanced settings should not be visible for non EE users.
|
||||
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('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Apply require passkey for Recipient 1.
|
||||
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('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
||||
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Apply require passkey for Recipient 1.
|
||||
if (isBillingEnabled) {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next-auth": "4.24.5",
|
||||
"react": "^18",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { equals } from 'remeda';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { getLimits } from '../client';
|
||||
import { FREE_PLAN_LIMITS } from '../constants';
|
||||
@@ -42,7 +42,7 @@ export const LimitsProvider = ({
|
||||
const newLimits = await getLimits({ teamId });
|
||||
|
||||
setLimits((oldLimits) => {
|
||||
if (equals(oldLimits, newLimits)) {
|
||||
if (isDeepEqual(oldLimits, newLimits)) {
|
||||
return oldLimits;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,19 +3,19 @@ import { useCallback, useRef, useState } from 'react';
|
||||
type ThrottleOptions = {
|
||||
leading?: boolean;
|
||||
trailing?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
ms = 500,
|
||||
options: ThrottleOptions = {}
|
||||
options: ThrottleOptions = {},
|
||||
): [(...args: Parameters<T>) => void, boolean, () => void] {
|
||||
const [isThrottling, setIsThrottling] = useState(false);
|
||||
const $isThrottling = useRef(false);
|
||||
|
||||
const $timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const $lastArgs = useRef<Parameters<T> | null>(null);
|
||||
|
||||
|
||||
const { leading = true, trailing = true } = options;
|
||||
|
||||
const $setIsThrottling = useCallback((value: boolean) => {
|
||||
@@ -44,7 +44,7 @@ export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
|
||||
$lastArgs.current = args;
|
||||
}
|
||||
},
|
||||
[fn, ms, leading, trailing, $setIsThrottling]
|
||||
[fn, ms, leading, trailing, $setIsThrottling],
|
||||
);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
|
||||
@@ -5,12 +5,13 @@ export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||
|
||||
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_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_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||
export const IS_APP_WEB_I18N_ENABLED = false;
|
||||
export const IS_APP_WEB_I18N_ENABLED = true;
|
||||
|
||||
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
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');
|
||||
|
||||
@@ -38,4 +38,8 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
|
||||
full: 'English',
|
||||
short: 'en',
|
||||
},
|
||||
fr: {
|
||||
full: 'French',
|
||||
short: 'fr',
|
||||
},
|
||||
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
|
||||
|
||||
@@ -41,14 +41,14 @@
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "14.2.6",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next-auth": "4.24.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.11.3",
|
||||
"playwright": "1.43.0",
|
||||
"react": "^18",
|
||||
"remeda": "^1.27.1",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"remeda": "^2.12.1",
|
||||
"sharp": "0.32.6",
|
||||
"stripe": "^12.7.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
diffDocumentMetaChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
documentId: number;
|
||||
@@ -16,6 +17,7 @@ export type CreateDocumentMetaOptions = {
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
userId: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
};
|
||||
@@ -29,6 +31,7 @@ export const upsertDocumentMeta = async ({
|
||||
password,
|
||||
userId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@@ -78,6 +81,7 @@ export const upsertDocumentMeta = async ({
|
||||
timezone,
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
@@ -86,6 +90,7 @@ export const upsertDocumentMeta = async ({
|
||||
dateFormat,
|
||||
timezone,
|
||||
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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
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 { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
|
||||
@@ -29,6 +36,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
where: {
|
||||
token,
|
||||
@@ -59,6 +67,16 @@ export const completeDocumentWithToken = async ({
|
||||
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({
|
||||
where: {
|
||||
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: {
|
||||
documentId: document.id,
|
||||
signingStatus: {
|
||||
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 });
|
||||
|
||||
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({
|
||||
@@ -138,7 +187,7 @@ export const completeDocumentWithToken = async ({
|
||||
id: document.id,
|
||||
Recipient: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,10 +2,11 @@ import { DateTime } from 'luxon';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { Document, Prisma, Team, TeamEmail, User } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { FindResultSet } from '../../types/find-result-set';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
|
||||
@@ -58,6 +59,14 @@ export const findDocuments = async ({
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -70,6 +79,7 @@ export const findDocuments = async ({
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.members[0].role ?? null;
|
||||
|
||||
const termFilters = match(term)
|
||||
.with(P.string.minLength(1), () => {
|
||||
@@ -82,7 +92,37 @@ export const findDocuments = async ({
|
||||
})
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const filters = team ? findTeamDocumentsFilter(status, team) : findDocumentsFilter(status, user);
|
||||
const visibilityFilters = [
|
||||
match(teamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user);
|
||||
|
||||
if (team) {
|
||||
filters = findTeamDocumentsFilter(status, team, visibilityFilters);
|
||||
}
|
||||
|
||||
if (filters === null) {
|
||||
return {
|
||||
@@ -148,9 +188,7 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
...termFilters,
|
||||
...filters,
|
||||
...deletedFilter,
|
||||
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }],
|
||||
};
|
||||
|
||||
if (period) {
|
||||
@@ -333,6 +371,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
const findTeamDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus,
|
||||
team: Team & { teamEmail: TeamEmail | null },
|
||||
visibilityFilters: Prisma.DocumentWhereInput[],
|
||||
) => {
|
||||
const teamEmail = team.teamEmail?.email ?? null;
|
||||
|
||||
@@ -343,6 +382,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -358,6 +398,7 @@ const findTeamDocumentsFilter = (
|
||||
email: teamEmail,
|
||||
},
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
});
|
||||
|
||||
// Filter to display all documents that have been sent by the team email.
|
||||
@@ -365,6 +406,7 @@ const findTeamDocumentsFilter = (
|
||||
User: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,6 +431,7 @@ const findTeamDocumentsFilter = (
|
||||
},
|
||||
},
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
};
|
||||
})
|
||||
.with(ExtendedDocumentStatus.DRAFT, () => {
|
||||
@@ -397,6 +440,7 @@ const findTeamDocumentsFilter = (
|
||||
{
|
||||
teamId: team.id,
|
||||
status: ExtendedDocumentStatus.DRAFT,
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -407,6 +451,7 @@ const findTeamDocumentsFilter = (
|
||||
User: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -418,6 +463,7 @@ const findTeamDocumentsFilter = (
|
||||
{
|
||||
teamId: team.id,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -436,11 +482,13 @@ const findTeamDocumentsFilter = (
|
||||
},
|
||||
},
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -454,6 +502,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -466,11 +515,13 @@ const findTeamDocumentsFilter = (
|
||||
email: teamEmail,
|
||||
},
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
@@ -28,6 +32,11 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -115,5 +124,35 @@ export const getDocumentWhereInput = async ({
|
||||
);
|
||||
}
|
||||
|
||||
return documentWhereInput;
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const visibilityFilters = [
|
||||
...match(team.currentTeamMember?.role)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
{ visibility: DocumentVisibility.ADMIN },
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
])
|
||||
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...documentWhereInput,
|
||||
OR: [...visibilityFilters],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { Prisma, User } from '@documenso/prisma/client';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
|
||||
export type GetStatsInput = {
|
||||
user: User;
|
||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||
@@ -27,7 +31,7 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
||||
}
|
||||
|
||||
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
||||
? getTeamCounts({ ...options.team, createdAt })
|
||||
? getTeamCounts({ ...options.team, createdAt, currentUserEmail: user.email, userId: user.id })
|
||||
: getCounts({ user, createdAt }));
|
||||
|
||||
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||
@@ -147,7 +151,10 @@ type GetTeamCountsOption = {
|
||||
teamId: number;
|
||||
teamEmail?: string;
|
||||
senderIds?: number[];
|
||||
currentUserEmail: string;
|
||||
userId: number;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
};
|
||||
|
||||
const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
@@ -172,6 +179,49 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
let notSignedCountsGroupByArgs = null;
|
||||
let hasSignedCountsGroupByArgs = null;
|
||||
|
||||
const visibilityFilters = [
|
||||
...match(options.currentTeamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
{ visibility: DocumentVisibility.ADMIN },
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
])
|
||||
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
|
||||
];
|
||||
|
||||
ownerCountsWhereInput = {
|
||||
...ownerCountsWhereInput,
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
visibility: {
|
||||
in: visibilityFilters.map((filter) => filter.visibility),
|
||||
},
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
none: {
|
||||
email: options.currentUserEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
some: {
|
||||
email: options.currentUserEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (teamEmail) {
|
||||
ownerCountsWhereInput = {
|
||||
userId: userIdWhereClause,
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
@@ -57,7 +63,9 @@ export const sendDocument = async ({
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
Recipient: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
documentData: true,
|
||||
},
|
||||
@@ -75,6 +83,21 @@ export const sendDocument = async ({
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
let recipientsToNotify = document.Recipient;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = document.Recipient.filter(
|
||||
(r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC,
|
||||
).slice(0, 1);
|
||||
|
||||
// Secondary filter so we aren't resending if the current active recipient has already
|
||||
// received the document.
|
||||
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
|
||||
}
|
||||
|
||||
const { documentData } = document;
|
||||
|
||||
if (!documentData.data) {
|
||||
@@ -135,7 +158,7 @@ export const sendDocument = async ({
|
||||
|
||||
if (sendEmail) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@@ -19,6 +20,7 @@ export type UpdateDocumentSettingsOptions = {
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: string | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
@@ -91,10 +93,14 @@ export const updateDocumentSettings = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === document.externalId;
|
||||
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
@@ -165,6 +171,21 @@ export const updateDocumentSettings = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
@@ -182,7 +203,8 @@ export const updateDocumentSettings = async ({
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
@@ -20,22 +22,15 @@ import {
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
documentId: number;
|
||||
fields: {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
}[];
|
||||
fields: FieldData[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
@@ -63,6 +58,9 @@ export const setFieldsForDocument = async ({
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@@ -97,21 +95,36 @@ export const setFieldsForDocument = async ({
|
||||
(existingField) => !fields.find((field) => field.id === existingField.id),
|
||||
);
|
||||
|
||||
const linkedFields = fields
|
||||
.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
return {
|
||||
...field,
|
||||
_persisted: existing,
|
||||
};
|
||||
})
|
||||
.filter((field) => {
|
||||
return (
|
||||
field._persisted?.Recipient?.sendStatus !== SendStatus.SENT &&
|
||||
field._persisted?.Recipient?.signingStatus !== SigningStatus.SIGNED
|
||||
const recipient = document.Recipient.find(
|
||||
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
|
||||
);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, `Recipient not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
// Check whether the existing field can be modified.
|
||||
if (
|
||||
existing &&
|
||||
hasFieldBeenChanged(existing, field) &&
|
||||
!canRecipientFieldsBeModified(recipient, existingFields)
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.INVALID_REQUEST,
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
_persisted: existing,
|
||||
_recipient: recipient,
|
||||
};
|
||||
});
|
||||
|
||||
const persistedFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
@@ -322,3 +335,33 @@ export const setFieldsForDocument = async ({
|
||||
|
||||
return [...filteredFields, ...persistedFields];
|
||||
};
|
||||
|
||||
/**
|
||||
* If you change this you MUST update the `hasFieldBeenChanged` function.
|
||||
*/
|
||||
type FieldData = {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
};
|
||||
|
||||
const hasFieldBeenChanged = (field: Field, newFieldData: FieldData) => {
|
||||
const currentFieldMeta = field.fieldMeta || null;
|
||||
const newFieldMeta = newFieldData.fieldMeta || null;
|
||||
|
||||
return (
|
||||
field.type !== newFieldData.type ||
|
||||
field.page !== newFieldData.pageNumber ||
|
||||
field.positionX.toNumber() !== newFieldData.pageX ||
|
||||
field.positionY.toNumber() !== newFieldData.pageY ||
|
||||
field.width.toNumber() !== newFieldData.pageWidth ||
|
||||
field.height.toNumber() !== newFieldData.pageHeight ||
|
||||
!isDeepEqual(currentFieldMeta, newFieldMeta)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,5 +4,5 @@ import { cookies } from 'next/headers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export const switchI18NLanguage = async (lang: string) => {
|
||||
cookies().set('i18n', lang);
|
||||
cookies().set('language', lang);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSigningOrder, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type GetIsRecipientTurnOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOptions) {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
orderBy: {
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const recipients = document.Recipient;
|
||||
|
||||
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
|
||||
|
||||
if (currentRecipientIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < currentRecipientIndex; i++) {
|
||||
if (recipients[i].signingStatus !== SigningStatus.SIGNED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import {
|
||||
type TRecipientActionAuthTypes,
|
||||
@@ -16,19 +21,16 @@ import type { Recipient } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface SetRecipientsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
recipients: {
|
||||
id?: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
}[];
|
||||
recipients: RecipientData[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
@@ -58,6 +60,9 @@ export const setRecipientsForDocument = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Field: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@@ -115,25 +120,28 @@ export const setRecipientsForDocument = async ({
|
||||
),
|
||||
);
|
||||
|
||||
const linkedRecipients = normalizedRecipients
|
||||
.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
);
|
||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
);
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
_persisted: existing,
|
||||
};
|
||||
})
|
||||
.filter((recipient) => {
|
||||
return (
|
||||
recipient._persisted?.role === RecipientRole.CC ||
|
||||
(recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
|
||||
if (
|
||||
existing &&
|
||||
hasRecipientBeenChanged(existing, recipient) &&
|
||||
!canRecipientBeModified(existing, document.Field)
|
||||
) {
|
||||
throw new AppError(
|
||||
AppErrorCode.INVALID_REQUEST,
|
||||
'Cannot modify a recipient who has already interacted with the document',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
_persisted: existing,
|
||||
};
|
||||
});
|
||||
|
||||
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
@@ -156,6 +164,7 @@ export const setRecipientsForDocument = async ({
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
@@ -166,6 +175,7 @@ export const setRecipientsForDocument = async ({
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
@@ -265,6 +275,37 @@ export const setRecipientsForDocument = async ({
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
// Send emails to deleted recipients.
|
||||
await Promise.all(
|
||||
removedRecipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: user.name || undefined,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: 'You have been removed from a document',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter out recipients that have been removed or have been updated.
|
||||
@@ -281,3 +322,27 @@ export const setRecipientsForDocument = async ({
|
||||
|
||||
return [...filteredRecipients, ...persistedRecipients];
|
||||
};
|
||||
|
||||
/**
|
||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
||||
*/
|
||||
type RecipientData = {
|
||||
id?: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
};
|
||||
|
||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
return (
|
||||
recipient.email !== newRecipientData.email ||
|
||||
recipient.name !== newRecipientData.name ||
|
||||
recipient.role !== newRecipientData.role ||
|
||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||
authOptions.actionAuth !== newRecipientData.actionAuth
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export type SetRecipientsForTemplateOptions = {
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
}[];
|
||||
};
|
||||
@@ -162,6 +163,7 @@ export const setRecipientsForTemplate = async ({
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
templateId,
|
||||
authOptions,
|
||||
},
|
||||
@@ -169,6 +171,7 @@ export const setRecipientsForTemplate = async ({
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
templateId,
|
||||
authOptions,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type UpdateRecipientOptions = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
@@ -30,6 +31,7 @@ export const updateRecipient = async ({
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
signingOrder,
|
||||
actionAuth,
|
||||
userId,
|
||||
teamId,
|
||||
@@ -112,6 +114,7 @@ export const updateRecipient = async ({
|
||||
email: email?.toLowerCase() ?? recipient.email,
|
||||
name: name ?? recipient.name,
|
||||
role: role ?? recipient.role,
|
||||
signingOrder,
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: recipientAuthOptions.accessAuth,
|
||||
actionAuth: actionAuth ?? null,
|
||||
|
||||
@@ -6,6 +6,8 @@ export type GetTeamByIdOptions = {
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export type GetTeamResponse = Awaited<ReturnType<typeof getTeamById>>;
|
||||
|
||||
/**
|
||||
* Get a team given a teamId.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ export type CreateDocumentFromTemplateLegacyOptions = {
|
||||
name?: string;
|
||||
email: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -73,6 +74,7 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
})),
|
||||
},
|
||||
@@ -129,12 +131,14 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
},
|
||||
create: {
|
||||
documentId: document.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import type { DocumentSigningOrder, Field } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
type Recipient,
|
||||
@@ -41,6 +41,7 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
id: number;
|
||||
name?: string;
|
||||
email: string;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
|
||||
/**
|
||||
@@ -54,6 +55,7 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
};
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
@@ -134,6 +136,7 @@ export const createDocumentFromTemplate = async ({
|
||||
name: foundRecipient ? foundRecipient.name ?? '' : templateRecipient.name,
|
||||
email: foundRecipient ? foundRecipient.email : templateRecipient.email,
|
||||
role: templateRecipient.role,
|
||||
signingOrder: foundRecipient?.signingOrder ?? templateRecipient.signingOrder,
|
||||
authOptions: templateRecipient.authOptions,
|
||||
};
|
||||
});
|
||||
@@ -168,6 +171,8 @@ export const createDocumentFromTemplate = async ({
|
||||
password: override?.password || template.templateMeta?.password,
|
||||
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||
signingOrder:
|
||||
override?.signingOrder || template.templateMeta?.signingOrder || undefined,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const updateTemplateSettings = async ({
|
||||
meta,
|
||||
data,
|
||||
}: UpdateTemplateSettingsOptions) => {
|
||||
if (Object.values(data).length === 0) {
|
||||
if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-05 06:04\n"
|
||||
"PO-Revision-Date: 2024-09-16 16:03\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -60,12 +60,12 @@ msgstr "<0>Passkey erforderlich</0> - Der Empfänger muss ein Konto haben und de
|
||||
msgid "Add a document"
|
||||
msgstr "Dokument hinzufügen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:305
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:336
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:339
|
||||
msgid "Add a URL to redirect the user to once the document is signed"
|
||||
msgstr "Fügen Sie eine URL hinzu, um den Benutzer nach der Unterzeichnung des Dokuments weiterzuleiten"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:217
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:248
|
||||
msgid "Add an external ID to the document. This can be used to identify the document in external systems."
|
||||
msgstr "Fügen Sie dem Dokument eine externe ID hinzu. Diese kann verwendet werden, um das Dokument in externen Systemen zu identifizieren."
|
||||
|
||||
@@ -82,19 +82,19 @@ msgstr "Weitere Option hinzufügen"
|
||||
msgid "Add another value"
|
||||
msgstr "Weiteren Wert hinzufügen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:359
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:662
|
||||
msgid "Add myself"
|
||||
msgstr "Mich selbst hinzufügen"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:369
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:637
|
||||
msgid "Add Myself"
|
||||
msgstr "Mich hinzufügen"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:355
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:623
|
||||
msgid "Add Placeholder Recipient"
|
||||
msgstr "Platzhalterempfänger hinzufügen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:348
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:651
|
||||
msgid "Add Signer"
|
||||
msgstr "Unterzeichner hinzufügen"
|
||||
|
||||
@@ -110,12 +110,12 @@ msgstr "Text zum Feld hinzufügen"
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:199
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:230
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:238
|
||||
msgid "Advanced Options"
|
||||
msgstr "Erweiterte Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:527
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Erweiterte Einstellungen"
|
||||
@@ -142,21 +142,25 @@ msgstr "Genehmigung"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:276
|
||||
msgid "Black"
|
||||
msgstr ""
|
||||
msgstr "Schwarz"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:290
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
msgstr "Blau"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:287
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:164
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:193
|
||||
msgid "Cannot remove signer"
|
||||
msgstr "Unterzeichner kann nicht entfernt werden"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:221
|
||||
#~ msgid "Cannot update signer because they have already signed a field"
|
||||
#~ msgstr ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:17
|
||||
msgid "Cc"
|
||||
msgstr "Cc"
|
||||
@@ -174,7 +178,7 @@ msgstr "CC'd"
|
||||
msgid "Character Limit"
|
||||
msgstr "Zeichenbeschränkung"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:950
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
@@ -203,7 +207,7 @@ msgstr "Schließen"
|
||||
msgid "Configure Direct Recipient"
|
||||
msgstr "Direkten Empfänger konfigurieren"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:528
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Konfigurieren Sie das Feld {0}"
|
||||
@@ -220,22 +224,22 @@ msgstr "In die Zwischenablage kopiert"
|
||||
msgid "Custom Text"
|
||||
msgstr "Benutzerdefinierter Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:846
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:240
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:271
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:279
|
||||
msgid "Date Format"
|
||||
msgstr "Datumsformat"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:312
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:570
|
||||
msgid "Direct link receiver"
|
||||
msgstr "Empfänger des direkten Links"
|
||||
|
||||
#: packages/ui/components/document/document-global-auth-access-select.tsx:62
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:166
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:174
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:151
|
||||
msgid "Document access"
|
||||
msgstr "Dokumentenzugriff"
|
||||
@@ -252,7 +256,7 @@ msgstr "Herunterladen"
|
||||
msgid "Drag & drop your PDF here."
|
||||
msgstr "Ziehen Sie Ihr PDF hierher."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:976
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
@@ -261,13 +265,12 @@ msgstr "Dropdown"
|
||||
msgid "Dropdown options"
|
||||
msgstr "Dropdown-Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:776
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:794
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:232
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:239
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:500
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:632
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:463
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:470
|
||||
msgid "Email"
|
||||
msgstr "E-Mail"
|
||||
|
||||
@@ -279,6 +282,11 @@ msgstr "E-Mail-Optionen"
|
||||
msgid "Enable Direct Link Signing"
|
||||
msgstr "Direktlink-Signierung aktivieren"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:401
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:362
|
||||
msgid "Enable signing order"
|
||||
msgstr "Aktiviere die Signaturreihenfolge"
|
||||
|
||||
#: packages/ui/primitives/document-password-dialog.tsx:84
|
||||
msgid "Enter password"
|
||||
msgstr "Passwort eingeben"
|
||||
@@ -287,7 +295,7 @@ msgstr "Passwort eingeben"
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:210
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:241
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:249
|
||||
msgid "External ID"
|
||||
msgstr "Externe ID"
|
||||
@@ -322,7 +330,7 @@ msgstr "Zurück"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:297
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
msgstr "Grün"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:72
|
||||
msgid "I am a signer of this document"
|
||||
@@ -376,12 +384,13 @@ msgstr "Nachricht <0>(Optional)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:820
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:535
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:541
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:658
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:498
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:504
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
@@ -397,12 +406,12 @@ msgstr "Muss unterzeichnen"
|
||||
msgid "Needs to view"
|
||||
msgstr "Muss sehen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:631
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:497
|
||||
msgid "No recipient matching this description was found."
|
||||
msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:629
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:647
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "Keine Empfänger mit dieser Rolle"
|
||||
@@ -427,7 +436,7 @@ msgstr "Kein Unterschriftsfeld gefunden"
|
||||
msgid "No value found."
|
||||
msgstr "Kein Wert gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:898
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Nummer"
|
||||
@@ -462,7 +471,7 @@ msgstr "Wählen Sie eine Zahl"
|
||||
msgid "Placeholder"
|
||||
msgstr "Platzhalter"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:924
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
@@ -484,21 +493,21 @@ msgid "Receives copy"
|
||||
msgstr "Erhält Kopie"
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:184
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:215
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:169
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Empfängeraktion Authentifizierung"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:283
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
msgstr "Rot"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:329
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
msgstr "Weiterleitungs-URL"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:996
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1027
|
||||
msgid "Remove"
|
||||
msgstr "Entfernen"
|
||||
|
||||
@@ -522,6 +531,10 @@ msgstr "Speichern"
|
||||
msgid "Save Template"
|
||||
msgstr "Vorlage speichern"
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx:34
|
||||
msgid "Search languages..."
|
||||
msgstr "Sprachen suchen..."
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105
|
||||
msgid "Select"
|
||||
msgstr "Auswählen"
|
||||
@@ -556,8 +569,8 @@ msgstr "Unterschriftenkarte teilen"
|
||||
msgid "Share the Link"
|
||||
msgstr "Link teilen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:377
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:387
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:680
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:655
|
||||
msgid "Show advanced settings"
|
||||
msgstr "Erweiterte Einstellungen anzeigen"
|
||||
|
||||
@@ -565,7 +578,7 @@ msgstr "Erweiterte Einstellungen anzeigen"
|
||||
msgid "Sign"
|
||||
msgstr "Unterschreiben"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:742
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
@@ -613,7 +626,7 @@ msgstr "Einreichen"
|
||||
msgid "Template title"
|
||||
msgstr "Vorlagentitel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:872
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
@@ -674,7 +687,7 @@ msgstr "Der Name des Unterzeichners"
|
||||
msgid "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
msgstr "Dies kann überschrieben werden, indem die Authentifizierungsanforderungen im nächsten Schritt direkt für jeden Empfänger festgelegt werden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:685
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:703
|
||||
msgid "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
msgstr "Dieses Dokument wurde bereits an diesen Empfänger gesendet. Sie können diesen Empfänger nicht mehr bearbeiten."
|
||||
|
||||
@@ -682,28 +695,36 @@ msgstr "Dieses Dokument wurde bereits an diesen Empfänger gesendet. Sie können
|
||||
msgid "This document is password protected. Please enter the password to view the document."
|
||||
msgstr "Dieses Dokument ist durch ein Passwort geschützt. Bitte geben Sie das Passwort ein, um das Dokument anzusehen."
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:315
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:573
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "Dieses Feld kann nicht geändert oder gelöscht werden. Wenn Sie den direkten Link dieser Vorlage teilen oder zu Ihrem öffentlichen Profil hinzufügen, kann jeder, der darauf zugreift, seinen Namen und seine E-Mail-Adresse eingeben und die ihm zugewiesenen Felder ausfüllen."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:165
|
||||
msgid "This signer has already received the document."
|
||||
msgstr "Dieser Unterzeichner hat das Dokument bereits erhalten."
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1007
|
||||
msgid "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:195
|
||||
#~ msgid "This signer has already received the document."
|
||||
#~ msgstr "Dieser Unterzeichner hat das Dokument bereits erhalten."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:194
|
||||
msgid "This signer has already signed the document."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:48
|
||||
msgid "This will override any global settings."
|
||||
msgstr "Dies überschreibt alle globalen Einstellungen."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:274
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:305
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:309
|
||||
msgid "Time Zone"
|
||||
msgstr "Zeitzone"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:145
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:153
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:971
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:990
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:828
|
||||
msgid "To proceed further, please set at least one value for the {0} field."
|
||||
msgstr "Um fortzufahren, legen Sie bitte mindestens einen Wert für das Feld {0} fest."
|
||||
@@ -752,7 +773,7 @@ msgstr "Viewing"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
|
||||
#~ msgid "White"
|
||||
#~ msgstr ""
|
||||
#~ msgstr "White"
|
||||
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
|
||||
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-05 06:04\n"
|
||||
"PO-Revision-Date: 2024-09-16 14:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -430,8 +430,8 @@ msgid "Save $60 or $120"
|
||||
msgstr "Sparen Sie $60 oder $120"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
|
||||
msgid "Search languages..."
|
||||
msgstr "Sprachen suchen..."
|
||||
#~ msgid "Search languages..."
|
||||
#~ msgstr "Search languages..."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
|
||||
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
|
||||
|
||||
File diff suppressed because one or more lines are too long
+191
-153
File diff suppressed because it is too large
Load Diff
@@ -55,12 +55,12 @@ msgstr "<0>Require passkey</0> - The recipient must have an account and passkey
|
||||
msgid "Add a document"
|
||||
msgstr "Add a document"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:305
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:336
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:339
|
||||
msgid "Add a URL to redirect the user to once the document is signed"
|
||||
msgstr "Add a URL to redirect the user to once the document is signed"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:217
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:248
|
||||
msgid "Add an external ID to the document. This can be used to identify the document in external systems."
|
||||
msgstr "Add an external ID to the document. This can be used to identify the document in external systems."
|
||||
|
||||
@@ -77,19 +77,19 @@ msgstr "Add another option"
|
||||
msgid "Add another value"
|
||||
msgstr "Add another value"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:359
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:662
|
||||
msgid "Add myself"
|
||||
msgstr "Add myself"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:369
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:637
|
||||
msgid "Add Myself"
|
||||
msgstr "Add Myself"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:355
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:623
|
||||
msgid "Add Placeholder Recipient"
|
||||
msgstr "Add Placeholder Recipient"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:348
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:651
|
||||
msgid "Add Signer"
|
||||
msgstr "Add Signer"
|
||||
|
||||
@@ -105,12 +105,12 @@ msgstr "Add text to the field"
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:199
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:230
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:238
|
||||
msgid "Advanced Options"
|
||||
msgstr "Advanced Options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:527
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Advanced settings"
|
||||
@@ -148,10 +148,14 @@ msgstr "Blue"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:164
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:193
|
||||
msgid "Cannot remove signer"
|
||||
msgstr "Cannot remove signer"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:221
|
||||
#~ msgid "Cannot update signer because they have already signed a field"
|
||||
#~ msgstr "Cannot update signer because they have already signed a field"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:17
|
||||
msgid "Cc"
|
||||
msgstr "Cc"
|
||||
@@ -169,7 +173,7 @@ msgstr "CC'd"
|
||||
msgid "Character Limit"
|
||||
msgstr "Character Limit"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:950
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
@@ -198,7 +202,7 @@ msgstr "Close"
|
||||
msgid "Configure Direct Recipient"
|
||||
msgstr "Configure Direct Recipient"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:528
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Configure the {0} field"
|
||||
@@ -215,22 +219,22 @@ msgstr "Copied to clipboard"
|
||||
msgid "Custom Text"
|
||||
msgstr "Custom Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:846
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:240
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:271
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:279
|
||||
msgid "Date Format"
|
||||
msgstr "Date Format"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:312
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:570
|
||||
msgid "Direct link receiver"
|
||||
msgstr "Direct link receiver"
|
||||
|
||||
#: packages/ui/components/document/document-global-auth-access-select.tsx:62
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:166
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:174
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:151
|
||||
msgid "Document access"
|
||||
msgstr "Document access"
|
||||
@@ -247,7 +251,7 @@ msgstr "Download"
|
||||
msgid "Drag & drop your PDF here."
|
||||
msgstr "Drag & drop your PDF here."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:976
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
@@ -256,13 +260,12 @@ msgstr "Dropdown"
|
||||
msgid "Dropdown options"
|
||||
msgstr "Dropdown options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:776
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:794
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:232
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:239
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:500
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:632
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:210
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:217
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:463
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:470
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
@@ -274,6 +277,11 @@ msgstr "Email Options"
|
||||
msgid "Enable Direct Link Signing"
|
||||
msgstr "Enable Direct Link Signing"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:401
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:362
|
||||
msgid "Enable signing order"
|
||||
msgstr "Enable signing order"
|
||||
|
||||
#: packages/ui/primitives/document-password-dialog.tsx:84
|
||||
msgid "Enter password"
|
||||
msgstr "Enter password"
|
||||
@@ -282,7 +290,7 @@ msgstr "Enter password"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:210
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:241
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:249
|
||||
msgid "External ID"
|
||||
msgstr "External ID"
|
||||
@@ -371,12 +379,13 @@ msgstr "Message <0>(Optional)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:820
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:535
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:541
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:658
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:245
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:251
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:498
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:504
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
@@ -392,12 +401,12 @@ msgstr "Needs to sign"
|
||||
msgid "Needs to view"
|
||||
msgstr "Needs to view"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:631
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:497
|
||||
msgid "No recipient matching this description was found."
|
||||
msgstr "No recipient matching this description was found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:629
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:647
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "No recipients with this role"
|
||||
@@ -422,7 +431,7 @@ msgstr "No signature field found"
|
||||
msgid "No value found."
|
||||
msgstr "No value found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:898
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Number"
|
||||
@@ -457,7 +466,7 @@ msgstr "Pick a number"
|
||||
msgid "Placeholder"
|
||||
msgstr "Placeholder"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:924
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
@@ -479,7 +488,7 @@ msgid "Receives copy"
|
||||
msgstr "Receives copy"
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:184
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:215
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:169
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Recipient action authentication"
|
||||
@@ -488,12 +497,12 @@ msgstr "Recipient action authentication"
|
||||
msgid "Red"
|
||||
msgstr "Red"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:329
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
msgstr "Redirect URL"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:996
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1027
|
||||
msgid "Remove"
|
||||
msgstr "Remove"
|
||||
|
||||
@@ -517,6 +526,10 @@ msgstr "Save"
|
||||
msgid "Save Template"
|
||||
msgstr "Save Template"
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx:34
|
||||
msgid "Search languages..."
|
||||
msgstr "Search languages..."
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx:105
|
||||
msgid "Select"
|
||||
msgstr "Select"
|
||||
@@ -551,8 +564,8 @@ msgstr "Share Signature Card"
|
||||
msgid "Share the Link"
|
||||
msgstr "Share the Link"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:377
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:387
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:680
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:655
|
||||
msgid "Show advanced settings"
|
||||
msgstr "Show advanced settings"
|
||||
|
||||
@@ -560,7 +573,7 @@ msgstr "Show advanced settings"
|
||||
msgid "Sign"
|
||||
msgstr "Sign"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:742
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
@@ -608,7 +621,7 @@ msgstr "Submit"
|
||||
msgid "Template title"
|
||||
msgstr "Template title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:872
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
@@ -669,7 +682,7 @@ msgstr "The signer's name"
|
||||
msgid "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
msgstr "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:685
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:703
|
||||
msgid "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
msgstr "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
|
||||
@@ -677,28 +690,36 @@ msgstr "This document has already been sent to this recipient. You can no longer
|
||||
msgid "This document is password protected. Please enter the password to view the document."
|
||||
msgstr "This document is password protected. Please enter the password to view the document."
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:315
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:573
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1007
|
||||
msgid "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
msgstr "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:165
|
||||
msgid "This signer has already received the document."
|
||||
msgstr "This signer has already received the document."
|
||||
#~ msgid "This signer has already received the document."
|
||||
#~ msgstr "This signer has already received the document."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:194
|
||||
msgid "This signer has already signed the document."
|
||||
msgstr "This signer has already signed the document."
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:48
|
||||
msgid "This will override any global settings."
|
||||
msgstr "This will override any global settings."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:274
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:305
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:309
|
||||
msgid "Time Zone"
|
||||
msgstr "Time Zone"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:145
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:153
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:971
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:990
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:828
|
||||
msgid "To proceed further, please set at least one value for the {0} field."
|
||||
msgstr "To proceed further, please set at least one value for the {0} field."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -425,8 +425,8 @@ msgid "Save $60 or $120"
|
||||
msgstr "Save $60 or $120"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
|
||||
msgid "Search languages..."
|
||||
msgstr "Search languages..."
|
||||
#~ msgid "Search languages..."
|
||||
#~ msgstr "Search languages..."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
|
||||
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user