mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
37 Commits
v1.7.0-rc.
...
feat/save-
| Author | SHA1 | Date | |
|---|---|---|---|
| d1528fd63b | |||
| 85012ae991 | |||
| 357bdd374f | |||
| 7b06b68572 | |||
| 9ee89346b1 | |||
| 77da7847d9 | |||
| c36306d2c9 | |||
| f6f893fbf7 | |||
| e1b2206d28 | |||
| ad135b72d8 | |||
| e81023f8d4 | |||
| bfb09e7928 | |||
| d7e5aa1d26 | |||
| 8cb3ad4f3c | |||
| 6c3acb1c2d | |||
| 3f82720383 | |||
| a6f93698b4 | |||
| bdc4ec1a31 | |||
| bc471fcd9f | |||
| ff4c9616fc | |||
| f4e98ae03a | |||
| 0298e79e8c | |||
| 8ab7464b84 | |||
| ad4cff937d | |||
| fd004f5f24 | |||
| bb222cc0fc | |||
| f627cbc7db | |||
| 67be4fdb45 | |||
| c8b7d13b4f | |||
| 2b4699bb61 | |||
| 39603fc150 | |||
| 7f2a8deda3 | |||
| f24a33c182 | |||
| 4a8f32a44f | |||
| bbf2ed7154 | |||
| 5ce7f4adcc | |||
| d74aca2aa6 |
@ -27,6 +27,8 @@ NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
||||
# [[URLS]]
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
|
||||
# URL used by the web app to request itself (e.g. local background jobs)
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
|
||||
|
||||
# [[DATABASE]]
|
||||
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||
|
||||
3
.github/workflows/e2e-tests.yml
vendored
3
.github/workflows/e2e-tests.yml
vendored
@ -32,6 +32,9 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run ci
|
||||
env:
|
||||
# Needed since we use next start which will set the NODE_ENV to production
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: './example/cert.p12'
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
||||
@ -303,6 +303,10 @@ WantedBy=multi-user.target
|
||||
|
||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||
|
||||
## Elestio
|
||||
|
||||
[](https://elest.io/open-source/documenso)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I'm not receiving any emails when using the developer quickstart.
|
||||
|
||||
@ -3,5 +3,6 @@
|
||||
"quickstart": "Developer Quickstart",
|
||||
"manual": "Manual Setup",
|
||||
"gitpod": "Gitpod",
|
||||
"signing-certificate": "Signing Certificate"
|
||||
}
|
||||
"signing-certificate": "Signing Certificate",
|
||||
"translations": "Translations"
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
---
|
||||
title: Translations
|
||||
description: Handling translations in code.
|
||||
---
|
||||
|
||||
# About
|
||||
|
||||
Documenso uses the following stack to handle translations:
|
||||
|
||||
- [Lingui](https://lingui.dev/) - React i10n library
|
||||
- [Crowdin](https://crowdin.com/) - Handles syncing translations
|
||||
- [OpenAI](https://openai.com/) - Provides AI translations
|
||||
|
||||
Additional reading can be found in the [Lingui documentation](https://lingui.dev/introduction).
|
||||
|
||||
## Requirements
|
||||
|
||||
You **must** insert **`setupI18nSSR()`** when creating any of the following files:
|
||||
|
||||
- Server layout.tsx
|
||||
- Server page.tsx
|
||||
- Server loading.tsx
|
||||
|
||||
Server meaning it does not have `'use client'` in it.
|
||||
|
||||
```tsx
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export default function SomePage() {
|
||||
setupI18nSSR(); // Required if there are translations within the page, or nested in components.
|
||||
|
||||
// Rest of code...
|
||||
}
|
||||
```
|
||||
|
||||
Additional information can be found [here.](https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui)
|
||||
|
||||
## Quick guide
|
||||
|
||||
If you require more in-depth information, please see the [Lingui documentation](https://lingui.dev/introduction).
|
||||
|
||||
### HTML
|
||||
|
||||
Wrap all text to translate in **`<Trans></Trans>`** tags exported from **@lingui/macro** (not @lingui/react).
|
||||
|
||||
```html
|
||||
<h1>
|
||||
<Trans>Title</Trans>
|
||||
</h1>
|
||||
```
|
||||
|
||||
For text that is broken into elements, but represent a whole sentence, you must wrap it in a Trans tag so ensure the full message is extracted correctly.
|
||||
|
||||
```html
|
||||
<h1>
|
||||
<Trans>
|
||||
This is one
|
||||
<span className="text-foreground/60">full</span>
|
||||
<a href="https://documenso.com">sentence</a>
|
||||
</Trans>
|
||||
</h1>
|
||||
```
|
||||
|
||||
### Constants outside of react components
|
||||
|
||||
```tsx
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
// Wrap text in msg`text to translate` when it's in a constant here, or another file/package.
|
||||
export const CONSTANT_WITH_MSG = {
|
||||
foo: msg`Hello`,
|
||||
bar: msg`World`,
|
||||
};
|
||||
|
||||
export const SomeComponent = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* This will render the correct translated text. */}
|
||||
<p>{_(CONSTANT_WITH_MSG.foo)}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Plurals
|
||||
|
||||
Lingui provides a Plural component to make it easy. See full documentation [here.](https://lingui.dev/ref/macro#plural-1)
|
||||
|
||||
```tsx
|
||||
// Basic usage.
|
||||
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
|
||||
```
|
||||
|
||||
### Dates
|
||||
|
||||
Lingui provides a [DateTime instance](https://lingui.dev/ref/core#i18n.date) with the configured locale.
|
||||
|
||||
#### Server components
|
||||
|
||||
Note that the i18n instance is coming from **setupI18nSSR**.
|
||||
|
||||
```tsx
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
export const SomeComponent = () => {
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
|
||||
};
|
||||
```
|
||||
|
||||
#### Client components
|
||||
|
||||
Note that the i18n instance is coming from the **import**.
|
||||
|
||||
```tsx
|
||||
import { i18n } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
export const SomeComponent = () => {
|
||||
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
|
||||
};
|
||||
```
|
||||
@ -5,6 +5,8 @@ description: Learn how to self-host Documenso on your server or cloud infrastruc
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
import { CallToAction } from '@documenso/ui/components/call-to-action';
|
||||
|
||||
# Self Hosting
|
||||
|
||||
We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind!
|
||||
@ -273,3 +275,5 @@ We offer several alternative deployment methods for Documenso if you need more o
|
||||
## Koyeb
|
||||
|
||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||
|
||||
<CallToAction className="mt-12" utmSource="self-hosting" />
|
||||
|
||||
@ -3,6 +3,10 @@ title: Getting Started with Self-Hosting
|
||||
description: A step-by-step guide to setting up and hosting your own Documenso instance.
|
||||
---
|
||||
|
||||
import { CallToAction } from '@documenso/ui/components/call-to-action';
|
||||
|
||||
# Getting Started with Self-Hosting
|
||||
|
||||
This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses).
|
||||
|
||||
<CallToAction className="mt-12" utmSource="self-hosting" />
|
||||
|
||||
72
apps/marketing/content/blog/introducing-embedding.mdx
Normal file
72
apps/marketing/content/blog/introducing-embedding.mdx
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
title: 'Introducing Embedding Support for Documenso'
|
||||
description: 'Embedding is now here! Learn how we built it and how it can be used to bring e-signing to your own applications.'
|
||||
authorName: 'Lucas Smith'
|
||||
authorImage: '/blog/blog-author-lucas.png'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-09-06
|
||||
tags:
|
||||
- Development
|
||||
---
|
||||
|
||||
When we first launched Documenso, one of the most requested features was embedding. We knew it was important and aligned with our desire to not just be a e-signing application but to instead provide the e-signature infrastructure for the web and beyond.
|
||||
|
||||
With that said, we decided to hold off initially so we could focus on building a solid, well-featured core application. Looking back, this was definitely the right call. Embedding is only as good as the features behind it, and we didn't want to release something that wasn't ready to meet user and developer expectations.
|
||||
|
||||
Over the past year, we've been busy adding tons of new features and reaching new levels of compliance, like 21 CFR Part 11. We've also introduced [new fields](/blog/introducing-advanced-signing-fields), [built out an API](/blog/public-api), [added webhooks, integrations with Zapier](/blog/launch-week-2-day-4), and a lot more.
|
||||
|
||||
Now that we've laid a solid foundation, it's finally time to focus on embedding, the top-requested feature from both our users and those self-hosting our platform.
|
||||
|
||||
## Why Embedding Took Time
|
||||
|
||||
In previous projects, I’ve often seen embedding built by bundling components for use in a client’s website or app. This method gives users maximum flexibility for styling and behavior, while avoiding certain cross-origin issues. However, it can also introduce problems like code conflicts or performance bottlenecks. For example, third-party tools such as Google Tag Manager (GTM) or other marketing scripts can interfere with your SDK. Additionally, the SDK must remain lightweight to avoid slowing down the client’s page.
|
||||
|
||||
For Documenso, we decided to explore a different approach. After carefully researching our options, we opted for an iframe-based solution. While iframes are typically less flexible—especially when it comes to theming or passing pre-filled data containing personally identifiable information (PII)—we identified ways to mitigate these concerns.
|
||||
|
||||
One of the biggest challenges was ensuring that we could pass sensitive data, like emails for pre-filling forms, without exposing PII to our server. To solve this, we used [fragment identifiers](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) in the URL, which are processed client-side and never sent in network requests. This method ensures that PII is protected and not logged by our server or any intermediate web services.
|
||||
|
||||
### Using the PostMessage API for Communication
|
||||
|
||||
To maintain a high level of interactivity, our iframes communicate with the parent window using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This allows us to notify the parent app when specific events occur inside the iframe, creating a more dynamic user experience and bridging the gap between our iframe-based solution and typical fat SDKs.
|
||||
|
||||
Additionally, props are passed into the iframe via the [fragment identifier](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) of the URL. This avoids the need for complex two-way data synchronization between the parent and child frames, making the system stable and more reliable.
|
||||
|
||||
### Building the Embeds with Mitosis
|
||||
|
||||
Given that our iframe solution is quite lightweight, we saw this as a great opportunity to experiment with [Mitosis](https://mitosis.builder.io/) which would let us do something truly special. For those unfamiliar, Mitosis is a project by Builder.io that lets you write components once and then transpile them into a variety of frameworks like React, Vue, and Svelte.
|
||||
|
||||
We used Mitosis to build two key components: a direct template embed and a document signing embed. The direct template allows users to use a template as if it were an evergreen document—meaning that, when someone completes the template, a new document is automatically generated. This is the use case we expect most users to adopt for embedding. For more advanced workflows, we also offer a document signing embed, which can handle multi-recipient workflows and other complex scenarios intended for use in deeper, rich integrations.
|
||||
|
||||
Mitosis allowed us to quickly target several popular frameworks, including [React](https://www.npmjs.com/package/@documenso/embed-react), [Preact](https://www.npmjs.com/package/@documenso/embed-preact), [Vue](https://www.npmjs.com/package/@documenso/embed-vue), [Svelte](https://www.npmjs.com/package/@documenso/embed-svelte), and [SolidJS](https://www.npmjs.com/package/@documenso/embed-solid).
|
||||
|
||||
I had also hoped to include Angular, but while Mitosis makes it really easy to transpile component, we still have to take care of bundling and packaging the resulting component ourselves. While the above frameworks can all be bundled using Vite.js, Angular still has it's own set of tooling that we would need to learn and use. Given this constraint we opted to put Angular on hold for now while we wait for the newer Vite.js support to mature.
|
||||
|
||||
### Challenges and Lessons with Mitosis and more
|
||||
|
||||
While our experience with Mitosis was largely positive, there were some challenges along the way. For instance, certain state properties with the same names as props caused issues during the transpilation process, leading to type errors and unexpected transpilation results with some targets.
|
||||
|
||||
This was also a challenge since our initial implementation of the two components had some minor separation of concerns which also resulted in some transpilation issues with some targets. We addressed this by removing the separation of concerns for now since it was mostly for show rather than out of necessity.
|
||||
|
||||
On top of that, packaging and publishing the embeds posed its own set of challenges, particularly given the growing complexity of JavaScript package management. Tools like [Publint](https://www.npmjs.com/package/publint) helped streamline the process by ensuring we followed best practices for both CommonJS and ESM formats.
|
||||
|
||||
### To the Future, The Documenso Platform
|
||||
|
||||
With the embedding feature now in place, we're excited to continue expanding Documenso's capabilities. Embeds are just the beginning of what we're calling the Documenso platform. Through our user research, we've learned that while many businesses appreciate having a flexible e-signature solution, they're even more interested in using our tools to build signing functionality directly into their own apps—without worrying about the technical complexities of compliance and security that come with e-signing.
|
||||
|
||||
Over the coming months, we'll be working on enhancing our API, strengthening integrations with tools like Zapier, and improving our webhook system. Our goal is to give users the ability to embed e-signatures and document management wherever they need it, whether that's through self-hosting or by using Documenso as a platform. We can't wait to see how our users and self-hosters leverage these new capabilities!
|
||||
|
||||
### Ready to Get Started?
|
||||
|
||||
If you're ready to embed document signing into your own app or website, check out our [Embedding Documentation](https://docs.documenso.com/developers/embedding?utm_source=blog&utm_campaign=introducing-embedding) to see how easy it is to get started. You'll find everything you need to get started today!
|
||||
|
||||
<video
|
||||
src="/blog/introducing-embedding/embedding-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
We're always here to help! If you have questions or need support, join our [Discord](https://documen.so/discord) or [book a demo](https://documen.so/book-a-demo). We'd love to hear how you're using Documenso or wanting to use Documenso to enhance your workflow.
|
||||
|
||||
Stay tuned for more updates as we continue to evolve the Documenso platform and make it even easier to bring document signing into your workflows.
|
||||
@ -8,6 +8,80 @@ Check out what's new in the latest version and read our thoughts on it. For more
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.7.0: Embedded Signing, Copy and Paste, and More
|
||||
|
||||
We're thrilled to announce the release of Documenso v1.7.0, packed with exciting new features and improvements that enhance document signing flexibility, user experience, and global accessibility.
|
||||
|
||||
We're excited to see what you'll create with this release and we'd love to hear your feedback. Let's dive into the highlights:
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
### Embedded Signing Experience
|
||||
|
||||
Take your document signing to the next level with our new embedded signing feature. Now you can seamlessly integrate Documenso's signing process directly into your own website or application, providing a smooth, branded experience for your users.
|
||||
|
||||
<video
|
||||
src="/blog/introducing-embedding/embedding-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
controls
|
||||
/>
|
||||
|
||||
Check out our [Embedding documentation](https://docs.documenso.com/developers/embedding) to learn more about how to get started.
|
||||
|
||||
### Copy and Paste Fields
|
||||
|
||||
Streamline your document preparation with our new copy and paste functionality for fields. This feature allows you to quickly duplicate fields across your document, saving time and ensuring consistency in your templates.
|
||||
|
||||
### Customizable Signature Colors
|
||||
|
||||
Recipients can now select a signature color from our list of available colors, supporting workflows where specific colors are required for each recipient, location, or document.
|
||||
|
||||
### Enhanced Internationalization (i18n)
|
||||
|
||||
Following on from our last release we've now expanded our i18n support to the main web application. We haven't yet added support for any additional languages but that will be coming quickly now that we have completed the hard work of wrapping all of our content in our new i18n system.
|
||||
|
||||
These enhancements make Documenso more accessible to users worldwide.
|
||||
|
||||
## 🔧 Other Improvements
|
||||
|
||||
- **API Enhancements**:
|
||||
|
||||
- New endpoint to prefill fields via API
|
||||
- Updated createFields API endpoint for more flexibility
|
||||
- Automatically set public profile URL for OIDC users
|
||||
|
||||
- **Security and Performance**:
|
||||
|
||||
- Document sealing moved to a background job for improved performance
|
||||
- Disable 2FA with backup codes for enhanced account recovery options
|
||||
- Extended lifespan for invites and confirmations
|
||||
|
||||
- **User Experience**:
|
||||
|
||||
- Updated email templates to reflect team-specific information
|
||||
- Fixed issues with dialog closing on page refresh
|
||||
- Improved field editing in document templates
|
||||
|
||||
- **Other Items**:
|
||||
- Added Elestio as a one-click deploy option
|
||||
- Updated README for manual self-hosting
|
||||
- New environment variable for internal webapp URL configuration
|
||||
|
||||
## 📚 New Content
|
||||
|
||||
- [Advanced fields article to help you make the most of Documenso's capabilities](/blog/introducing-advanced-signing-fields)
|
||||
- [Embedding blog post to guide you through how we implemented embedding](/blog/introducing-embedding)
|
||||
|
||||
## 👏 Community Contributions
|
||||
|
||||
A big thank you to our vibrant community! This release includes contributions from several new contributors, further enriching Documenso's capabilities.
|
||||
|
||||
We're excited to see how you'll use these new features to streamline your document workflows. As always, we appreciate your feedback and support in making Documenso the best open-source document signing solution available.
|
||||
|
||||
Enjoy exploring v1.7.0!
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.6.1: Internationalization, Enhanced OIDC, and More
|
||||
|
||||
We're excited to announce the release of Documenso v1.6.1, which brings several improvements to enhance your document signing experience. Here are the key updates:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
3
apps/marketing/process-env.d.ts
vendored
3
apps/marketing/process-env.d.ts
vendored
@ -2,7 +2,8 @@ declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
NEXT_PUBLIC_WEBAPP_URL?: string;
|
||||
NEXT_PUBLIC_MARKETING_URL?: string;
|
||||
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
|
||||
|
||||
NEXT_PRIVATE_DATABASE_URL: string;
|
||||
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,6 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import { AxiomWebVitals } from 'next-axiom';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
@ -10,8 +9,6 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
|
||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
|
||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -59,25 +56,7 @@ export function generateMetadata() {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
||||
|
||||
// Should be safe to remove when we upgrade NextJS.
|
||||
// https://github.com/vercel/next.js/pull/65008
|
||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
||||
// during the same render.
|
||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
||||
if (!cookies().get('i18n')) {
|
||||
const setCookieValue = headers().get('set-cookie');
|
||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
||||
|
||||
if (i18nCookie) {
|
||||
const i18n = i18nCookie.split('=')[1];
|
||||
|
||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
||||
const { lang, locales, i18n } = setupI18nSSR();
|
||||
|
||||
return (
|
||||
<html
|
||||
@ -105,7 +84,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<PlausibleProvider>
|
||||
<TrpcProvider>
|
||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
||||
<I18nClientProvider
|
||||
initialLocaleData={{ lang, locales }}
|
||||
initialMessages={i18n.messages}
|
||||
>
|
||||
{children}
|
||||
</I18nClientProvider>
|
||||
</TrpcProvider>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,39 +0,0 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
||||
|
||||
export default function middleware(req: NextRequest) {
|
||||
const lang = extractSupportedLanguage({
|
||||
headers: req.headers,
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
response.cookies.set('i18n', lang);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - ingest (analytics)
|
||||
* - site.webmanifest
|
||||
*/
|
||||
{
|
||||
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
|
||||
missing: [
|
||||
{ type: 'header', key: 'next-router-prefetch' },
|
||||
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
1
apps/web/process-env.d.ts
vendored
1
apps/web/process-env.d.ts
vendored
@ -2,6 +2,7 @@ declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
NEXT_PUBLIC_WEBAPP_URL?: string;
|
||||
NEXT_PUBLIC_MARKETING_URL?: string;
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
|
||||
|
||||
NEXT_PRIVATE_DATABASE_URL: string;
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { AdminActions } from './admin-actions';
|
||||
import { RecipientItem } from './recipient-item';
|
||||
@ -25,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
|
||||
};
|
||||
|
||||
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
|
||||
setupI18nSSR();
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
const document = await getEntireDocument({ id: Number(params.id) });
|
||||
|
||||
@ -46,12 +45,11 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
|
||||
|
||||
<div className="text-muted-foreground mt-4 text-sm">
|
||||
<div>
|
||||
<Trans>Created on</Trans>:{' '}
|
||||
<LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
|
||||
<Trans>Created on</Trans>: {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Trans>Last updated at</Trans>:{' '}
|
||||
<LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
|
||||
<Trans>Last updated at</Trans>: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -21,12 +21,11 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
// export type AdminDocumentResultsProps = {};
|
||||
|
||||
export const AdminDocumentResults = () => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@ -62,7 +61,7 @@ export const AdminDocumentResults = () => {
|
||||
{
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
header: _(msg`Title`),
|
||||
@ -122,7 +121,7 @@ export const AdminDocumentResults = () => {
|
||||
{
|
||||
header: 'Last updated',
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.updatedAt),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
|
||||
export type DocumentPageViewInformationProps = {
|
||||
@ -24,21 +23,9 @@ export const DocumentPageViewInformation = ({
|
||||
}: DocumentPageViewInformationProps) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const { locale } = useLocale();
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const documentInformation = useMemo(() => {
|
||||
let createdValue = DateTime.fromJSDate(document.createdAt).toFormat('MMMM d, yyyy');
|
||||
let lastModifiedValue = DateTime.fromJSDate(document.updatedAt).toRelative();
|
||||
|
||||
if (!isMounted) {
|
||||
createdValue = DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(locale)
|
||||
.toFormat('MMMM d, yyyy');
|
||||
|
||||
lastModifiedValue = DateTime.fromJSDate(document.updatedAt).setLocale(locale).toRelative();
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
description: msg`Uploaded by`,
|
||||
@ -46,15 +33,19 @@ export const DocumentPageViewInformation = ({
|
||||
},
|
||||
{
|
||||
description: msg`Created`,
|
||||
value: createdValue,
|
||||
value: DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toFormat('MMMM d, yyyy'),
|
||||
},
|
||||
{
|
||||
description: msg`Last modified`,
|
||||
value: lastModifiedValue,
|
||||
value: DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toRelative(),
|
||||
},
|
||||
];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isMounted, document, locale, userId]);
|
||||
}, [isMounted, document, userId]);
|
||||
|
||||
return (
|
||||
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
|
||||
|
||||
@ -20,8 +20,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export type DocumentLogsDataTableProps = {
|
||||
documentId: number;
|
||||
};
|
||||
@ -32,7 +30,7 @@ const dateFormat: DateTimeFormatOptions = {
|
||||
};
|
||||
|
||||
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
@ -78,7 +76,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
{
|
||||
header: _(msg`Time`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
|
||||
},
|
||||
{
|
||||
header: _(msg`User`),
|
||||
@ -106,9 +104,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
header: _(msg`Action`),
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}
|
||||
</span>
|
||||
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -9,7 +9,6 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { Recipient, Team } from '@documenso/prisma/client';
|
||||
@ -32,9 +31,7 @@ export type DocumentLogsPageViewProps = {
|
||||
};
|
||||
|
||||
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const locale = getLocale();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const { id } = params;
|
||||
|
||||
@ -87,13 +84,13 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
{
|
||||
description: msg`Date created`,
|
||||
value: DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(locale)
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
||||
},
|
||||
{
|
||||
description: msg`Last updated`,
|
||||
value: DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(locale)
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
|
||||
},
|
||||
{
|
||||
|
||||
@ -18,7 +18,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
|
||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { DataTableActionButton } from './data-table-action-button';
|
||||
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||
@ -41,8 +40,9 @@ export const DocumentsDataTable = ({
|
||||
showSenderColumn,
|
||||
team,
|
||||
}: DocumentsDataTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const { data: session } = useSession();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@ -53,12 +53,8 @@ export const DocumentsDataTable = ({
|
||||
{
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => (
|
||||
<LocaleDate
|
||||
date={row.original.createdAt}
|
||||
format={{ ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
|
||||
},
|
||||
{
|
||||
header: _(msg`Title`),
|
||||
@ -88,8 +84,7 @@ export const DocumentsDataTable = ({
|
||||
{
|
||||
header: _(msg`Actions`),
|
||||
cell: ({ row }) =>
|
||||
(!row.original.deletedAt ||
|
||||
row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
||||
(!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<DataTableActionButton team={team} row={row.original} />
|
||||
<DataTableActionDropdown team={team} row={row.original} />
|
||||
|
||||
@ -16,8 +16,6 @@ import { type Stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
|
||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { BillingPlans } from './billing-plans';
|
||||
import { BillingPortalButton } from './billing-portal-button';
|
||||
|
||||
@ -26,7 +24,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function BillingSettingsPage() {
|
||||
setupI18nSSR();
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
let { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@ -104,12 +102,12 @@ export default async function BillingSettingsPage() {
|
||||
{subscription.cancelAtPeriodEnd ? (
|
||||
<span>
|
||||
end on{' '}
|
||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
automatically renew on{' '}
|
||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@ -20,15 +20,13 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
...DateTime.DATETIME_SHORT,
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
export const UserSecurityActivityDataTable = () => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
@ -71,7 +69,7 @@ export const UserSecurityActivityDataTable = () => {
|
||||
{
|
||||
header: _(msg`Date`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
|
||||
},
|
||||
{
|
||||
header: _(msg`Device`),
|
||||
|
||||
@ -7,11 +7,10 @@ import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-use
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { ApiTokenForm } from '~/components/forms/token';
|
||||
|
||||
export default async function ApiTokensPage() {
|
||||
setupI18nSSR();
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@ -65,13 +64,11 @@ export default async function ApiTokensPage() {
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>Created on</Trans>{' '}
|
||||
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Expires on</Trans>{' '}
|
||||
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
|
||||
@ -16,10 +16,9 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
||||
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export default function WebhookPage() {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
|
||||
|
||||
@ -86,10 +85,7 @@ export default function WebhookPage() {
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</Trans>
|
||||
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { TemplateType } from '~/components/formatter/template-type';
|
||||
|
||||
import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||
@ -48,7 +47,7 @@ export const TemplatesDataTable = ({
|
||||
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
const { remaining } = useLimits();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
@ -56,7 +55,7 @@ export const TemplatesDataTable = ({
|
||||
{
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
header: _(msg`Title`),
|
||||
@ -81,8 +80,8 @@ export const TemplatesDataTable = ({
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
Public templates are connected to your public profile. Any modifications
|
||||
to public templates will also appear in your public profile.
|
||||
Public templates are connected to your public profile. Any modifications to
|
||||
public templates will also appear in your public profile.
|
||||
</Trans>
|
||||
</p>
|
||||
</li>
|
||||
@ -94,9 +93,9 @@ export const TemplatesDataTable = ({
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
Direct link templates contain one dynamic recipient placeholder. Anyone
|
||||
with access to this link can sign the document, and it will then appear
|
||||
on your documents page.
|
||||
Direct link templates contain one dynamic recipient placeholder. Anyone with
|
||||
access to this link can sign the document, and it will then appear on your
|
||||
documents page.
|
||||
</Trans>
|
||||
</p>
|
||||
</li>
|
||||
@ -109,8 +108,8 @@ export const TemplatesDataTable = ({
|
||||
<p>
|
||||
{teamId ? (
|
||||
<Trans>
|
||||
Team only templates are not linked anywhere and are visible only to
|
||||
your team.
|
||||
Team only templates are not linked anywhere and are visible only to your
|
||||
team.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Private templates can only be modified and viewed by you.</Trans>
|
||||
|
||||
@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import {
|
||||
@ -15,8 +16,6 @@ import {
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export type AuditLogDataTableProps = {
|
||||
logs: TDocumentAuditLog[];
|
||||
};
|
||||
@ -49,7 +48,9 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
|
||||
{logs.map((log, i) => (
|
||||
<TableRow className="break-inside-avoid" key={i}>
|
||||
<TableCell>
|
||||
<LocaleDate format={dateFormat} date={log.createdAt} />
|
||||
{DateTime.fromJSDate(log.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toLocaleString(dateFormat)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
|
||||
@ -2,7 +2,9 @@ import React from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
|
||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
@ -10,7 +12,6 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { AuditLogDataTable } from './data-table';
|
||||
|
||||
@ -21,8 +22,6 @@ type AuditLogProps = {
|
||||
};
|
||||
|
||||
export default async function AuditLog({ searchParams }: AuditLogProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { d } = searchParams;
|
||||
|
||||
if (typeof d !== 'string' || !d) {
|
||||
@ -89,7 +88,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
||||
<span className="font-medium">Created At</span>
|
||||
|
||||
<span className="mt-1 block">
|
||||
<LocaleDate date={document.createdAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
||||
{DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -97,7 +98,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
|
||||
<span className="font-medium">Last Updated</span>
|
||||
|
||||
<span className="mt-1 block">
|
||||
<LocaleDate date={document.updatedAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
|
||||
{DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
@ -2,10 +2,11 @@ import React from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION_ENG,
|
||||
RECIPIENT_ROLE_SIGNING_REASONS_ENG,
|
||||
@ -27,7 +28,6 @@ import {
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
type SigningCertificateProps = {
|
||||
searchParams: {
|
||||
@ -41,8 +41,6 @@ const FRIENDLY_SIGNING_REASONS = {
|
||||
};
|
||||
|
||||
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { d } = searchParams;
|
||||
|
||||
if (typeof d !== 'string' || !d) {
|
||||
@ -231,42 +229,33 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">Sent:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.EMAIL_SENT[0] ? (
|
||||
<LocaleDate
|
||||
date={logs.EMAIL_SENT[0].createdAt}
|
||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
||||
/>
|
||||
) : (
|
||||
'Unknown'
|
||||
)}
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: 'Unknown'}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">Viewed:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_OPENED[0] ? (
|
||||
<LocaleDate
|
||||
date={logs.DOCUMENT_OPENED[0].createdAt}
|
||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
||||
/>
|
||||
) : (
|
||||
'Unknown'
|
||||
)}
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: 'Unknown'}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">Signed:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] ? (
|
||||
<LocaleDate
|
||||
date={logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt}
|
||||
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
|
||||
/>
|
||||
) : (
|
||||
'Unknown'
|
||||
)}
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: 'Unknown'}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@ export const DropdownField = ({
|
||||
await removeSignedFieldWithToken(payload);
|
||||
}
|
||||
|
||||
setLocalChoice(parsedFieldMeta.defaultValue ?? '');
|
||||
setLocalChoice('');
|
||||
startTransition(() => router.refresh());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -179,7 +179,7 @@ export const DropdownField = ({
|
||||
|
||||
{!field.inserted && (
|
||||
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
|
||||
<Select value={parsedFieldMeta.defaultValue} onValueChange={handleSelectItem}>
|
||||
<Select value={localChoice} onValueChange={handleSelectItem}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0',
|
||||
@ -189,7 +189,7 @@ export const DropdownField = ({
|
||||
},
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder={`-- ${_(msg`Select`)} --`} />
|
||||
<SelectValue placeholder={`${_(msg`Select`)}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
|
||||
{parsedFieldMeta?.values?.map((item, index) => (
|
||||
|
||||
@ -12,7 +12,6 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { ApiTokenForm } from '~/components/forms/token';
|
||||
|
||||
type ApiTokensPageProps = {
|
||||
@ -22,7 +21,7 @@ type ApiTokensPageProps = {
|
||||
};
|
||||
|
||||
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
setupI18nSSR();
|
||||
const { i18n } = setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
@ -98,13 +97,17 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>Created on</Trans>{' '}
|
||||
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>
|
||||
Created on
|
||||
{i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
|
||||
</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Expires on</Trans>{' '}
|
||||
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>
|
||||
Expires on
|
||||
{i18n.date(token.expires, DateTime.DATETIME_FULL)}
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
|
||||
@ -16,11 +16,10 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
|
||||
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function WebhookPage() {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
@ -91,10 +90,7 @@ export default function WebhookPage() {
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</Trans>
|
||||
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import { AxiomWebVitals } from 'next-axiom';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
@ -9,12 +8,8 @@ import { PublicEnvScript } from 'next-runtime-env';
|
||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
|
||||
import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||
@ -61,32 +56,7 @@ export function generateMetadata() {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getServerComponentAllFlags();
|
||||
|
||||
const locale = getLocale();
|
||||
|
||||
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
|
||||
|
||||
// Should be safe to remove when we upgrade NextJS.
|
||||
// https://github.com/vercel/next.js/pull/65008
|
||||
// Currently if the middleware sets the cookie, it's not accessible in the cookies
|
||||
// during the same render.
|
||||
// So we go the roundabout way of checking the header for the set-cookie value.
|
||||
if (!cookies().get('i18n')) {
|
||||
const setCookieValue = headers().get('set-cookie');
|
||||
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
|
||||
|
||||
if (i18nCookie) {
|
||||
const i18n = i18nCookie.split('=')[1];
|
||||
|
||||
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable i18n for now until we get translations.
|
||||
if (!IS_APP_WEB_I18N_ENABLED) {
|
||||
overrideLang = 'en';
|
||||
}
|
||||
|
||||
const { lang, i18n } = setupI18nSSR(overrideLang);
|
||||
const { i18n, lang, locales } = setupI18nSSR();
|
||||
|
||||
return (
|
||||
<html
|
||||
@ -110,21 +80,22 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
</Suspense>
|
||||
|
||||
<body>
|
||||
<LocaleProvider locale={locale}>
|
||||
<FeatureFlagProvider initialFlags={flags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<TooltipProvider>
|
||||
<TrpcProvider>
|
||||
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
|
||||
{children}
|
||||
</I18nClientProvider>
|
||||
</TrpcProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
<FeatureFlagProvider initialFlags={flags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<TooltipProvider>
|
||||
<TrpcProvider>
|
||||
<I18nClientProvider
|
||||
initialLocaleData={{ lang, locales }}
|
||||
initialMessages={i18n.messages}
|
||||
>
|
||||
{children}
|
||||
</I18nClientProvider>
|
||||
</TrpcProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
<Toaster />
|
||||
</FeatureFlagProvider>
|
||||
</LocaleProvider>
|
||||
<Toaster />
|
||||
</FeatureFlagProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -22,12 +22,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
|
||||
|
||||
export const CurrentUserTeamsDataTable = () => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
@ -91,7 +89,7 @@ export const CurrentUserTeamsDataTable = () => {
|
||||
{
|
||||
header: _(msg`Member Since`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@ -18,13 +18,11 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
|
||||
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
|
||||
|
||||
export const PendingUserTeamsDataTable = () => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
@ -79,7 +77,7 @@ export const PendingUserTeamsDataTable = () => {
|
||||
{
|
||||
header: _(msg`Created on`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@ -27,8 +27,6 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export type TeamMemberInvitesDataTableProps = {
|
||||
teamId: number;
|
||||
};
|
||||
@ -37,7 +35,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
@ -129,7 +127,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
|
||||
{
|
||||
header: _(msg`Invited At`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
header: _(msg`Actions`),
|
||||
|
||||
@ -29,8 +29,6 @@ import {
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
|
||||
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
|
||||
|
||||
@ -47,7 +45,7 @@ export const TeamMembersDataTable = ({
|
||||
teamId,
|
||||
teamName,
|
||||
}: TeamMembersDataTableProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
@ -114,7 +112,7 @@ export const TeamMembersDataTable = ({
|
||||
{
|
||||
header: _(msg`Member Since`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
header: _(msg`Actions`),
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ArrowRightIcon, Loader } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
@ -18,8 +20,6 @@ import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
|
||||
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
|
||||
|
||||
export type DocumentHistorySheetProps = {
|
||||
@ -37,6 +37,8 @@ export const DocumentHistorySheet = ({
|
||||
onMenuOpenChange,
|
||||
children,
|
||||
}: DocumentHistorySheetProps) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
|
||||
|
||||
const {
|
||||
@ -153,7 +155,9 @@ export const DocumentHistorySheet = ({
|
||||
{formatDocumentAuditLogActionString(auditLog, userId)}
|
||||
</p>
|
||||
<p className="text-foreground/50 text-xs">
|
||||
<LocaleDate date={auditLog.createdAt} format="d MMM, yyyy HH:MM a" />
|
||||
{DateTime.fromJSDate(auditLog.createdAt)
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toFormat('d MMM, yyyy HH:MM a')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
||||
|
||||
export type LocaleDateProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
date: string | number | Date;
|
||||
format?: DateTimeFormatOptions | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the date based on the user locale.
|
||||
*
|
||||
* Will use the estimated locale from the user headers on SSR, then will use
|
||||
* the client browser locale once mounted.
|
||||
*/
|
||||
export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
const formatDateTime = useCallback(
|
||||
(date: DateTime) => {
|
||||
if (typeof format === 'string') {
|
||||
return date.toFormat(format);
|
||||
}
|
||||
|
||||
return date.toLocaleString(format);
|
||||
},
|
||||
[format],
|
||||
);
|
||||
|
||||
const [localeDate, setLocaleDate] = useState(() =>
|
||||
formatDateTime(DateTime.fromJSDate(new Date(date)).setLocale(locale)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocaleDate(formatDateTime(DateTime.fromJSDate(new Date(date))));
|
||||
}, [date, format, formatDateTime]);
|
||||
|
||||
return (
|
||||
<span className={className} {...props}>
|
||||
{localeDate}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -52,8 +52,6 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { LocaleDate } from '../formatter/locale-date';
|
||||
|
||||
export type ManagePublicTemplateDialogProps = {
|
||||
directTemplates: (Template & {
|
||||
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
|
||||
@ -93,7 +91,7 @@ export const ManagePublicTemplateDialog = ({
|
||||
onIsOpenChange,
|
||||
...props
|
||||
}: ManagePublicTemplateDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { _, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, onOpenChange] = useState(isOpen);
|
||||
@ -300,7 +298,7 @@ export const ManagePublicTemplateDialog = ({
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
<LocaleDate date={row.createdAt} />
|
||||
{i18n.date(row.createdAt)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
|
||||
@ -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 { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
async function middleware(req: NextRequest): Promise<NextResponse> {
|
||||
@ -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 = extractSupportedLanguage({
|
||||
headers: req.headers,
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
response.cookies.set('i18n', lang);
|
||||
// Can place anything that needs to be set on the response here.
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -74,6 +74,7 @@ docker run -d \
|
||||
-e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>"
|
||||
-e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>"
|
||||
-e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>"
|
||||
-e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
|
||||
-e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>"
|
||||
-e NEXT_PRIVATE_DIRECT_DATABASE_URL="<your-next-private-database-url>"
|
||||
-e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>"
|
||||
|
||||
@ -29,6 +29,7 @@ services:
|
||||
- NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
|
||||
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}
|
||||
- NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err}
|
||||
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL=${NEXT_PRIVATE_INTERNAL_WEBAPP_URL:-http://localhost:$PORT}
|
||||
- NEXT_PUBLIC_MARKETING_URL=${NEXT_PUBLIC_MARKETING_URL:-https://documenso.com}
|
||||
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err}
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}}
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -80,7 +80,7 @@
|
||||
},
|
||||
"apps/marketing": {
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@ -424,7 +424,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.7.0-rc.4",
|
||||
"version": "1.7.1-rc.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@ -16,6 +16,7 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
|
||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||
import { updateField } from '@documenso/lib/server-only/field/update-field';
|
||||
@ -295,6 +296,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
if (body.authOptions) {
|
||||
await updateDocumentSettings({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
const recipients = await setRecipientsForDocument({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
@ -465,6 +476,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
});
|
||||
}
|
||||
|
||||
if (body.authOptions) {
|
||||
await updateDocumentSettings({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@ -547,6 +568,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
});
|
||||
}
|
||||
|
||||
if (body.authOptions) {
|
||||
await updateDocumentSettings({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@ -682,7 +713,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
const { name, email, role, authOptions } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@ -736,6 +767,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
actionAuth: authOptions?.actionAuth ?? null,
|
||||
},
|
||||
],
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
@ -767,7 +799,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, recipientId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
const { name, email, role, authOptions } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@ -801,6 +833,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
actionAuth: authOptions?.actionAuth,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}).catch(() => null);
|
||||
|
||||
@ -1050,7 +1083,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
updateField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, fieldId } = args.params;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
|
||||
args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
@ -1112,6 +1146,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
fieldMeta: fieldMeta ? ZFieldMetaSchema.parse(fieldMeta) : undefined,
|
||||
});
|
||||
|
||||
const remappedField = {
|
||||
|
||||
@ -5,6 +5,11 @@ import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/const
|
||||
import '@documenso/lib/constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ZUrlSchema } from '@documenso/lib/schemas/common';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
DocumentDataType,
|
||||
@ -120,6 +125,12 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||
});
|
||||
|
||||
@ -166,6 +177,12 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||
});
|
||||
|
||||
@ -223,6 +240,12 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||
});
|
||||
|
||||
@ -254,6 +277,11 @@ export const ZCreateRecipientMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().min(1),
|
||||
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
|
||||
authOptions: z
|
||||
.object({
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -5,19 +5,24 @@ import { useState } from 'react';
|
||||
import { type Messages, setupI18n } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import type { I18nLocaleData } from '../../constants/i18n';
|
||||
|
||||
export function I18nClientProvider({
|
||||
children,
|
||||
initialLocale,
|
||||
initialLocaleData,
|
||||
initialMessages,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialLocale: string;
|
||||
initialLocaleData: I18nLocaleData;
|
||||
initialMessages: Messages;
|
||||
}) {
|
||||
const { lang, locales } = initialLocaleData;
|
||||
|
||||
const [i18n] = useState(() => {
|
||||
return setupI18n({
|
||||
locale: initialLocale,
|
||||
messages: { [initialLocale]: initialMessages },
|
||||
locale: lang,
|
||||
locales: locales,
|
||||
messages: { [lang]: initialMessages },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
import 'server-only';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import type { I18n, Messages } from '@lingui/core';
|
||||
import { setupI18n } from '@lingui/core';
|
||||
import { setI18n } from '@lingui/react/server';
|
||||
|
||||
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../../constants/app';
|
||||
import { IS_APP_WEB } from '../../constants/app';
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
|
||||
import { extractSupportedLanguage } from '../../utils/i18n';
|
||||
import { extractLocaleData } from '../../utils/i18n';
|
||||
|
||||
type SupportedLocales = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||
|
||||
async function loadCatalog(locale: SupportedLocales): Promise<{
|
||||
async function loadCatalog(lang: SupportedLanguages): Promise<{
|
||||
[k: string]: Messages;
|
||||
}> {
|
||||
const { messages } = await import(
|
||||
`../../translations/${locale}/${IS_APP_WEB ? 'web' : 'marketing'}.js`
|
||||
`../../translations/${lang}/${IS_APP_WEB ? 'web' : 'marketing'}.js`
|
||||
);
|
||||
|
||||
return {
|
||||
[locale]: messages,
|
||||
[lang]: messages,
|
||||
};
|
||||
}
|
||||
|
||||
@ -31,18 +31,18 @@ export const allMessages = catalogs.reduce((acc, oneCatalog) => {
|
||||
return { ...acc, ...oneCatalog };
|
||||
}, {});
|
||||
|
||||
type AllI18nInstances = { [K in SupportedLocales]: I18n };
|
||||
type AllI18nInstances = { [K in SupportedLanguages]: I18n };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) => {
|
||||
const messages = allMessages[locale] ?? {};
|
||||
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, lang) => {
|
||||
const messages = allMessages[lang] ?? {};
|
||||
|
||||
const i18n = setupI18n({
|
||||
locale,
|
||||
messages: { [locale]: messages },
|
||||
locale: lang,
|
||||
messages: { [lang]: messages },
|
||||
});
|
||||
|
||||
return { ...acc, [locale]: i18n };
|
||||
return { ...acc, [lang]: i18n };
|
||||
}, {}) as AllI18nInstances;
|
||||
|
||||
/**
|
||||
@ -50,24 +50,23 @@ export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, locale) =>
|
||||
*
|
||||
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
|
||||
*/
|
||||
export const setupI18nSSR = (overrideLang?: SupportedLocales) => {
|
||||
let lang =
|
||||
overrideLang ||
|
||||
extractSupportedLanguage({
|
||||
cookies: cookies(),
|
||||
});
|
||||
|
||||
// Override web app to be English.
|
||||
if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
|
||||
lang = 'en';
|
||||
}
|
||||
export const setupI18nSSR = () => {
|
||||
const { lang, locales } = extractLocaleData({
|
||||
cookies: cookies(),
|
||||
headers: headers(),
|
||||
});
|
||||
|
||||
// Get and set a ready-made i18n instance for the given language.
|
||||
const i18n = allI18nInstances[lang];
|
||||
|
||||
// Reactivate the i18n instance with the locale for date and number formatting.
|
||||
i18n.activate(lang, locales);
|
||||
|
||||
setI18n(i18n);
|
||||
|
||||
return {
|
||||
lang,
|
||||
locales,
|
||||
i18n,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type LocaleContextValue = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export const LocaleContext = createContext<LocaleContextValue | null>(null);
|
||||
|
||||
export const useLocale = () => {
|
||||
const context = useContext(LocaleContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useLocale must be used within a LocaleProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export function LocaleProvider({
|
||||
children,
|
||||
locale,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
locale: string;
|
||||
}) {
|
||||
return (
|
||||
<LocaleContext.Provider
|
||||
value={{
|
||||
locale: locale,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -1,15 +1,16 @@
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
export const IS_APP_WEB_I18N_ENABLED = false;
|
||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
||||
|
||||
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
||||
export const NEXT_PUBLIC_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 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 = true;
|
||||
|
||||
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
|
||||
|
||||
|
||||
@ -6,9 +6,22 @@ export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).cat
|
||||
|
||||
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||
|
||||
export type I18nLocaleData = {
|
||||
/**
|
||||
* The supported language extracted from the locale.
|
||||
*/
|
||||
lang: SupportedLanguageCodes;
|
||||
|
||||
/**
|
||||
* The preferred locales.
|
||||
*/
|
||||
locales: string[];
|
||||
};
|
||||
|
||||
export const APP_I18N_OPTIONS = {
|
||||
supportedLangs: SUPPORTED_LANGUAGE_CODES,
|
||||
sourceLang: 'en',
|
||||
defaultLocale: 'en-US',
|
||||
} as const;
|
||||
|
||||
type SupportedLanguage = {
|
||||
|
||||
@ -6,7 +6,7 @@ import { json } from 'micro';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
import { sign } from '../../server-only/crypto/sign';
|
||||
import { verify } from '../../server-only/crypto/verify';
|
||||
import {
|
||||
@ -229,7 +229,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
}) {
|
||||
const { jobId, jobDefinitionId, data, isRetry } = options;
|
||||
|
||||
const endpoint = `${NEXT_PUBLIC_WEBAPP_URL()}/api/jobs/${jobDefinitionId}/${jobId}`;
|
||||
const endpoint = `${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/jobs/${jobDefinitionId}/${jobId}`;
|
||||
const signature = sign(data);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@ -5,7 +5,7 @@ import { getToken } from 'next-auth/jwt';
|
||||
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
|
||||
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
|
||||
|
||||
/**
|
||||
@ -46,6 +46,10 @@ export default async function handlerFeatureFlagAll(req: Request) {
|
||||
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
|
||||
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@ -7,7 +7,7 @@ import { getToken } from 'next-auth/jwt';
|
||||
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
|
||||
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL, NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
/**
|
||||
* Evaluate a single feature flag based on the current user if possible.
|
||||
@ -67,6 +67,10 @@ export default async function handleFeatureFlagGet(req: Request) {
|
||||
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
|
||||
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@ -107,7 +107,10 @@ export const setFieldsForTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
|
||||
if (field.type === FieldType.CHECKBOX) {
|
||||
if (!field.fieldMeta) {
|
||||
throw new Error('Checkbox field is missing required metadata');
|
||||
}
|
||||
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateCheckboxField(
|
||||
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
|
||||
@ -118,7 +121,10 @@ export const setFieldsForTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.RADIO && field.fieldMeta) {
|
||||
if (field.type === FieldType.RADIO) {
|
||||
if (!field.fieldMeta) {
|
||||
throw new Error('Radio field is missing required metadata');
|
||||
}
|
||||
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
|
||||
(option) => option.checked,
|
||||
@ -129,7 +135,10 @@ export const setFieldsForTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN && field.fieldMeta) {
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
if (!field.fieldMeta) {
|
||||
throw new Error('Dropdown field is missing required metadata');
|
||||
}
|
||||
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
|
||||
if (errors.length > 0) {
|
||||
|
||||
@ -65,6 +65,11 @@ export const updateField = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const newFieldMeta = {
|
||||
...(oldField.fieldMeta as FieldMeta),
|
||||
...fieldMeta,
|
||||
};
|
||||
|
||||
const field = prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
@ -78,13 +83,39 @@ export const updateField = async ({
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta,
|
||||
fieldMeta: newFieldMeta,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
@ -108,31 +139,5 @@ export const updateField = async ({
|
||||
return updatedField;
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export const getLocale = () => {
|
||||
const headerItems = headers();
|
||||
|
||||
const locales = headerItems.get('accept-language') ?? 'en-US';
|
||||
|
||||
const [locale] = locales.split(',');
|
||||
|
||||
return locale;
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { RecipientRole, Team } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import {
|
||||
type TRecipientActionAuthTypes,
|
||||
ZRecipientAuthOptionsSchema,
|
||||
} from '../../types/document-auth';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
|
||||
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
documentId: number;
|
||||
@ -11,6 +18,7 @@ export type UpdateRecipientOptions = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
@ -22,6 +30,7 @@ export const updateRecipient = async ({
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
actionAuth,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
@ -48,6 +57,9 @@ export const updateRecipient = async ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Document: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
@ -75,6 +87,22 @@ export const updateRecipient = async ({
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (actionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
'You do not have permission to set the action auth',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
const updatedRecipient = await prisma.$transaction(async (tx) => {
|
||||
const persisted = await prisma.recipient.update({
|
||||
where: {
|
||||
@ -84,6 +112,10 @@ export const updateRecipient = async ({
|
||||
email: email?.toLowerCase() ?? recipient.email,
|
||||
name: name ?? recipient.name,
|
||||
role: role ?? recipient.role,
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: recipientAuthOptions.accessAuth,
|
||||
actionAuth: actionAuth ?? null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../../constants/app';
|
||||
import { sign } from '../../crypto/sign';
|
||||
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
|
||||
|
||||
@ -29,7 +29,7 @@ export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWeb
|
||||
const signature = sign(body);
|
||||
|
||||
await Promise.race([
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, {
|
||||
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2024-07-24 13:01+1000\n"
|
||||
"POT-Creation-Date: 2024-08-22 05:16+0000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-08-27 16:03\n"
|
||||
"PO-Revision-Date: 2024-09-05 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -116,7 +116,7 @@ msgid "Advanced Options"
|
||||
msgstr "Erweiterte Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:369
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Erweiterte Einstellungen"
|
||||
|
||||
@ -140,7 +140,15 @@ msgstr "Genehmiger"
|
||||
msgid "Approving"
|
||||
msgstr "Genehmigung"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:276
|
||||
msgid "Black"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:290
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
@ -167,7 +175,7 @@ msgid "Character Limit"
|
||||
msgstr "Zeichenbeschränkung"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:755
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
|
||||
@ -179,7 +187,7 @@ msgstr "Checkbox-Werte"
|
||||
msgid "Clear filters"
|
||||
msgstr "Filter löschen"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:310
|
||||
msgid "Clear Signature"
|
||||
msgstr "Unterschrift löschen"
|
||||
|
||||
@ -196,7 +204,7 @@ msgid "Configure Direct Recipient"
|
||||
msgstr "Direkten Empfänger konfigurieren"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Konfigurieren Sie das Feld {0}"
|
||||
|
||||
@ -213,7 +221,7 @@ msgid "Custom Text"
|
||||
msgstr "Benutzerdefinierter Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:651
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
|
||||
@ -245,7 +253,7 @@ msgid "Drag & drop your PDF here."
|
||||
msgstr "Ziehen Sie Ihr PDF hierher."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:781
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
|
||||
@ -257,7 +265,7 @@ msgstr "Dropdown-Optionen"
|
||||
#: 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/template-flow/add-template-fields.tsx:599
|
||||
#: 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
|
||||
msgid "Email"
|
||||
@ -275,7 +283,7 @@ msgstr "Direktlink-Signierung aktivieren"
|
||||
msgid "Enter password"
|
||||
msgstr "Passwort eingeben"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:226
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:216
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
@ -284,7 +292,7 @@ msgstr "Fehler"
|
||||
msgid "External ID"
|
||||
msgstr "Externe ID"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:227
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:217
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Einstellungen konnten nicht gespeichert werden."
|
||||
|
||||
@ -312,6 +320,10 @@ msgstr "Globale Empfängerauthentifizierung"
|
||||
msgid "Go Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:297
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:72
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Ich bin ein Unterzeichner dieses Dokuments"
|
||||
@ -355,7 +367,7 @@ msgstr "Max"
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:95
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:229
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:215
|
||||
msgid "Message <0>(Optional)</0>"
|
||||
msgstr "Nachricht <0>(Optional)</0>"
|
||||
@ -367,7 +379,7 @@ msgstr "Min"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:625
|
||||
#: 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
|
||||
msgid "Name"
|
||||
@ -386,12 +398,12 @@ msgid "Needs to view"
|
||||
msgstr "Muss sehen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:464
|
||||
#: 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/template-flow/add-template-fields.tsx:480
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "Keine Empfänger mit dieser Rolle"
|
||||
|
||||
@ -416,7 +428,7 @@ msgid "No value found."
|
||||
msgstr "Kein Wert gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:703
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Nummer"
|
||||
|
||||
@ -451,7 +463,7 @@ msgid "Placeholder"
|
||||
msgstr "Platzhalter"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:729
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
|
||||
@ -477,6 +489,10 @@ msgstr "Erhält Kopie"
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Empfängeraktion Authentifizierung"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:283
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
@ -498,14 +514,18 @@ msgstr "Pflichtfeld"
|
||||
msgid "Rows per page"
|
||||
msgstr "Zeilen pro Seite"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:296
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:286
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:797
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:848
|
||||
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"
|
||||
@ -522,7 +542,7 @@ msgstr "Wählen Sie mindestens"
|
||||
msgid "Select default option"
|
||||
msgstr "Standardoption auswählen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:124
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:275
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:34
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:64
|
||||
msgid "Send"
|
||||
@ -552,7 +572,7 @@ msgstr "Unterschreiben"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: 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:547
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
msgid "Signature"
|
||||
msgstr "Unterschrift"
|
||||
|
||||
@ -584,7 +604,7 @@ msgstr "Etwas ist schief gelaufen."
|
||||
msgid "Step <0>{step} of {maxStep}</0>"
|
||||
msgstr "Schritt <0>{step} von {maxStep}</0>"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:78
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:194
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:195
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Betreff <0>(Optional)</0>"
|
||||
@ -598,7 +618,7 @@ msgid "Template title"
|
||||
msgstr "Vorlagentitel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:677
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@ -688,10 +708,11 @@ msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:971
|
||||
#: 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."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:124
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:275
|
||||
msgid "Update"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
@ -733,6 +754,10 @@ msgstr "Viewer"
|
||||
msgid "Viewing"
|
||||
msgstr "Viewing"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
|
||||
#~ msgid "White"
|
||||
#~ msgstr ""
|
||||
|
||||
#: 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?"
|
||||
msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,14 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2024-07-24 13:01+1000\n"
|
||||
"POT-Creation-Date: 2024-08-22 05:16+0000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-08-27 16:03\n"
|
||||
"PO-Revision-Date: 2024-09-05 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -20,19 +20,19 @@ msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/blog/page.tsx:45
|
||||
msgid "{0}"
|
||||
msgstr "{0}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:100
|
||||
msgid "5 standard documents per month"
|
||||
msgstr "5 Standarddokumente pro Monat"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:198
|
||||
msgid "5 Users Included"
|
||||
msgstr "5 Benutzer inbegriffen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:34
|
||||
msgid "A 10x better signing experience."
|
||||
msgstr "Eine 10x bessere Signaturerfahrung."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/singleplayer/client.tsx:51
|
||||
msgid "Add document"
|
||||
@ -40,142 +40,138 @@ msgstr "Dokument hinzufügen"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:201
|
||||
msgid "Add More Users for {0}"
|
||||
msgstr "Mehr Benutzer hinzufügen für {0}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
|
||||
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
|
||||
msgstr "Alle unsere Kennzahlen, Finanzen und Erkenntnisse sind öffentlich. Wir glauben an Transparenz und möchten unsere Reise mit Ihnen teilen. Mehr erfahren Sie hier: <0>Ankündigung Offene Kennzahlen</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/funding-raised.tsx:58
|
||||
#: apps/marketing/src/app/(marketing)/open/funding-raised.tsx:65
|
||||
msgid "Amount Raised"
|
||||
msgstr "Erhobener Betrag"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:145
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:189
|
||||
msgid "API Access"
|
||||
msgstr "API-Zugriff"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:67
|
||||
msgid "Beautiful."
|
||||
msgstr "Schön."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:69
|
||||
msgid "Because signing should be celebrated. That’s why we care about the smallest detail in our product."
|
||||
msgstr "Weil Unterschriften gefeiert werden sollten. Deshalb kümmern wir uns um jedes kleinste Detail in unserem Produkt."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:35
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:57
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:36
|
||||
msgid "Blog"
|
||||
msgstr "Blog"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:64
|
||||
msgid "Build on top."
|
||||
msgstr "Aufbauen oben drauf."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:163
|
||||
msgid "Can I use Documenso commercially?"
|
||||
msgstr "Kann ich Documenso kommerziell nutzen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:42
|
||||
msgid "Careers"
|
||||
msgstr "Karrieren"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:36
|
||||
msgid "Changelog"
|
||||
msgstr "Änderungsprotokoll"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:85
|
||||
msgid "Choose a template from the community app store. Or submit your own template for others to use."
|
||||
msgstr "Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
|
||||
msgid "Community"
|
||||
msgstr "Gemeinschaft"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-completed-documents-chart.tsx:55
|
||||
msgid "Completed Documents"
|
||||
msgstr "Fertige Dokumente"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-completed-documents-chart.tsx:33
|
||||
msgid "Completed Documents per Month"
|
||||
msgstr "Fertige Dokumente pro Monat"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:65
|
||||
msgid "Connections"
|
||||
msgstr "Verbindungen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/enterprise.tsx:35
|
||||
msgid "Contact Us"
|
||||
msgstr "Kontaktiere uns"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:67
|
||||
msgid "Create connections and automations with Zapier and more to integrate with your favorite tools."
|
||||
msgstr "Erstellen Sie Verbindungen und Automatisierungen mit Zapier und mehr, um sich mit Ihren Lieblingstools zu integrieren."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/call-to-action.tsx:23
|
||||
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
|
||||
msgstr "Erstellen Sie Ihr Konto und beginnen Sie mit der Nutzung modernster Dokumentensignaturen. Offene und schöne Signaturen sind zum Greifen nah."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/tooltip.tsx:35
|
||||
msgid "Customers with an Active Subscriptions."
|
||||
msgstr "Kunden mit einer aktiven Abonnements."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:33
|
||||
msgid "Customise and expand."
|
||||
msgstr "Anpassen und erweitern."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:38
|
||||
msgid "Design"
|
||||
msgstr "Design"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:44
|
||||
msgid "Designed for every stage of your journey."
|
||||
msgstr "Entwickelt für jede Phase Ihrer Reise."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:40
|
||||
msgid "Direct Link"
|
||||
msgstr "Direktlink"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:181
|
||||
msgid "Documenso is a community effort to create an open and vibrant ecosystem around a tool, everybody is free to use and adapt. By being truly open we want to create trusted infrastructure for the future of the internet."
|
||||
msgstr "Documenso ist eine Gemeinschaftsanstrengung, um ein offenes und lebendiges Ökosystem um ein Werkzeug zu schaffen, das jeder frei nutzen und anpassen kann. Indem wir wirklich offen sind, wollen wir vertrauenswürdige Infrastruktur für die Zukunft des Internets schaffen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/typefully.tsx:28
|
||||
msgid "Documenso on X"
|
||||
msgstr "Documenso auf X"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/hero.tsx:104
|
||||
msgid "Document signing,<0/>finally open source."
|
||||
msgstr "Unterschriften,<0/>endlich Open Source."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:33
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:50
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:28
|
||||
msgid "Documentation"
|
||||
msgstr "Dokumentation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:110
|
||||
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
|
||||
msgstr "Betten Sie Documenso ganz einfach in Ihr Produkt ein. Kopieren und fügen Sie einfach unser React-Widget in Ihre Anwendung ein."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
|
||||
#~ msgid "Easy Sharing (Soon)."
|
||||
#~ msgstr "Easy Sharing (Soon)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
|
||||
msgid "Easy Sharing."
|
||||
msgstr "Einfaches Teilen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:148
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:192
|
||||
msgid "Email and Discord Support"
|
||||
msgstr "E-Mail- und Discord-Support"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:43
|
||||
msgid "Engagement"
|
||||
msgstr "Beteiligung"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/singleplayer/client.tsx:64
|
||||
msgid "Enter your details."
|
||||
@ -183,43 +179,43 @@ msgstr "Geben Sie Ihre Details ein."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/enterprise.tsx:16
|
||||
msgid "Enterprise Compliance, License or Technical Needs?"
|
||||
msgstr "Enterprise-Konformität, Lizenz- oder technische Bedürfnisse?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:128
|
||||
msgid "Everything you need for a great signing experience."
|
||||
msgstr "Alles, was Sie für ein großartiges Signaturerlebnis benötigen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:45
|
||||
msgid "Fast."
|
||||
msgstr "Schnell."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:36
|
||||
msgid "Faster, smarter and more beautiful."
|
||||
msgstr "Schneller, intelligenter und schöner."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
|
||||
msgid "Finances"
|
||||
msgstr "Finanzen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/typefully.tsx:38
|
||||
msgid "Follow us on X"
|
||||
msgstr "Folgen Sie uns auf X"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:172
|
||||
msgid "For companies looking to scale across multiple teams."
|
||||
msgstr "Für Unternehmen, die über mehrere Teams skalieren möchten."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:85
|
||||
msgid "For small teams and individuals with basic needs."
|
||||
msgstr "Für kleine Teams und Einzelpersonen mit grundlegenden Bedürfnissen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:80
|
||||
msgid "Free"
|
||||
msgstr "Kostenlos"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/blog/page.tsx:26
|
||||
msgid "From the blog"
|
||||
msgstr "Aus dem Blog"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:9
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:17
|
||||
@ -227,227 +223,222 @@ msgstr "Aus dem Blog"
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:41
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:49
|
||||
msgid "Full-Time"
|
||||
msgstr "Vollzeit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:87
|
||||
msgid "Get paid (Soon)."
|
||||
msgstr "Lassen Sie sich bezahlen (Bald)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/call-to-action.tsx:31
|
||||
msgid "Get started"
|
||||
msgstr "Loslegen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:75
|
||||
msgid "Get Started"
|
||||
msgstr "Loslegen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:47
|
||||
msgid "Get started today."
|
||||
msgstr "Fangen Sie heute an."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/blog/page.tsx:30
|
||||
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
|
||||
msgstr "Erhalten Sie die neuesten Nachrichten von Documenso, einschließlich Produkt-Updates, Team-Ankündigungen und mehr!"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
|
||||
msgid "GitHub: Total Merged PRs"
|
||||
msgstr "GitHub: Gesamte PRs zusammengeführt"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
|
||||
msgid "GitHub: Total Open Issues"
|
||||
msgstr "GitHub: Gesamte offene Issues"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
|
||||
msgid "GitHub: Total Stars"
|
||||
msgstr "GitHub: Gesamtanzahl Sterne"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/salary-bands.tsx:23
|
||||
msgid "Global Salary Bands"
|
||||
msgstr "Globale Gehaltsbänder"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
|
||||
msgid "Growth"
|
||||
msgstr "Wachstum"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:134
|
||||
msgid "How can I contribute?"
|
||||
msgstr "Wie kann ich beitragen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:105
|
||||
msgid "How do you handle my data?"
|
||||
msgstr "Wie gehen Sie mit meinen Daten um?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:118
|
||||
msgid "Individual"
|
||||
msgstr "Einzelperson"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:89
|
||||
msgid "Integrated payments with Stripe so you don’t have to worry about getting paid."
|
||||
msgstr "Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezahlen machen müssen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:35
|
||||
msgid "Integrates with all your favourite tools."
|
||||
msgstr "Integriert sich mit all Ihren Lieblingstools."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
|
||||
msgid "Is there more?"
|
||||
msgstr "Gibt es mehr?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:44
|
||||
msgid "It’s up to you. Either clone our repository or rely on our easy to use hosting solution."
|
||||
msgstr "Es liegt an Ihnen. Entweder klonen Sie unser Repository oder nutzen unsere einfach zu bedienende Hosting-Lösung."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:49
|
||||
msgid "Join Date"
|
||||
msgstr "Eintrittsdatum"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/call-to-action.tsx:19
|
||||
msgid "Join the Open Signing Movement"
|
||||
msgstr "Treten Sie der Open Signing-Bewegung bei"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:46
|
||||
msgid "Location"
|
||||
msgstr "Standort"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:66
|
||||
msgid "Make it your own through advanced customization and adjustability."
|
||||
msgstr "Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
|
||||
msgid "Merged PR's"
|
||||
msgstr "Zusammengeführte PRs"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
|
||||
msgid "Merged PRs"
|
||||
msgstr "Zusammengeführte PRs"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:40
|
||||
msgid "Monthly"
|
||||
msgstr "Monatlich"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:34
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx:30
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx:43
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx:52
|
||||
msgid "New Users"
|
||||
msgstr "Neue Benutzer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:106
|
||||
msgid "No credit card required"
|
||||
msgstr "Keine Kreditkarte erforderlich"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/callout.tsx:29
|
||||
#: apps/marketing/src/components/(marketing)/hero.tsx:125
|
||||
msgid "No Credit Card required"
|
||||
msgstr "Keine Kreditkarte erforderlich"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:61
|
||||
msgid "None of these work for you? Try self-hosting!"
|
||||
msgstr "Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
|
||||
msgid "Open Issues"
|
||||
msgstr "Offene Issues"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:42
|
||||
msgid "Open Source or Hosted."
|
||||
msgstr "Open Source oder Hosted."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:37
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:64
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
|
||||
msgid "Open Startup"
|
||||
msgstr "Offenes Startup"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:41
|
||||
msgid "OSS Friends"
|
||||
msgstr "OSS-Freunde"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:91
|
||||
msgid "Our custom templates come with smart rules that can help you save time and energy."
|
||||
msgstr "Unsere benutzerdefinierten Vorlagen verfügen über intelligente Regeln, die Ihnen Zeit und Energie sparen können."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
|
||||
msgid "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
|
||||
msgstr "Unsere Enterprise-Lizenz ist ideal für große Organisationen, die auf Documenso für all ihre Signaturanforderungen umsteigen möchten. Sie ist sowohl für unser Cloud-Angebot als auch für selbstgehostete Setups verfügbar und bietet eine breite Palette an Compliance- und Verwaltungsfunktionen."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
|
||||
#~ msgid "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
|
||||
#~ msgstr "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:65
|
||||
msgid "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
|
||||
msgstr "Unsere selbstgehostete Option ist ideal für kleine Teams und Einzelpersonen, die eine einfache Lösung benötigen. Sie können unser docker-basiertes Setup verwenden, um in wenigen Minuten loszulegen. Übernehmen Sie die Kontrolle mit vollständiger Anpassbarkeit und Datenhoheit."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
msgid "Part-Time"
|
||||
msgstr "Teilzeit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
|
||||
msgid "Premium Profile Name"
|
||||
msgstr "Premium Profilname"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:40
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:31
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:42
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:24
|
||||
msgid "Pricing"
|
||||
msgstr "Preise"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:43
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:53
|
||||
msgid "Privacy"
|
||||
msgstr "Datenschutz"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:58
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:108
|
||||
msgid "React Widget (Soon)."
|
||||
msgstr "React Widget (Demnächst)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:48
|
||||
msgid "Receive your personal link to share with everyone you care about."
|
||||
msgstr "Erhalten Sie Ihren persönlichen Link zum Teilen mit allen, die Ihnen wichtig sind."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:37
|
||||
msgid "Role"
|
||||
msgstr "Rolle"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/salary-bands.tsx:37
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:40
|
||||
msgid "Salary"
|
||||
msgstr "Gehalt"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:62
|
||||
msgid "Save $60 or $120"
|
||||
msgstr "Sparen Sie $60 oder $120"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
|
||||
msgid "Search languages..."
|
||||
msgstr "Sprachen suchen..."
|
||||
#~ msgid "Search languages..."
|
||||
#~ msgstr "Sprachen suchen..."
|
||||
|
||||
#: 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."
|
||||
msgstr "Sicher. Unsere Rechenzentren befinden sich in Frankfurt (Deutschland) und bieten uns die besten lokalen Datenschutzgesetze. Uns ist die sensible Natur unserer Daten sehr bewusst und wir folgen bewährten Praktiken, um die Sicherheit und Integrität der uns anvertrauten Daten zu gewährleisten."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:37
|
||||
msgid "Send, connect, receive and embed everywhere."
|
||||
msgstr "Überall senden, verbinden, empfangen und einbetten."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/salary-bands.tsx:34
|
||||
msgid "Seniority"
|
||||
msgstr "Dienstalter"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:39
|
||||
msgid "Shop"
|
||||
msgstr "Shop"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/singleplayer/client.tsx:63
|
||||
msgid "Sign"
|
||||
@ -456,115 +447,115 @@ msgstr "Signieren"
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:72
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:61
|
||||
msgid "Sign in"
|
||||
msgstr "Anmelden"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/header.tsx:77
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:57
|
||||
msgid "Sign up"
|
||||
msgstr "Registrieren"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:22
|
||||
msgid "Signing Process"
|
||||
msgstr "Signaturprozess"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:94
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:136
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:180
|
||||
msgid "Signup Now"
|
||||
msgstr "Jetzt registrieren"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:89
|
||||
msgid "Smart."
|
||||
msgstr "Intelligent."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/hero.tsx:132
|
||||
msgid "Star on GitHub"
|
||||
msgstr "Auf GitHub favorisieren"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
|
||||
msgid "Stars"
|
||||
msgstr "Favoriten"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:40
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:44
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/footer.tsx:34
|
||||
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:48
|
||||
msgid "Support"
|
||||
msgstr "Support"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/team-members.tsx:26
|
||||
msgid "Team"
|
||||
msgstr "Team"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:195
|
||||
msgid "Team Inbox"
|
||||
msgstr "Team-Posteingang"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:28
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:162
|
||||
msgid "Teams"
|
||||
msgstr "Teams"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:83
|
||||
msgid "Template Store (Soon)."
|
||||
msgstr "Vorlagen-Shop (Demnächst)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:138
|
||||
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
|
||||
msgstr "Das ist großartig. Sie können sich die aktuellen <0>Issues</0> ansehen und unserer <1>Discord-Community</1> beitreten, um auf dem neuesten Stand zu bleiben, was die aktuellen Prioritäten sind. In jedem Fall sind wir eine offene Gemeinschaft und begrüßen jegliche Beiträge, technische und nicht-technische ❤️"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
|
||||
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
|
||||
msgstr "Diese Seite entwickelt sich weiter, während wir lernen, was ein großartiges Signing-Unternehmen ausmacht. Wir werden sie aktualisieren, wenn wir mehr zu teilen haben."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/salary-bands.tsx:31
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/total-signed-documents-chart.tsx:30
|
||||
#: apps/marketing/src/app/(marketing)/open/total-signed-documents-chart.tsx:55
|
||||
msgid "Total Completed Documents"
|
||||
msgstr "Insgesamt Abgeschlossene Dokumente"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
|
||||
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
|
||||
msgid "Total Customers"
|
||||
msgstr "Insgesamt Kunden"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/funding-raised.tsx:29
|
||||
msgid "Total Funding Raised"
|
||||
msgstr "Insgesamt Finanzierungsvolumen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx:30
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx:43
|
||||
#: apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx:52
|
||||
msgid "Total Users"
|
||||
msgstr "Gesamtanzahl der Benutzer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/open-build-template-bento.tsx:31
|
||||
msgid "Truly your own."
|
||||
msgstr "Wirklich Ihr Eigenes."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/callout.tsx:27
|
||||
#: apps/marketing/src/components/(marketing)/hero.tsx:123
|
||||
msgid "Try our Free Plan"
|
||||
msgstr "Probieren Sie unseren Gratisplan aus"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/typefully.tsx:20
|
||||
msgid "Twitter Stats"
|
||||
msgstr "Twitter-Statistiken"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:142
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:186
|
||||
msgid "Unlimited Documents per Month"
|
||||
msgstr "Unbegrenzte Dokumente pro Monat"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:103
|
||||
msgid "Up to 10 recipients per document"
|
||||
msgstr "Bis zu 10 Empfänger pro Dokument"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/singleplayer/client.tsx:52
|
||||
msgid "Upload a document and add fields."
|
||||
@ -572,48 +563,52 @@ msgstr "Laden Sie ein Dokument hoch und fügen Sie Felder hinzu."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:123
|
||||
msgid "Using our hosted version is the easiest way to get started, you can simply subscribe and start signing your documents. We take care of the infrastructure, so you can focus on your business. Additionally, when using our hosted version you benefit from our trusted signing certificates which helps you to build trust with your customers."
|
||||
msgstr "Die Nutzung unserer gehosteten Version ist der einfachste Weg, um zu starten. Sie können einfach abonnieren und mit der Unterzeichnung Ihrer Dokumente beginnen. Wir kümmern uns um die Infrastruktur, damit Sie sich auf Ihr Geschäft konzentrieren können. Zudem profitieren Sie bei der Nutzung unserer gehosteten Version von unseren vertrauenswürdigen Signaturzertifikaten, die Ihnen helfen, Vertrauen bei Ihren Kunden aufzubauen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/typefully.tsx:33
|
||||
msgid "View all stats"
|
||||
msgstr "Alle Statistiken anzeigen"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:195
|
||||
msgid "We are happy to assist you at <0>support@documenso.com</0> or <1>in our Discord-Support-Channel</1> please message either Lucas or Timur to get added to the channel if you are not already a member."
|
||||
msgstr "Wir helfen Ihnen gerne unter <0>support@documenso.com</0> oder <1>in unserem Discord-Support-Kanal</1>. Bitte senden Sie Lucas oder Timur eine Nachricht, um dem Kanal beizutreten, falls Sie noch kein Mitglied sind."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:89
|
||||
msgid "What is the difference between the plans?"
|
||||
msgstr "Was ist der Unterschied zwischen den Plänen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx:47
|
||||
msgid "When it comes to sending or receiving a contract, you can count on lightning-fast speeds."
|
||||
msgstr "Wenn es um das Senden oder Empfangen eines Vertrags geht, können Sie auf blitzschnelle Geschwindigkeiten zählen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:191
|
||||
msgid "Where can I get support?"
|
||||
msgstr "Wo kann ich Unterstützung bekommen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:177
|
||||
msgid "Why should I prefer Documenso over DocuSign or some other signing tool?"
|
||||
msgstr "Warum sollte ich Documenso gegenüber DocuSign oder einem anderen Signatur-Tool bevorzugen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:119
|
||||
msgid "Why should I use your hosting service?"
|
||||
msgstr "Warum sollte ich Ihren Hosting-Service nutzen?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:60
|
||||
msgid "Yearly"
|
||||
msgstr "Jährlich"
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:167
|
||||
msgid "Yes! Documenso is offered under the GNU AGPL V3 open source license. This means you can use it for free and even modify it to fit your needs, as long as you publish your changes under the same license."
|
||||
msgstr "Ja! Documenso wird unter der GNU AGPL V3 Open-Source-Lizenz angeboten. Das bedeutet, dass Sie es kostenlos nutzen und sogar an Ihre Bedürfnisse anpassen können, solange Sie Ihre Änderungen unter derselben Lizenz veröffentlichen."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:93
|
||||
msgid "You can self-host Documenso for free or use our ready-to-use hosted version. The hosted version comes with additional support, painless scalability and more. Early adopters will get access to all features we build this year, for no additional cost! Forever! Yes, that includes multiple users per account later. If you want Documenso for your enterprise, we are happy to talk about your needs."
|
||||
msgstr "Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatzbereite gehostete Version nutzen. Die gehostete Version bietet zusätzlichen Support, schmerzfreie Skalierbarkeit und mehr. Frühzeitige Anwender erhalten in diesem Jahr Zugriff auf alle Funktionen, die wir entwickeln, ohne zusätzliche Kosten! Für immer! Ja, das beinhaltet später mehrere Benutzer pro Konto. Wenn Sie Documenso für Ihr Unternehmen möchten, sprechen wir gerne über Ihre Bedürfnisse."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:265
|
||||
msgid "Your browser does not support the video tag."
|
||||
msgstr "Ihr Browser unterstützt das Video-Tag nicht."
|
||||
msgstr ""
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:265
|
||||
#~ msgid "Your browser does not support the video tag.<<<<<<< HEAD"
|
||||
#~ msgstr "Ihr Browser unterstützt das Video-Tag nicht.>>>>>>> main"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -111,7 +111,7 @@ msgid "Advanced Options"
|
||||
msgstr "Advanced Options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:510
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:369
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Advanced settings"
|
||||
|
||||
@ -135,7 +135,15 @@ msgstr "Approver"
|
||||
msgid "Approving"
|
||||
msgstr "Approving"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:276
|
||||
msgid "Black"
|
||||
msgstr "Black"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:290
|
||||
msgid "Blue"
|
||||
msgstr "Blue"
|
||||
|
||||
#: 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 "Cancel"
|
||||
@ -162,7 +170,7 @@ msgid "Character Limit"
|
||||
msgstr "Character Limit"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:932
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:755
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
|
||||
@ -174,7 +182,7 @@ msgstr "Checkbox values"
|
||||
msgid "Clear filters"
|
||||
msgstr "Clear filters"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:310
|
||||
msgid "Clear Signature"
|
||||
msgstr "Clear Signature"
|
||||
|
||||
@ -191,7 +199,7 @@ msgid "Configure Direct Recipient"
|
||||
msgstr "Configure Direct Recipient"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:511
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:370
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Configure the {0} field"
|
||||
|
||||
@ -208,7 +216,7 @@ msgid "Custom Text"
|
||||
msgstr "Custom Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:828
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:651
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
@ -240,7 +248,7 @@ msgid "Drag & drop your PDF here."
|
||||
msgstr "Drag & drop your PDF here."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:958
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:781
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
|
||||
@ -252,7 +260,7 @@ msgstr "Dropdown options"
|
||||
#: 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/template-flow/add-template-fields.tsx:599
|
||||
#: 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
|
||||
msgid "Email"
|
||||
@ -270,7 +278,7 @@ msgstr "Enable Direct Link Signing"
|
||||
msgid "Enter password"
|
||||
msgstr "Enter password"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:226
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:216
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
@ -279,7 +287,7 @@ msgstr "Error"
|
||||
msgid "External ID"
|
||||
msgstr "External ID"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:227
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:217
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Failed to save settings."
|
||||
|
||||
@ -307,6 +315,10 @@ msgstr "Global recipient action authentication"
|
||||
msgid "Go Back"
|
||||
msgstr "Go Back"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:297
|
||||
msgid "Green"
|
||||
msgstr "Green"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:72
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "I am a signer of this document"
|
||||
@ -350,7 +362,7 @@ msgstr "Max"
|
||||
msgid "Member"
|
||||
msgstr "Member"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:95
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:229
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:215
|
||||
msgid "Message <0>(Optional)</0>"
|
||||
msgstr "Message <0>(Optional)</0>"
|
||||
@ -362,7 +374,7 @@ msgstr "Min"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:802
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:265
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:625
|
||||
#: 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
|
||||
msgid "Name"
|
||||
@ -381,12 +393,12 @@ msgid "Needs to view"
|
||||
msgstr "Needs to view"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:613
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:464
|
||||
#: 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/template-flow/add-template-fields.tsx:480
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "No recipients with this role"
|
||||
|
||||
@ -411,7 +423,7 @@ msgid "No value found."
|
||||
msgstr "No value found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:880
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:703
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Number"
|
||||
|
||||
@ -446,7 +458,7 @@ msgid "Placeholder"
|
||||
msgstr "Placeholder"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:906
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:729
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
|
||||
@ -472,6 +484,10 @@ msgstr "Receives copy"
|
||||
msgid "Recipient action authentication"
|
||||
msgstr "Recipient action authentication"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:283
|
||||
msgid "Red"
|
||||
msgstr "Red"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-settings.tsx:298
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
|
||||
msgid "Redirect URL"
|
||||
@ -493,14 +509,18 @@ msgstr "Required field"
|
||||
msgid "Rows per page"
|
||||
msgstr "Rows per page"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:296
|
||||
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:286
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:797
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:848
|
||||
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"
|
||||
@ -517,7 +537,7 @@ msgstr "Select at least"
|
||||
msgid "Select default option"
|
||||
msgstr "Select default option"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:124
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:275
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:34
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:64
|
||||
msgid "Send"
|
||||
@ -547,7 +567,7 @@ msgstr "Sign"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:724
|
||||
#: 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:547
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
msgid "Signature"
|
||||
msgstr "Signature"
|
||||
|
||||
@ -579,7 +599,7 @@ msgstr "Something went wrong."
|
||||
msgid "Step <0>{step} of {maxStep}</0>"
|
||||
msgstr "Step <0>{step} of {maxStep}</0>"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:78
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:194
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:195
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Subject <0>(Optional)</0>"
|
||||
@ -593,7 +613,7 @@ msgid "Template title"
|
||||
msgstr "Template title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:854
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:677
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@ -683,10 +703,11 @@ msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:971
|
||||
#: 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."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:124
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx:275
|
||||
msgid "Update"
|
||||
msgstr "Update"
|
||||
|
||||
@ -728,6 +749,10 @@ msgstr "Viewer"
|
||||
msgid "Viewing"
|
||||
msgstr "Viewing"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
|
||||
#~ msgid "White"
|
||||
#~ 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?"
|
||||
msgstr "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
@ -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."
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,8 @@ import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension
|
||||
|
||||
import type { I18n } from '@lingui/core';
|
||||
|
||||
import { IS_APP_WEB } from '../constants/app';
|
||||
import type { SupportedLanguageCodes } from '../constants/i18n';
|
||||
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../constants/app';
|
||||
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
|
||||
import { APP_I18N_OPTIONS } from '../constants/i18n';
|
||||
|
||||
export async function dynamicActivate(i18nInstance: I18n, locale: string) {
|
||||
@ -14,48 +14,58 @@ export async function dynamicActivate(i18nInstance: I18n, locale: string) {
|
||||
i18nInstance.loadAndActivate({ locale, messages });
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the language if supported from the cookies header.
|
||||
*
|
||||
* Returns `null` if not supported or not found.
|
||||
*/
|
||||
export const extractSupportedLanguageFromCookies = (
|
||||
cookies: ReadonlyRequestCookies,
|
||||
): SupportedLanguageCodes | null => {
|
||||
const preferredLanguage = cookies.get('i18n');
|
||||
|
||||
const foundSupportedLanguage = APP_I18N_OPTIONS.supportedLangs.find(
|
||||
(lang): lang is SupportedLanguageCodes => lang === preferredLanguage?.value,
|
||||
);
|
||||
|
||||
return foundSupportedLanguage || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the language from the `accept-language` header.
|
||||
*
|
||||
* Returns `null` if not supported or not found.
|
||||
*/
|
||||
export const extractSupportedLanguageFromHeaders = (
|
||||
headers: Headers,
|
||||
): SupportedLanguageCodes | null => {
|
||||
const locales = headers.get('accept-language') ?? '';
|
||||
|
||||
const [locale] = locales.split(',');
|
||||
|
||||
// Convert locale to language.
|
||||
const [language] = locale.split('-');
|
||||
const parseLanguageFromLocale = (locale: string): SupportedLanguageCodes | null => {
|
||||
const [language, _country] = locale.split('-');
|
||||
|
||||
const foundSupportedLanguage = APP_I18N_OPTIONS.supportedLangs.find(
|
||||
(lang): lang is SupportedLanguageCodes => lang === language,
|
||||
);
|
||||
|
||||
return foundSupportedLanguage || null;
|
||||
if (!foundSupportedLanguage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return foundSupportedLanguage;
|
||||
};
|
||||
|
||||
type ExtractSupportedLanguageOptions = {
|
||||
headers?: Headers;
|
||||
cookies?: ReadonlyRequestCookies;
|
||||
/**
|
||||
* Extract the language if supported from the cookies header.
|
||||
*
|
||||
* Returns `null` if not supported or not found.
|
||||
*/
|
||||
export const extractLocaleDataFromCookies = (
|
||||
cookies: ReadonlyRequestCookies,
|
||||
): SupportedLanguageCodes | null => {
|
||||
const preferredLocale = cookies.get('language')?.value || '';
|
||||
|
||||
const language = parseLanguageFromLocale(preferredLocale || '');
|
||||
|
||||
if (!language) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return language;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the language from the `accept-language` header.
|
||||
*/
|
||||
export const extractLocaleDataFromHeaders = (
|
||||
headers: Headers,
|
||||
): { lang: SupportedLanguageCodes | null; locales: string[] } => {
|
||||
const headerLocales = (headers.get('accept-language') ?? '').split(',');
|
||||
|
||||
const language = parseLanguageFromLocale(headerLocales[0]);
|
||||
|
||||
return {
|
||||
lang: language,
|
||||
locales: [headerLocales[0]],
|
||||
};
|
||||
};
|
||||
|
||||
type ExtractLocaleDataOptions = {
|
||||
headers: Headers;
|
||||
cookies: ReadonlyRequestCookies;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -63,25 +73,25 @@ type ExtractSupportedLanguageOptions = {
|
||||
*
|
||||
* Will return the default fallback language if not found.
|
||||
*/
|
||||
export const extractSupportedLanguage = ({
|
||||
export const extractLocaleData = ({
|
||||
headers,
|
||||
cookies,
|
||||
}: ExtractSupportedLanguageOptions): SupportedLanguageCodes => {
|
||||
if (cookies) {
|
||||
const langCookie = extractSupportedLanguageFromCookies(cookies);
|
||||
}: ExtractLocaleDataOptions): I18nLocaleData => {
|
||||
let lang: SupportedLanguageCodes | null = extractLocaleDataFromCookies(cookies);
|
||||
|
||||
if (langCookie) {
|
||||
return langCookie;
|
||||
}
|
||||
const langHeader = extractLocaleDataFromHeaders(headers);
|
||||
|
||||
if (!lang && langHeader?.lang) {
|
||||
lang = langHeader.lang;
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
const langHeader = extractSupportedLanguageFromHeaders(headers);
|
||||
|
||||
if (langHeader) {
|
||||
return langHeader;
|
||||
}
|
||||
// Override web app to be English.
|
||||
if (!IS_APP_WEB_I18N_ENABLED && IS_APP_WEB) {
|
||||
lang = 'en';
|
||||
}
|
||||
|
||||
return APP_I18N_OPTIONS.sourceLang;
|
||||
return {
|
||||
lang: lang || APP_I18N_OPTIONS.sourceLang,
|
||||
locales: langHeader.locales,
|
||||
};
|
||||
};
|
||||
|
||||
@ -28,9 +28,18 @@ export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
|
||||
}
|
||||
|
||||
if (!cert) {
|
||||
cert = Buffer.from(
|
||||
fs.readFileSync(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12'),
|
||||
);
|
||||
let certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || '/opt/documenso/cert.p12';
|
||||
|
||||
// We don't want to make the development server suddenly crash when using the `dx` script
|
||||
// so we retain this when NODE_ENV isn't set to production which it should be in most production
|
||||
// deployments.
|
||||
//
|
||||
// Our docker image automatically sets this so it shouldn't be an issue for self-hosters.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
certPath = process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12';
|
||||
}
|
||||
|
||||
cert = Buffer.from(fs.readFileSync(certPath));
|
||||
}
|
||||
|
||||
const signature = signWithP12({
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
ZResendDocumentMutationSchema,
|
||||
ZSearchDocumentsMutationSchema,
|
||||
ZSendDocumentMutationSchema,
|
||||
ZSetDocumentEmailSettingsMutationSchema,
|
||||
ZSetPasswordForDocumentMutationSchema,
|
||||
ZSetSettingsForDocumentMutationSchema,
|
||||
ZSetTitleForDocumentMutationSchema,
|
||||
@ -275,6 +276,32 @@ export const documentRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
setDocumentEmailSettings: authenticatedProcedure
|
||||
.input(ZSetDocumentEmailSettingsMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { documentId, subject, message } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
return await upsertDocumentMeta({
|
||||
documentId,
|
||||
userId,
|
||||
subject,
|
||||
message,
|
||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message:
|
||||
'We were unable to update the email settings for this document. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
setPasswordForDocument: authenticatedProcedure
|
||||
.input(ZSetPasswordForDocumentMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@ -139,6 +139,12 @@ export const ZSendDocumentMutationSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZSetDocumentEmailSettingsMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
subject: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSetPasswordForDocumentMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
password: z.string(),
|
||||
|
||||
35
packages/ui/components/call-to-action.tsx
Normal file
35
packages/ui/components/call-to-action.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '../primitives/button';
|
||||
import { Card, CardContent } from '../primitives/card';
|
||||
|
||||
type CallToActionProps = {
|
||||
className?: string;
|
||||
utmSource?: string;
|
||||
};
|
||||
|
||||
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
|
||||
return (
|
||||
<Card spotlight className={className}>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<h2 className="text-center text-2xl font-bold">Looking for the managed solution?</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
|
||||
You can get started with Documenso in minutes. We handle the infrastructure, so you can
|
||||
focus on signing documents.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="focus-visible:ring-ring ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90text-sm mt-8 inline-flex items-center justify-center rounded-full border font-medium no-underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
variant="default"
|
||||
size="lg"
|
||||
asChild
|
||||
>
|
||||
<Link href={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
57
packages/ui/components/common/language-switcher-dialog.tsx
Normal file
57
packages/ui/components/common/language-switcher-dialog.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
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 {
|
||||
CommandDialog,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
|
||||
type LanguageSwitcherDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialogProps) => {
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
const setLanguage = async (lang: string) => {
|
||||
setOpen(false);
|
||||
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<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',
|
||||
i18n.locale === language.short ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES[language.short].full}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
};
|
||||
@ -1,14 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
@ -45,6 +54,7 @@ export const AddSubjectFormPartial = ({
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TAddSubjectFormSchema>({
|
||||
defaultValues: {
|
||||
@ -56,6 +66,111 @@ export const AddSubjectFormPartial = ({
|
||||
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||
});
|
||||
|
||||
const [oldEmailData, setOldEmailData] = useState<{ subject: string; message: string } | null>(
|
||||
document.documentMeta
|
||||
? {
|
||||
subject: document.documentMeta.subject ?? '',
|
||||
message: document.documentMeta.message ?? '',
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
const [changedFields, setChangedFields] = useState<{ subject: boolean; message: boolean }>({
|
||||
subject: false,
|
||||
message: false,
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { mutateAsync: setEmailSettingsForDocument } =
|
||||
trpc.document.setDocumentEmailSettings.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: () => {
|
||||
const data = utils.document.getDocumentWithDetailsById.getData({
|
||||
id: document.id,
|
||||
});
|
||||
|
||||
setOldEmailData({
|
||||
subject: data?.documentMeta?.subject ?? '',
|
||||
message: data?.documentMeta?.message ?? '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleOnBlur = async (field: 'subject' | 'message', value: string) => {
|
||||
if (value == oldEmailData?.[field]) return;
|
||||
|
||||
try {
|
||||
await setEmailSettingsForDocument({
|
||||
documentId: document.id,
|
||||
[field]: value,
|
||||
});
|
||||
|
||||
setChangedFields((prev) => ({ ...prev, [field]: true }));
|
||||
router.refresh();
|
||||
|
||||
toast({
|
||||
title: 'Email settings updated',
|
||||
description: 'The email settings for the document have been updated.',
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while updating the email settings.',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUndoButton = async (field: 'subject' | 'message') => {
|
||||
if (!oldEmailData) return;
|
||||
|
||||
try {
|
||||
await setEmailSettingsForDocument({
|
||||
documentId: document.id,
|
||||
[field]: oldEmailData[field],
|
||||
});
|
||||
|
||||
setValue(`meta.${field}`, oldEmailData[field]);
|
||||
|
||||
setOldEmailData((prev) => (prev ? { ...prev, [field]: undefined } : null));
|
||||
setChangedFields((prev) => ({ ...prev, [field]: false }));
|
||||
router.refresh();
|
||||
|
||||
toast({
|
||||
title: 'Change reverted',
|
||||
description: `The latest change to the ${field} has been reverted.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
if (oldEmailData.subject === undefined && oldEmailData.message === undefined) {
|
||||
setOldEmailData(null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while undoing the latest change',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setChangedFields({ subject: false, message: false });
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [oldEmailData]);
|
||||
|
||||
const onFormSubmit = handleSubmit(onSubmit);
|
||||
const { currentStep, totalSteps, previousStep } = useStep();
|
||||
|
||||
@ -65,6 +180,7 @@ export const AddSubjectFormPartial = ({
|
||||
title={documentFlow.title}
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{isDocumentPdfLoaded &&
|
||||
@ -84,9 +200,27 @@ export const AddSubjectFormPartial = ({
|
||||
id="subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.subject')}
|
||||
{...register('meta.subject', {
|
||||
onBlur: (event) => {
|
||||
void handleOnBlur('subject', event.target.value);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
{changedFields.subject && oldEmailData && (
|
||||
<div className="mt-2 flex items-center">
|
||||
<Button
|
||||
className="-ml-2"
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => void handleUndoButton('subject')}
|
||||
>
|
||||
<span className="text-xs">Undo change</span>
|
||||
<Loader className="ml-1 h-4 w-4 animate-spin text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormErrorMessage className="mt-2" error={errors.meta?.subject} />
|
||||
</div>
|
||||
|
||||
@ -101,15 +235,32 @@ export const AddSubjectFormPartial = ({
|
||||
id="message"
|
||||
className="bg-background mt-2 h-32 resize-none"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.message')}
|
||||
{...register('meta.message', {
|
||||
onBlur: (event) => {
|
||||
void handleOnBlur('message', event.target.value);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
{changedFields.message && oldEmailData && (
|
||||
<div className="mt-2 flex items-center">
|
||||
<Button
|
||||
className="-ml-2"
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => void handleUndoButton('message')}
|
||||
>
|
||||
<span className="text-xs">Undo change</span>
|
||||
<Loader className="ml-1 h-4 w-4 animate-spin text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormErrorMessage
|
||||
className="mt-2"
|
||||
error={typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DocumentSendEmailMessageHelper />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -155,17 +155,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
|
||||
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
|
||||
|
||||
const { data: fieldData } = trpc.field.getField.useQuery(
|
||||
{
|
||||
fieldId: Number(field.nativeId),
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
enabled: doesFieldExist,
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMeta = fieldData?.fieldMeta;
|
||||
const fieldMeta = field?.fieldMeta;
|
||||
|
||||
const localStorageKey = `field_${field.formId}_${field.type}`;
|
||||
|
||||
|
||||
@ -9,6 +9,13 @@ import type { StrokeOptions } from 'perfect-freehand';
|
||||
import { getStroke } from 'perfect-freehand';
|
||||
|
||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getSvgPathFromStroke } from './helper';
|
||||
@ -36,6 +43,7 @@ export const SignaturePad = ({
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
const [lines, setLines] = useState<Point[][]>([]);
|
||||
const [currentLine, setCurrentLine] = useState<Point[]>([]);
|
||||
const [selectedColor, setSelectedColor] = useState('black');
|
||||
|
||||
const perfectFreehandOptions = useMemo(() => {
|
||||
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
|
||||
@ -85,6 +93,7 @@ export const SignaturePad = ({
|
||||
ctx.restore();
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
lines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -129,6 +138,7 @@ export const SignaturePad = ({
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
newLines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -237,7 +247,13 @@ export const SignaturePad = ({
|
||||
>
|
||||
<canvas
|
||||
ref={$el}
|
||||
className={cn('relative block dark:invert', className)}
|
||||
className={cn(
|
||||
'relative block',
|
||||
{
|
||||
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{ touchAction: 'none' }}
|
||||
onPointerMove={(event) => onMouseMove(event)}
|
||||
onPointerDown={(event) => onMouseDown(event)}
|
||||
@ -247,6 +263,44 @@ export const SignaturePad = ({
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="text-foreground absolute right-2 top-2 filter">
|
||||
<Select defaultValue={selectedColor} onValueChange={(value) => setSelectedColor(value)}>
|
||||
<SelectTrigger className="h-auto w-auto border-none p-1">
|
||||
<SelectValue placeholder="" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent className="w-[150px]" align="end">
|
||||
<SelectItem value="black">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm" />
|
||||
<Trans>Black</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="red">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[red] shadow-sm" />
|
||||
<Trans>Red</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="blue">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[blue] shadow-sm" />
|
||||
<Trans>Blue</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="green">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[green] shadow-sm" />
|
||||
<Trans>Green</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -112,6 +112,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
signerToken:
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
})),
|
||||
},
|
||||
});
|
||||
@ -156,6 +157,38 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
|
||||
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
||||
fields
|
||||
.filter((field) => field.type === fieldType)
|
||||
.filter((field) => {
|
||||
if (field.fieldMeta && 'values' in field.fieldMeta) {
|
||||
return field.fieldMeta.values?.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const emptyCheckboxFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.CHECKBOX),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const emptyRadioFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.RADIO),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const emptySelectFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.DROPDOWN),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const hasErrors =
|
||||
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
|
||||
|
||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
@ -788,6 +821,24 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
{hasErrors && (
|
||||
<div className="mt-4">
|
||||
<ul>
|
||||
<li className="text-sm text-red-500">
|
||||
<Trans>
|
||||
To proceed further, please set at least one value for the{' '}
|
||||
{emptyCheckboxFields.length > 0
|
||||
? 'Checkbox'
|
||||
: emptyRadioFields.length > 0
|
||||
? 'Radio'
|
||||
: 'Select'}{' '}
|
||||
field.
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
|
||||
|
||||
@ -795,6 +846,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
goNextLabel={msg`Save Template`}
|
||||
disableNextStep={hasErrors}
|
||||
onGoBackClick={() => {
|
||||
previousStep();
|
||||
remove();
|
||||
|
||||
@ -43,6 +43,8 @@ services:
|
||||
envVarKey: RENDER_EXTERNAL_URL
|
||||
- key: NEXT_PUBLIC_MARKETING_URL
|
||||
value: 'http://localhost:3001'
|
||||
- key: NEXT_PRIVATE_INTERNAL_WEBAPP_URL
|
||||
value: 'http://localhost:10000'
|
||||
|
||||
# SMTP
|
||||
- key: NEXT_PRIVATE_SMTP_TRANSPORT
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
"NEXT_PUBLIC_PROJECT",
|
||||
"NEXT_PUBLIC_WEBAPP_URL",
|
||||
"NEXT_PUBLIC_MARKETING_URL",
|
||||
"NEXT_PRIVATE_INTERNAL_WEBAPP_URL",
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
||||
|
||||
Reference in New Issue
Block a user