mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
18 Commits
exp/next-1
...
feat/pulum
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aeb6325b0 | |||
| 2c1a18bafc | |||
| a2db5e9642 | |||
| 4ec9dc78c1 | |||
| faf2bd5384 | |||
| d40ed94b74 | |||
| cd3d9b701b | |||
| e40f47a73c | |||
| 64ea4a6f9f | |||
| 18115e95d7 | |||
| e736261056 | |||
| 2e57da7549 | |||
| 574454db0a | |||
| f05b670d93 | |||
| 318149fbf3 | |||
| 5f19dcf25c | |||
| c99cf4b848 | |||
| 18ec40f6af |
@ -16,12 +16,12 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"next-plausible": "^3.12.0",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"index": "Getting Started",
|
||||
"contributing-translations": "Contributing Translations"
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Contributing Translations
|
||||
description: Learn how to contribute translations to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Contributing Translations
|
||||
|
||||
We are always open for help with translations! Currently we utilise AI to generate the initial translations for new languages, which are then improved over time by our awesome community.
|
||||
|
||||
If you are looking for development notes on translations, you can find them [here](/developers/local-development/translations).
|
||||
|
||||
<Callout type="info">
|
||||
Contributions are made through GitHub Pull Requests, so you will need a GitHub account to
|
||||
contribute.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
We store our translations in PO files, which are located in our GitHub repository [here](https://github.com/documenso/documenso/tree/main/packages/lib/translations).
|
||||
|
||||
The translation files are organized into folders represented by their respective language codes (`en` for English, `de` for German, etc). Each language folder contains three PO files:
|
||||
|
||||
1. `web.po`: Translations for the web application
|
||||
2. `marketing.po`: Translations for the marketing application
|
||||
3. `common.po`: Shared translations between web and marketing
|
||||
|
||||
Each PO file contains translations which look like this:
|
||||
|
||||
```po
|
||||
#: apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
||||
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
||||
```
|
||||
|
||||
- `msgid`: The original text in English (never edit this manually)
|
||||
- `msgstr`: The translated text in the target language
|
||||
|
||||
<Callout type="warning">
|
||||
Notice the `<0>` tags? These represent HTML elements and must remain in both the `msgid` and `msgstr`. Make sure to translate the content between these tags while keeping the tags intact.
|
||||
</Callout>
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Updating Existing Translations
|
||||
|
||||
1. Fork the repository.
|
||||
2. Navigate to the appropriate language folder.
|
||||
3. Open the PO file you want to update (web.po, marketing.po, or common.po).
|
||||
4. Make your changes, ensuring you follow the PO file format.
|
||||
5. Commit your changes with a message such as `chore: update German translations`
|
||||
6. Create a Pull Request.
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
If you want to add translations for a language that doesn't exist yet:
|
||||
|
||||
1. Create an issue in our GitHub repository requesting the addition of the new language.
|
||||
2. Wait for our team to review and approve the request.
|
||||
3. Once approved, we will set up the necessary files and kickstart the translations with AI to provide initial coverage.
|
||||
|
||||
## Need Help?
|
||||
|
||||
<Callout type="info">
|
||||
If you have any questions, hop into our [Discord](https://documen.so/discord) and ask us directly!
|
||||
</Callout>
|
||||
|
||||
Thank you for helping make Documenso more accessible to users around the world!
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Contributing Guide
|
||||
title: Getting started
|
||||
description: Learn how to contribute to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 'Customer Story Prisma: 4 Reasons why Prisma chose Documenso for Signatures'
|
||||
description: We are happy to welcome Prisma, another OSS company, as a customer. Read here why they choose us.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-09-26
|
||||
tags:
|
||||
- Prisma
|
||||
- Customer Story
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/prisma.png"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="Primsa Landing Page We simplify database migration, connection pooling, database queries, and readable data models."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Prisma uses Documenso for collaborative team signing.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; Prisma is now using Documenso, and [we added visibility scopes](https://docs.documenso.com/users/document-visibility)
|
||||
|
||||
# Prisma
|
||||
|
||||
Prisma is an open-source company known for its modern OSS ORM (Object-Relational Mapping) tools that simplify database interactions for developers. Their flagship product, Prisma ORM, provides a type-safe way to query databases like PostgreSQL, MySQL, and many more. With the addition of Prisma Studio, an intuitive database management interface, Prisma makes it easier and more efficient for developers to work with databases. With their new additions, Prisma Pulse and Accelerate, you can react to real-time database changes and optimize your queries. And they are completely [open source](https://github.com/prisma/prisma)!
|
||||
|
||||
# We choose Prisma too!
|
||||
|
||||
I discovered Prisma when planning the tech stack for the [first version of Documenso](https://github.com/documenso/documenso/releases/tag/0.9-developer-preview). Prisma has felt natural to use since day one and has been the base of our database architecture ever since. It's great to see them develop and grow with us.
|
||||
|
||||
# Why they choose us
|
||||
|
||||
## 1. Signature Flows
|
||||
|
||||
Documenso signing flows are highly configurable, designed to adapt to the needs of any document signing process. Whether you're working with different roles, varying settings, or specific delivery methods, Documenso offers the flexibility to suit your requirements. You can choose to send documents via email, share a manual link, generate a link through the API, or even use a static direct link for quick access—all while ensuring a smooth signing experience.
|
||||
|
||||
Additionally, you can create templates to streamline and reuse common workflows, saving valuable time. Direct link templates enable users to drive the flow themselves, providing a straightforward path for signing. For a seamless experience, Documenso also allows you to embed the signing process directly into your website, ensuring an uninterrupted, integrated workflow tailored to your needs.
|
||||
|
||||
## 2. Modern UX
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/dsux.png"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="A completed document in Documenso, ready to download."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
We call Documenso's design "Happy Minimalism"
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
We’ve crafted Documenso with a sleek, modern interface that makes it incredibly easy to use. Whether you’re signing documents, managing workflows, or fine-tuning settings, its intuitive design allows you to accomplish tasks quickly and effortlessly. More than just powerful, Documenso is a pleasure to navigate—designed to be accessible to everyone, no matter their level of tech experience.
|
||||
|
||||
## 3. Teams
|
||||
|
||||
### Teamwork Makes the Dream Work
|
||||
|
||||
Documenso makes teamwork a breeze with its team management features. You can easily set up and organize teams, making it simple to share and manage documents and workflows together. This is a lifesaver for larger organizations or teams spread across different departments, ensuring everyone stays in sync and on track. Different visibility scopes ensure private documents stay private and others are shared for easy collaboration.
|
||||
|
||||
### Document Visibility
|
||||
|
||||
Collaboration within a team often demands different levels of access to documents. For instance, the Documenso team at Prisma needed a way to set custom visibility on some documents while keeping others accessible to everyone. To address this need, we introduced role-based visibility scopes. This feature allows teams to manage documents more effectively. They can make certain documents visible only to managers or, in special cases, restricted to admins. This ensures sensitive information stays protected while general documents remain accessible to those who need them.
|
||||
|
||||
Learn more about visibility scopes and [how they can benefit your team here](https://docs.documenso.com/users/document-visibility).
|
||||
|
||||
## 4. OSS!
|
||||
|
||||
As you might know, we are open-source! This means you can peek under the hood, tweak things to your liking, and even contribute to making the platform better. We love the community-driven aspect of open-source, and it aligns perfectly with our goal to keep improving and innovating with input from our users.
|
||||
|
||||
So, whether you're looking to streamline your document workflows or just need a solid, reliable platform, Documenso has got your back. And we're thrilled to serve another OSS company and help make the space more open.
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
86
apps/marketing/content/blog/go-fork-yourself.mdx
Normal file
86
apps/marketing/content/blog/go-fork-yourself.mdx
Normal file
@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Go Fork Yourself
|
||||
description: Curious about our take on open-source and code forking? Discover why we see forking not as a threat but as a vital part of the Open Source ecosystem.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-10-03
|
||||
tags:
|
||||
- Culture
|
||||
- Open Startup
|
||||
- Open Source
|
||||
---
|
||||
|
||||
> TLDR; At Documenso, we see OSS as co-owned by all. Forking—collaborative or not—is part of the open-source spirit.
|
||||
|
||||
## Freedom vs. Ownership
|
||||
|
||||
Recently, there has been a lot of debate on the subject of forks and the usage of OSS IP (Open Source Software Intellectual Property). While I mostly aim to stay out of these controversies (as there is no “winning”), I wanted to take this opportunity to share my views on IP and forking culture here at Documenso. I don’t presume this is the ideal path, but for me, it’s the only path that makes sense.
|
||||
|
||||
What these issues show foremost, in my opinion, is that the concept of Open Source is still evolving. I have heard many say, “Open Source is clearly defined” and that there is no ambiguity anymore. That may be true on the legal side, but there are vast differences in how these rules are interpreted and lived out. Here are a few questions to illustrate the point:
|
||||
|
||||
1. Is it okay to use an open-source project without ever giving back?
|
||||
2. Is it okay to fork (some might say copy) an OSS product and build something on top of it?
|
||||
3. Are we morally obliged to fight those who provide different answers to these questions than we do?
|
||||
|
||||
## Embracing Forks and Collaboration
|
||||
|
||||
Since starting Documenso, I’ve thought a lot about what it actually means to be Open Source for us. So far, it has been about openness in working with everyone, from contributors to customers and sharing our work transparently. For this, we have been richly rewarded with attention and reach. This collaborative give-and-take is what people commonly associate with being Open Source, and it seems ideal.
|
||||
|
||||
Yet, there are the questions mentioned above. And while these may be contentious, my take is straightforward:
|
||||
|
||||
1. Yes.
|
||||
2. Yes.
|
||||
3. No.
|
||||
|
||||
I say this because, to me, the principles of Open Source are rooted in freedom and collaboration. That means allowing others to use, improve, or even compete with what you’ve built without feeling possessive over the code. The beauty of Open Source lies in its openness—its ability to be forked, reused, and adapted by anyone.
|
||||
|
||||
You may answer these questions differently for your own reasons. One thing I’ve found lacking in the discourse is the fact that Open Source is still being treated as socially proprietary. If it’s under an open-source license, you can fork it and try to improve upon the original, and there’s nothing wrong with that. The same is true for closed-source startups. Yet in Open Source, there’s a notion that it’s somehow “dirty,” even though the license explicitly allows it.
|
||||
|
||||
## Forking in Action: Real-World Examples
|
||||
|
||||
When the team behind **Node.js** disagreed with its governance and pace of development, they forked the project to create **io.js**. This wasn’t seen as dirty but as a necessary push for change. In fact, the fork resulted in positive changes—better community governance and faster development—which eventually led to the merge of the two projects under the Node.js Foundation. It shows that forking can be a catalyst for improvement, not just competition.
|
||||
|
||||
## The Misconception of “Exploitative” Usage
|
||||
|
||||
However, sometimes forks don’t merge back but still bring positive change. A good example is **Jenkins**, which was forked from **Hudson** over disagreements in governance after Oracle acquired Sun Microsystems. Jenkins quickly overtook Hudson in terms of community support, development, and innovation. Rather than being seen as a hostile move, the fork enabled Jenkins to become a thriving project, better aligned with the open-source ethos of collaboration and transparency. It emphasizes that forking isn’t inherently exploitative; it can simply be a way to realize a project’s full potential.
|
||||
|
||||
And then there’s **MariaDB**, a fork of **MySQL**. After Oracle acquired MySQL, many in the community feared the project’s open-source nature could be compromised. The fork preserved its spirit, and MariaDB has since grown to become a popular and thriving database. It’s a reminder that sometimes, forking is not just acceptable—it’s necessary to uphold the values and freedoms of open-source software.
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/owncode.jpeg"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="Meme: If everyone owns the code, no one does."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Funny Meme to drive the point home.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
My view is that the code is not “your” code, just as Documenso’s code is not “our” code. It’s been co-owned by the world ever since we published the repo under AGPL V3. That is the whole point. It’s finally not owned by anyone (cue the “everyone/no one” meme). Open Source is for everyone, even competitors. Yet, we are still treating the licenses as extensions of the old, proprietary world and defending perceived injustices based on that model.
|
||||
|
||||
> Side Note: Full compliance with all license and other legal rules is a given here.
|
||||
|
||||
## Documenso’s Approach: Co-Ownership and Community
|
||||
|
||||
So, if you want to fork Documenso and build a business on it, you can. Whether that’s a cool thing to do is another matter. Whether you do a better job than us is also another matter (you won’t). But if you do, I’ll be the first to join. But why not join us from the start since you already have the upside? We exist because we believe this to be the best way forward—not because we force it.
|
||||
|
||||
## The Bigger Picture: Open-Source as Progress
|
||||
|
||||
I’ve also thought a lot about question #3. I understand the impulse to fight anyone who doesn’t appreciate this collaborative approach, but there is no part of this model that backs that up. You are free to “exploit” as long as it’s in a way that adds value. The fallacy is in considering someone else using the OSS part for their business as treason, which it’s not. It’s the whole point.
|
||||
|
||||
While some might say this is theoretical and that reality is different, this is the version of Open Source on which we are building Documenso. The point here is that OSS companies must be resilient to handle forking and competition; without this resilience, an open source driven economy can’t thrive. The focus on freedom and collaboration means being prepared for forks and challenges as part of the growth, not as threats.
|
||||
|
||||
Of course, all of this applies to Documenso, the OSS project, not Documenso Inc., the company, which is very much a privately owned, for-profit entity. However, since the goal is to scale Documenso to the entire world, there is plenty of room to see everyone as co-owners of the Open Source project rather than as competitors. In the end, Open Source is about progress through freedom. If you don’t like how we run things, go fork yourself and hold us accountable. We don’t own this; we just happened to start it.
|
||||
|
||||
> Since this article is open source as well, you are free to fork it and change it here: [https://documen.so/repo](https://documen.so/repo)
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -8,6 +8,61 @@ Check out what's new in the latest version and read our thoughts on it. For more
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.7.1: Signing order and document visibility
|
||||
|
||||
We're excited to introduce Documenso v1.7.1, bringing you improved control over your document signing process. Here are the key updates:
|
||||
|
||||
## 🌟 Key New Features
|
||||
|
||||
### 1. Signing Order
|
||||
|
||||
Specify the sequence in which recipients sign your documents. This ensures a structured signing process, particularly useful for complex agreements or hierarchical approvals.
|
||||
|
||||
<video
|
||||
src="/changelog/signing-order-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
### 2. Document Visibility
|
||||
|
||||
Manage who can view your documents and when. This feature offers greater privacy and flexibility in your document sharing workflows.
|
||||
|
||||
<video
|
||||
src="/changelog/document-visibility-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
## 🔧 Other Improvements
|
||||
|
||||
- Added language switcher for easier language selection
|
||||
- Support for smaller field bounds in documents
|
||||
- Improved select field UX
|
||||
- Enhanced template functionality for advanced fields
|
||||
- Added authOptions to the API
|
||||
- Various UI refinements and bug fixes
|
||||
|
||||
## 💡 Recent Features
|
||||
|
||||
Don't forget about these powerful features from our recent v1.7.0 release:
|
||||
|
||||
- Embedded Signing Experience
|
||||
- Copy and Paste Fields
|
||||
- Customizable Signature Colors
|
||||
|
||||
## 👏 Thank You
|
||||
|
||||
As always, we're grateful for our community's contributions and feedback. Your input continues to shape Documenso into the leading open-source document signing solution.
|
||||
|
||||
We're eager to see how these new features enhance your document workflows. Enjoy exploring Documenso v1.7.1!
|
||||
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.1-rc.3",
|
||||
"version": "1.7.2-rc.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -32,16 +32,16 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-plausible": "^3.10.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.77.3",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.11.0",
|
||||
"recharts": "^2.7.2",
|
||||
@ -51,9 +51,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/marketing/public/blog/dsux.png
Normal file
BIN
apps/marketing/public/blog/dsux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 692 KiB |
BIN
apps/marketing/public/blog/owncode.jpeg
Normal file
BIN
apps/marketing/public/blog/owncode.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/marketing/public/blog/prisma.png
Normal file
BIN
apps/marketing/public/blog/prisma.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 435 KiB |
BIN
apps/marketing/public/changelog/document-visibility-demo.mp4
Normal file
BIN
apps/marketing/public/changelog/document-visibility-demo.mp4
Normal file
Binary file not shown.
BIN
apps/marketing/public/changelog/signing-order-demo.mp4
Normal file
BIN
apps/marketing/public/changelog/signing-order-demo.mp4
Normal file
Binary file not shown.
@ -19,10 +19,10 @@ export const TEAM_MEMBERS = [
|
||||
},
|
||||
{
|
||||
name: 'Ephraim Atta-Duncan',
|
||||
role: 'Software Engineer - Intern',
|
||||
salary: 15_000,
|
||||
role: 'Software Engineer - I',
|
||||
salary: 60_000,
|
||||
location: 'Ghana',
|
||||
engagement: msg`Part-Time`,
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'June 6th, 2023',
|
||||
},
|
||||
{
|
||||
|
||||
@ -108,14 +108,21 @@ export const Carousel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap());
|
||||
const newIndex = emblaApi.selectedScrollSnap();
|
||||
|
||||
setSelectedIndex(newIndex);
|
||||
emblaThumbsApi.scrollTo(newIndex);
|
||||
|
||||
resetProgress();
|
||||
|
||||
const currentVideo = videoRefs.current[newIndex];
|
||||
if (currentVideo) {
|
||||
currentVideo.currentTime = 0;
|
||||
}
|
||||
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
const autoplay = emblaApi?.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (autoplay) {
|
||||
autoplay.reset();
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@ -3,4 +3,4 @@
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@ -25,20 +25,13 @@ const FONT_NOTO_SANS_BYTES = fs.readFileSync(
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
serverExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
swcPlugins: [['@lingui/swc-plugin', {}]],
|
||||
turbo: {
|
||||
rules: {
|
||||
'*.po': {
|
||||
loaders: ['@lingui/loader'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
@ -56,11 +49,26 @@ const config = {
|
||||
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
||||
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
|
||||
},
|
||||
// modularizeImports: {
|
||||
// 'lucide-react': {
|
||||
// transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||
// },
|
||||
// },
|
||||
modularizeImports: {
|
||||
'lucide-react': {
|
||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||
},
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// fixes: Module not found: Can’t resolve ‘../build/Release/canvas.node’
|
||||
if (isServer) {
|
||||
config.resolve.alias.canvas = false;
|
||||
}
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.po$/,
|
||||
use: {
|
||||
loader: '@lingui/loader',
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.1-rc.3",
|
||||
"version": "1.7.2-rc.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -35,7 +35,7 @@
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-plausible": "^3.10.1",
|
||||
@ -44,9 +44,9 @@
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"posthog-js": "^1.75.3",
|
||||
"posthog-node": "^3.1.1",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"react-call": "^1.3.0",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
@ -63,7 +63,7 @@
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@lingui/loader": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"@lingui/swc-plugin": "4.0.8",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
@ -74,4 +74,4 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
|
||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
@ -25,16 +26,17 @@ import { DataTableSenderFilter } from './data-table-sender-filter';
|
||||
import { EmptyDocumentState } from './empty-state';
|
||||
import { UploadDocument } from './upload-document';
|
||||
|
||||
export type DocumentsPageViewProps = {
|
||||
export interface DocumentsPageViewProps {
|
||||
searchParams?: {
|
||||
status?: ExtendedDocumentStatus;
|
||||
period?: PeriodSelectorValue;
|
||||
page?: string;
|
||||
perPage?: string;
|
||||
senderIds?: string;
|
||||
search?: string;
|
||||
};
|
||||
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
||||
};
|
||||
}
|
||||
|
||||
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
@ -44,6 +46,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 20;
|
||||
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
||||
const search = searchParams.search || '';
|
||||
const currentTeam = team
|
||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
||||
: undefined;
|
||||
@ -52,6 +55,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
const getStatOptions: GetStatsInput = {
|
||||
user,
|
||||
period,
|
||||
search,
|
||||
};
|
||||
|
||||
if (team) {
|
||||
@ -79,6 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
perPage,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
});
|
||||
|
||||
const getTabHref = (value: typeof status) => {
|
||||
@ -135,10 +140,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
<DocumentStatus status={value} />
|
||||
|
||||
{value !== ExtendedDocumentStatus.ALL && (
|
||||
<span className="ml-1 inline-block opacity-50">
|
||||
{Math.min(stats[value], 99)}
|
||||
{stats[value] > 99 && '+'}
|
||||
</span>
|
||||
<span className="ml-1 inline-block opacity-50">{stats[value]}</span>
|
||||
)}
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
@ -151,6 +153,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||
<PeriodSelector />
|
||||
</div>
|
||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||
<DocumentSearch initialValue={search} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { RecipientStatusType, getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { DocumentStatus, Recipient } from '@documenso/prisma/client';
|
||||
@ -29,24 +31,26 @@ export const StackAvatarsWithTooltip = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const waitingRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||
(recipient) => getRecipientType(recipient) === RecipientStatusType.WAITING,
|
||||
);
|
||||
|
||||
const openedRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'opened',
|
||||
(recipient) => getRecipientType(recipient) === RecipientStatusType.OPENED,
|
||||
);
|
||||
|
||||
const completedRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'completed',
|
||||
(recipient) => getRecipientType(recipient) === RecipientStatusType.COMPLETED,
|
||||
);
|
||||
|
||||
const uncompletedRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'unsigned',
|
||||
(recipient) => getRecipientType(recipient) === RecipientStatusType.UNSIGNED,
|
||||
);
|
||||
|
||||
const sortedRecipients = useMemo(() => recipients.sort((a, b) => a.id - b.id), [recipients]);
|
||||
|
||||
return (
|
||||
<PopoverHover
|
||||
trigger={children || <StackAvatars recipients={recipients} />}
|
||||
trigger={children || <StackAvatars recipients={sortedRecipients} />}
|
||||
contentProps={{
|
||||
className: 'flex flex-col gap-y-5 py-2',
|
||||
side: position,
|
||||
@ -65,7 +69,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<div className="">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import {
|
||||
getExtraRecipientsType,
|
||||
getRecipientType,
|
||||
} from '@documenso/lib/client-only/recipient-type';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
|
||||
@ -13,20 +16,27 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) {
|
||||
const remainingItems = recipients.length - itemsToRender.length;
|
||||
|
||||
return itemsToRender.map((recipient: Recipient, index: number) => {
|
||||
const first = index === 0 ? true : false;
|
||||
const first = index === 0;
|
||||
|
||||
const lastItemText =
|
||||
index === itemsToRender.length - 1 && remainingItems > 0
|
||||
? `+${remainingItems + 1}`
|
||||
: undefined;
|
||||
if (index === 4 && remainingItems > 0) {
|
||||
return (
|
||||
<StackAvatar
|
||||
key="extra-recipient"
|
||||
first={first}
|
||||
zIndex={String(zIndex - index * 10)}
|
||||
type={getExtraRecipientsType(recipients.slice(4))}
|
||||
fallbackText={`+${remainingItems + 1}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StackAvatar
|
||||
key={recipient.id}
|
||||
first={first}
|
||||
zIndex={String(zIndex - index * 10)}
|
||||
type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)}
|
||||
fallbackText={lastItemText ? lastItemText : recipientAbbreviation(recipient)}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [searchTerm, setSearchTerm] = useState(initialValue);
|
||||
const debouncedSearchTerm = useDebouncedValue(searchTerm, 500);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(term: string) => {
|
||||
const params = new URLSearchParams(searchParams?.toString() ?? '');
|
||||
if (term) {
|
||||
params.set('search', term);
|
||||
} else {
|
||||
params.delete('search');
|
||||
}
|
||||
router.push(`?${params.toString()}`);
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearch(searchTerm);
|
||||
}, [debouncedSearchTerm]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search documents..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -89,7 +89,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
||||
className="h-full w-full object-contain dark:invert"
|
||||
/>
|
||||
) : (
|
||||
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
|
||||
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl">
|
||||
{field.Signature?.typedSignature}
|
||||
</p>
|
||||
),
|
||||
@ -122,7 +122,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
||||
{field.Recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||
<p
|
||||
className={cn('text-muted-foreground text-lg duration-200', {
|
||||
'font-signature sm:text-xl md:text-2xl lg:text-3xl':
|
||||
'font-signature sm:text-xl md:text-2xl':
|
||||
field.type === FieldType.SIGNATURE ||
|
||||
field.type === FieldType.FREE_SIGNATURE,
|
||||
})}
|
||||
|
||||
File diff suppressed because one or more lines are too long
3
infra/gcloud/.gitignore
vendored
Normal file
3
infra/gcloud/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
/bin/
|
||||
/node_modules/
|
||||
2
infra/gcloud/Pulumi.gcloud-dev.yaml
Normal file
2
infra/gcloud/Pulumi.gcloud-dev.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
config:
|
||||
gcp:project: scratchpad-438301
|
||||
10
infra/gcloud/Pulumi.yaml
Normal file
10
infra/gcloud/Pulumi.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
name: gcloud
|
||||
runtime:
|
||||
name: nodejs
|
||||
options:
|
||||
packagemanager: npm
|
||||
description: documenso on google cloud
|
||||
config:
|
||||
app--url: 'https://app.documenso.com'
|
||||
app--smtp-from-name: 'Documenso'
|
||||
app--smtp-from-address: 'noreply@documenso.com'
|
||||
418
infra/gcloud/index.ts
Normal file
418
infra/gcloud/index.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import * as gcp from '@pulumi/gcp';
|
||||
import * as pulumi from '@pulumi/pulumi';
|
||||
import * as random from '@pulumi/random';
|
||||
|
||||
const config = new pulumi.Config();
|
||||
|
||||
// Storage Config
|
||||
const storageLocation = config.get('storage--location') || 'EU';
|
||||
const serviceLocation = config.get('service--location') || 'europe-west3';
|
||||
|
||||
// KMS Config
|
||||
const algorithm = config.get('kms--algorithm') || 'RSA_SIGN_PKCS1_4096_SHA256';
|
||||
|
||||
// Database Config
|
||||
const databasePassword = config.get('db--password') || 'password';
|
||||
|
||||
// App Config
|
||||
// App Config
|
||||
const appUrl = config.require('app--url');
|
||||
const nextAuthSecret =
|
||||
config.getSecret('app--nextauth-secret') ||
|
||||
new random.RandomString('next-auth-secret', {
|
||||
length: 32,
|
||||
}).result;
|
||||
const encryptionKey =
|
||||
config.getSecret('app--encryption-key') ||
|
||||
new random.RandomString('encryption-key', {
|
||||
length: 32,
|
||||
}).result;
|
||||
const encryptionSecondaryKey =
|
||||
config.getSecret('app--encryption-secondary-key') ||
|
||||
new random.RandomString('encryption-secondary-key', {
|
||||
length: 32,
|
||||
}).result;
|
||||
const googleClientId = config.get('app--google-client-id') || '';
|
||||
const googleClientSecret = config.get('app--google-client-secret') || '';
|
||||
const oidcWellKnown = config.get('app--oidc-well-known') || '';
|
||||
const oidcClientId = config.get('app--oidc-client-id') || '';
|
||||
const oidcClientSecret = config.get('app--oidc-client-secret') || '';
|
||||
const oidcProviderLabel = config.get('app--oidc-provider-label') || 'OIDC';
|
||||
const oidcAllowSignup = config.get('app--oidc-allow-signup') || '';
|
||||
const oidcSkipVerify = config.get('app--oidc-skip-verify') || '';
|
||||
const internalWebappUrl = config.get('app--internal-webapp-url') || '';
|
||||
const smtpTransport = config.get('app--smtp-transport') || 'smtp-auth';
|
||||
const smtpHost = config.get('app--smtp-host') || '';
|
||||
const smtpPort = config.get('app--smtp-port') || '';
|
||||
const smtpUsername = config.get('app--smtp-username') || '';
|
||||
const smtpPassword = config.getSecret('app--smtp-password') || '';
|
||||
const smtpApikeyUser = config.get('app--smtp-apikey-user') || '';
|
||||
const smtpApikey = config.getSecret('app--smtp-apikey') || '';
|
||||
const smtpSecure = config.get('app--smtp-secure') || '';
|
||||
const smtpUnsafeIgnoreTls = config.get('app--smtp-unsafe-ignore-tls') || '';
|
||||
const smtpFromName = config.require('app--smtp-from-name');
|
||||
const smtpFromAddress = config.require('app--smtp-from-address');
|
||||
const resendApiKey = config.getSecret('app--resend-api-key') || '';
|
||||
const documentSizeUploadLimit = config.get('app--document-size-upload-limit') || '50';
|
||||
const stripeApiKey = config.getSecret('app--stripe-api-key') || '';
|
||||
const stripeWebhookSecret = config.getSecret('app--stripe-webhook-secret') || '';
|
||||
const stripeEnterprisePlanMonthlyPriceId =
|
||||
config.get('app--stripe-enterprise-plan-monthly-price-id') || '';
|
||||
const jobsProvider = config.get('app--jobs-provider') || 'local';
|
||||
const triggerApiKey = config.get('app--trigger-api-key') || '';
|
||||
const triggerApiUrl = config.get('app--trigger-api-url') || '';
|
||||
const inngestEventKey = config.get('app--inngest-event-key') || '';
|
||||
const posthogKey = config.get('app--posthog-key') || '';
|
||||
const billingEnabled = config.get('app--billing-enabled') || 'false';
|
||||
|
||||
new gcp.projects.Service('cloud-kms-api', {
|
||||
project: gcp.config.project,
|
||||
service: 'cloudkms.googleapis.com',
|
||||
disableOnDestroy: false,
|
||||
});
|
||||
|
||||
new gcp.projects.Service('cloud-run-api', {
|
||||
project: gcp.config.project,
|
||||
service: 'run.googleapis.com',
|
||||
disableOnDestroy: false,
|
||||
});
|
||||
|
||||
new gcp.projects.Service('compute-api', {
|
||||
project: gcp.config.project,
|
||||
service: 'compute.googleapis.com',
|
||||
disableOnDestroy: false,
|
||||
});
|
||||
|
||||
const signupDisabled = config.get('app--signup-disabled') || 'false';
|
||||
|
||||
// Create a GCS bucket for storage
|
||||
const storageBucket = new gcp.storage.Bucket('documenso-storage', {
|
||||
name: 'documenso-storage',
|
||||
location: storageLocation,
|
||||
});
|
||||
|
||||
// Create a service account so we can create a HMAC key to use the S3-compatible storage
|
||||
// API
|
||||
const storageServiceAccount = new gcp.serviceaccount.Account('documenso-storage-sa', {
|
||||
accountId: 'documenso-storage-sa',
|
||||
displayName: 'Storage Service Account',
|
||||
});
|
||||
|
||||
const appServiceAccount = new gcp.serviceaccount.Account('documenso-app-sa', {
|
||||
accountId: 'documenso-app-sa',
|
||||
displayName: 'App Service Account',
|
||||
});
|
||||
|
||||
// Create the HMAC key for the storage service account
|
||||
const hmacKey = new gcp.storage.HmacKey('documenso-storage-key', {
|
||||
serviceAccountEmail: storageServiceAccount.email,
|
||||
});
|
||||
|
||||
// Create a Cloud HSM cluster
|
||||
const hsmCluster = new gcp.kms.KeyRing('hsm-keyring', {
|
||||
name: 'documenso-hsm-keyring',
|
||||
location: serviceLocation,
|
||||
});
|
||||
|
||||
const hsmKey = new gcp.kms.CryptoKey('hsm-key', {
|
||||
name: 'documenso-hsm-key',
|
||||
keyRing: hsmCluster.id,
|
||||
purpose: 'ASYMMETRIC_SIGN',
|
||||
versionTemplate: {
|
||||
algorithm,
|
||||
protectionLevel: 'HSM',
|
||||
},
|
||||
});
|
||||
|
||||
// Create the database
|
||||
const database = new gcp.sql.DatabaseInstance('documenso-db', {
|
||||
name: 'documenso-db',
|
||||
databaseVersion: 'POSTGRES_15',
|
||||
region: serviceLocation,
|
||||
settings: {
|
||||
tier: 'db-custom-2-4096',
|
||||
diskSize: 50,
|
||||
diskType: 'PD_SSD',
|
||||
diskAutoresize: true,
|
||||
ipConfiguration: {
|
||||
ipv4Enabled: true,
|
||||
},
|
||||
backupConfiguration: {
|
||||
enabled: true,
|
||||
startTime: '02:00',
|
||||
backupRetentionSettings: {
|
||||
retainedBackups: 30,
|
||||
},
|
||||
pointInTimeRecoveryEnabled: true,
|
||||
transactionLogRetentionDays: 7,
|
||||
},
|
||||
databaseFlags: [
|
||||
{ name: 'max_connections', value: '100' },
|
||||
{ name: 'log_min_duration_statement', value: '300' },
|
||||
],
|
||||
maintenanceWindow: {
|
||||
day: 7,
|
||||
hour: 3,
|
||||
},
|
||||
insightsConfig: {
|
||||
queryInsightsEnabled: true,
|
||||
queryStringLength: 1024,
|
||||
recordApplicationTags: true,
|
||||
recordClientAddress: true,
|
||||
},
|
||||
},
|
||||
deletionProtection: true,
|
||||
});
|
||||
|
||||
const user = new gcp.sql.User('documenso-db-user', {
|
||||
name: 'documenso',
|
||||
instance: database.name,
|
||||
password: databasePassword,
|
||||
});
|
||||
|
||||
// Build and deploy the containerized application using Cloud Run
|
||||
const appService = new gcp.cloudrun.Service('documenso-app', {
|
||||
name: 'documenso-app',
|
||||
location: serviceLocation,
|
||||
template: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'run.googleapis.com/cloudsql-instances': database.connectionName,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
image: 'documenso/documenso:latest',
|
||||
resources: {
|
||||
limits: {
|
||||
memory: '4Gi',
|
||||
cpu: '4',
|
||||
},
|
||||
},
|
||||
ports: [{ containerPort: 3000 }],
|
||||
envs: [
|
||||
{
|
||||
name: 'NEXTAUTH_URL',
|
||||
value: appUrl,
|
||||
},
|
||||
{
|
||||
name: 'NEXTAUTH_SECRET',
|
||||
value: nextAuthSecret,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_ENCRYPTION_KEY',
|
||||
value: encryptionKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY',
|
||||
value: encryptionSecondaryKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_GOOGLE_CLIENT_ID',
|
||||
value: googleClientId,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_GOOGLE_CLIENT_SECRET',
|
||||
value: googleClientSecret,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_WELL_KNOWN',
|
||||
value: oidcWellKnown,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_CLIENT_ID',
|
||||
value: oidcClientId,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_CLIENT_SECRET',
|
||||
value: oidcClientSecret,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_PROVIDER_LABEL',
|
||||
value: oidcProviderLabel,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_ALLOW_SIGNUP',
|
||||
value: oidcAllowSignup,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_OIDC_SKIP_VERIFY',
|
||||
value: oidcSkipVerify,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_WEBAPP_URL',
|
||||
value: appUrl,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_INTERNAL_WEBAPP_URL',
|
||||
value: internalWebappUrl,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_DATABASE_URL',
|
||||
value: pulumi.interpolate`postgres://${user.name}:${user.password}@localhost:5432/documenso?host=/cloudsql/${database.connectionName}/`,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_DIRECT_DATABASE_URL',
|
||||
value: pulumi.interpolate`postgres://${user.name}:${user.password}@localhost:5432/documenso?host=/cloudsql/${database.connectionName}/`,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SIGNING_TRANSPORT',
|
||||
value: 'gcloud-hsm',
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH',
|
||||
value: hsmKey.id,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_UPLOAD_TRANSPORT',
|
||||
value: 's3',
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_ENDPOINT',
|
||||
value: 'https://storage.googleapis.com',
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_REGION',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_BUCKET',
|
||||
value: storageBucket.name,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID',
|
||||
value: hmacKey.accessId,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY',
|
||||
value: hmacKey.secret,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_TRANSPORT',
|
||||
value: smtpTransport,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_HOST',
|
||||
value: smtpHost,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_PORT',
|
||||
value: smtpPort,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_USERNAME',
|
||||
value: smtpUsername,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_PASSWORD',
|
||||
value: smtpPassword,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_APIKEY_USER',
|
||||
value: smtpApikeyUser,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_APIKEY',
|
||||
value: smtpApikey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_SECURE',
|
||||
value: smtpSecure,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS',
|
||||
value: smtpUnsafeIgnoreTls,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_FROM_NAME',
|
||||
value: smtpFromName,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_SMTP_FROM_ADDRESS',
|
||||
value: smtpFromAddress,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_RESEND_API_KEY',
|
||||
value: resendApiKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT',
|
||||
value: documentSizeUploadLimit,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_STRIPE_API_KEY',
|
||||
value: stripeApiKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET',
|
||||
value: stripeWebhookSecret,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID',
|
||||
value: stripeEnterprisePlanMonthlyPriceId,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_JOBS_PROVIDER',
|
||||
value: jobsProvider,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_TRIGGER_API_KEY',
|
||||
value: triggerApiKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_TRIGGER_API_URL',
|
||||
value: triggerApiUrl,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PRIVATE_INNGEST_EVENT_KEY',
|
||||
value: inngestEventKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_POSTHOG_KEY',
|
||||
value: posthogKey,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_FEATURE_BILLING_ENABLED',
|
||||
value: billingEnabled,
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_DISABLE_SIGNUP',
|
||||
value: signupDisabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timeoutSeconds: 3600,
|
||||
serviceAccountName: appServiceAccount.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Allow unauthenticated invocations
|
||||
const iam = new gcp.cloudrun.IamMember('documenso-app-invoker', {
|
||||
service: appService.name,
|
||||
location: appService.location,
|
||||
role: 'roles/run.invoker',
|
||||
member: 'allUsers',
|
||||
});
|
||||
|
||||
// Allow the Cloud Run service to use the HSM for signing
|
||||
const _cryptoKeyIAMBinding = new gcp.kms.CryptoKeyIAMBinding('cryptoKeyIAMBinding', {
|
||||
cryptoKeyId: hsmKey.id,
|
||||
role: 'roles/cloudkms.signerVerifier',
|
||||
members: [appServiceAccount.email.apply((email) => `serviceAccount:${email}`)],
|
||||
});
|
||||
|
||||
// Allow the Cloud Run service to access the GCS Bucket
|
||||
const _bucketIAMBinding = new gcp.storage.BucketIAMBinding('bucketIAMBinding', {
|
||||
bucket: storageBucket.name,
|
||||
role: 'roles/storage.objectAdmin',
|
||||
members: [appServiceAccount.email.apply((email) => `serviceAccount:${email}`)],
|
||||
});
|
||||
|
||||
export const serviceUrl = appService.statuses[0].url;
|
||||
3631
infra/gcloud/package-lock.json
generated
Normal file
3631
infra/gcloud/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
infra/gcloud/package.json
Normal file
15
infra/gcloud/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "gcloud",
|
||||
"description": "documenso on google cloud",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@pulumi/pulumi": "*",
|
||||
"@pulumi/gcp": "7.8.0",
|
||||
"@pulumi/google-native": "0.31.1",
|
||||
"@pulumi/random": "^4.16.6"
|
||||
}
|
||||
}
|
||||
21
infra/gcloud/tsconfig.json
Normal file
21
infra/gcloud/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "bin",
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"pretty": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
3802
package-lock.json
generated
3802
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.7.1-rc.3",
|
||||
"version": "1.7.2-rc.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
@ -59,22 +59,18 @@
|
||||
"name": "@documenso/root",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"infra/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@documenso/pdf-sign": "^0.1.0",
|
||||
"@lingui/core": "^4.11.3",
|
||||
"@lingui/swc-plugin": "4.0.10",
|
||||
"inngest-cli": "^0.29.1",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"next": "15.0.0-canary.165",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
"react": "^18"
|
||||
},
|
||||
"overrides": {
|
||||
"next": "15.0.0-canary.165",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919"
|
||||
"next": "14.2.6"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
|
||||
249
packages/app-tests/e2e/teams/search-documents.spec.ts
Normal file
249
packages/app-tests/e2e/teams/search-documents.spec.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||
|
||||
test('[TEAMS]: search respects team document visibility', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
const adminUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
|
||||
const managerUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MANAGER });
|
||||
const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'EVERYONE',
|
||||
title: 'Searchable Document for Everyone',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'MANAGER_AND_ABOVE',
|
||||
title: 'Searchable Document for Managers',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'ADMIN',
|
||||
title: 'Searchable Document for Admins',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const testCases = [
|
||||
{ user: adminUser, visibleDocs: 3 },
|
||||
{ user: managerUser, visibleDocs: 2 },
|
||||
{ user: memberUser, visibleDocs: 1 },
|
||||
];
|
||||
|
||||
for (const { user, visibleDocs } of testCases) {
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Searchable');
|
||||
await page.waitForURL(/search=Searchable/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', visibleDocs);
|
||||
|
||||
await apiSignout({ page });
|
||||
}
|
||||
});
|
||||
|
||||
test('[TEAMS]: search does not reveal documents from other teams', async ({ page }) => {
|
||||
const { team: teamA, teamMember2: teamAMember } = await seedTeamDocuments();
|
||||
const { team: teamB } = await seedTeamDocuments();
|
||||
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: teamA.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: teamA.id,
|
||||
visibility: 'EVERYONE',
|
||||
title: 'Unique Team A Document',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: teamB.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: teamB.id,
|
||||
visibility: 'EVERYONE',
|
||||
title: 'Unique Team B Document',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamAMember.email,
|
||||
redirectPath: `/t/${teamA.url}/documents`,
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||
await page.waitForURL(/search=Unique/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(page.getByRole('link', { name: 'Unique Team A Document' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Unique Team B Document' })).not.toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[PERSONAL]: search does not reveal team documents in personal account', async ({ page }) => {
|
||||
const { team, teamMember2 } = await seedTeamDocuments();
|
||||
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: teamMember2,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: null,
|
||||
title: 'Personal Unique Document',
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'EVERYONE',
|
||||
title: 'Team Unique Document',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamMember2.email,
|
||||
redirectPath: '/documents',
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||
await page.waitForURL(/search=Unique/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(page.getByRole('link', { name: 'Personal Unique Document' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Team Unique Document' })).not.toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: search respects recipient visibility regardless of team visibility', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam();
|
||||
const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [memberUser],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'ADMIN',
|
||||
title: 'Admin Document with Member Recipient',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: memberUser.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Admin Document');
|
||||
await page.waitForURL(/search=Admin(%20|\+|\s)Document/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Admin Document with Member Recipient' }),
|
||||
).toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: search by recipient name respects visibility', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
const adminUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
|
||||
const memberUser = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
name: 'Team Member',
|
||||
});
|
||||
|
||||
const uniqueRecipient = await seedUser();
|
||||
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [uniqueRecipient],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'ADMIN',
|
||||
title: 'Admin Document for Unique Recipient',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Admin should see the document when searching by recipient name
|
||||
await apiSignin({
|
||||
page,
|
||||
email: adminUser.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
|
||||
).toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
|
||||
// Member should not see the document when searching by recipient name
|
||||
await apiSignin({
|
||||
page,
|
||||
email: memberUser.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 0);
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
|
||||
).not.toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
});
|
||||
@ -17,9 +17,9 @@
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { Field } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
export const useFieldPageCoords = (field: Field) => {
|
||||
const [coords, setCoords] = useState({
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export enum RecipientStatusType {
|
||||
COMPLETED = 'completed',
|
||||
OPENED = 'opened',
|
||||
WAITING = 'waiting',
|
||||
UNSIGNED = 'unsigned',
|
||||
}
|
||||
|
||||
export const getRecipientType = (recipient: Recipient) => {
|
||||
if (
|
||||
recipient.role === RecipientRole.CC ||
|
||||
(recipient.sendStatus === SendStatus.SENT && recipient.signingStatus === SigningStatus.SIGNED)
|
||||
) {
|
||||
return 'completed';
|
||||
return RecipientStatusType.COMPLETED;
|
||||
}
|
||||
|
||||
if (
|
||||
@ -14,12 +21,33 @@ export const getRecipientType = (recipient: Recipient) => {
|
||||
recipient.readStatus === ReadStatus.OPENED &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED
|
||||
) {
|
||||
return 'opened';
|
||||
return RecipientStatusType.OPENED;
|
||||
}
|
||||
|
||||
if (recipient.sendStatus === 'SENT' && recipient.signingStatus === 'NOT_SIGNED') {
|
||||
return 'waiting';
|
||||
if (
|
||||
recipient.sendStatus === SendStatus.SENT &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED
|
||||
) {
|
||||
return RecipientStatusType.WAITING;
|
||||
}
|
||||
|
||||
return 'unsigned';
|
||||
return RecipientStatusType.UNSIGNED;
|
||||
};
|
||||
|
||||
export const getExtraRecipientsType = (extraRecipients: Recipient[]) => {
|
||||
const types = extraRecipients.map((r) => getRecipientType(r));
|
||||
|
||||
if (types.includes(RecipientStatusType.UNSIGNED)) {
|
||||
return RecipientStatusType.UNSIGNED;
|
||||
}
|
||||
|
||||
if (types.includes(RecipientStatusType.OPENED)) {
|
||||
return RecipientStatusType.OPENED;
|
||||
}
|
||||
|
||||
if (types.includes(RecipientStatusType.WAITING)) {
|
||||
return RecipientStatusType.WAITING;
|
||||
}
|
||||
|
||||
return RecipientStatusType.COMPLETED;
|
||||
};
|
||||
|
||||
@ -41,13 +41,13 @@
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.11.3",
|
||||
"playwright": "1.43.0",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"remeda": "^2.12.1",
|
||||
"sharp": "0.32.6",
|
||||
"stripe": "^12.7.0",
|
||||
|
||||
@ -25,6 +25,7 @@ export type FindDocumentsOptions = {
|
||||
};
|
||||
period?: PeriodSelectorValue;
|
||||
senderIds?: number[];
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export const findDocuments = async ({
|
||||
@ -37,6 +38,7 @@ export const findDocuments = async ({
|
||||
orderBy,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
}: FindDocumentsOptions) => {
|
||||
const { user, team } = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findFirstOrThrow({
|
||||
@ -92,6 +94,14 @@ export const findDocuments = async ({
|
||||
})
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
const visibilityFilters = [
|
||||
match(teamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
@ -188,7 +198,7 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }],
|
||||
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }, { ...searchFilter }],
|
||||
};
|
||||
|
||||
if (period) {
|
||||
|
||||
@ -15,9 +15,10 @@ export type GetStatsInput = {
|
||||
user: User;
|
||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||
period?: PeriodSelectorValue;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
||||
export const getStats = async ({ user, period, search, ...options }: GetStatsInput) => {
|
||||
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
|
||||
if (period) {
|
||||
@ -31,8 +32,14 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
||||
}
|
||||
|
||||
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
||||
? getTeamCounts({ ...options.team, createdAt, currentUserEmail: user.email, userId: user.id })
|
||||
: getCounts({ user, createdAt }));
|
||||
? getTeamCounts({
|
||||
...options.team,
|
||||
createdAt,
|
||||
currentUserEmail: user.email,
|
||||
userId: user.id,
|
||||
search,
|
||||
})
|
||||
: getCounts({ user, createdAt, search }));
|
||||
|
||||
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||
@ -72,9 +79,18 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
||||
type GetCountsOption = {
|
||||
user: User;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
search?: string;
|
||||
};
|
||||
|
||||
const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
||||
const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
// Owner counts.
|
||||
prisma.document.groupBy({
|
||||
@ -87,6 +103,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
||||
createdAt,
|
||||
teamId: null,
|
||||
deletedAt: null,
|
||||
AND: [searchFilter],
|
||||
},
|
||||
}),
|
||||
// Not signed counts.
|
||||
@ -105,6 +122,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
||||
},
|
||||
},
|
||||
createdAt,
|
||||
AND: [searchFilter],
|
||||
},
|
||||
}),
|
||||
// Has signed counts.
|
||||
@ -142,6 +160,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
AND: [searchFilter],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@ -155,6 +174,7 @@ type GetTeamCountsOption = {
|
||||
userId: number;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
@ -169,6 +189,14 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
let ownerCountsWhereInput: Prisma.DocumentWhereInput = {
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
@ -220,6 +248,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
...searchFilter,
|
||||
};
|
||||
|
||||
if (teamEmail) {
|
||||
|
||||
@ -4,5 +4,8 @@ import { cookies } from 'next/headers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export const switchI18NLanguage = async (lang: string) => {
|
||||
cookies().set('language', lang);
|
||||
// Two year expiry.
|
||||
const maxAge = 60 * 60 * 24 * 365 * 2;
|
||||
|
||||
cookies().set('language', lang, { maxAge });
|
||||
};
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentSigningOrder, Field } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
type Field,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@ -153,7 +154,7 @@ export const createDocumentFromTemplate = async ({
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
source: DocumentSource.TEMPLATE,
|
||||
externalId,
|
||||
externalId: externalId || template.externalId,
|
||||
templateId: template.id,
|
||||
userId,
|
||||
teamId: template.teamId,
|
||||
@ -172,7 +173,9 @@ export const createDocumentFromTemplate = async ({
|
||||
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||
signingOrder:
|
||||
override?.signingOrder || template.templateMeta?.signingOrder || undefined,
|
||||
override?.signingOrder ||
|
||||
template.templateMeta?.signingOrder ||
|
||||
DocumentSigningOrder.PARALLEL,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
|
||||
@ -100,7 +100,7 @@ export const updateTemplateSettings = async ({
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
externalId: data.externalId,
|
||||
type: data.type,
|
||||
publicDescription: data.publicDescription,
|
||||
publicTitle: data.publicTitle,
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-16 16:03\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -115,7 +115,7 @@ msgstr "Admin"
|
||||
msgid "Advanced Options"
|
||||
msgstr "Erweiterte Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:527
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:565
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Erweiterte Einstellungen"
|
||||
@ -159,7 +159,7 @@ msgstr "Unterzeichner kann nicht entfernt werden"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:221
|
||||
#~ msgid "Cannot update signer because they have already signed a field"
|
||||
#~ msgstr ""
|
||||
#~ msgstr "Cannot update signer because they have already signed a field"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:17
|
||||
msgid "Cc"
|
||||
@ -178,7 +178,7 @@ msgstr "CC'd"
|
||||
msgid "Character Limit"
|
||||
msgstr "Zeichenbeschränkung"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:950
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:993
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
@ -207,7 +207,7 @@ msgstr "Schließen"
|
||||
msgid "Configure Direct Recipient"
|
||||
msgstr "Direkten Empfänger konfigurieren"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:528
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:566
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Konfigurieren Sie das Feld {0}"
|
||||
@ -224,7 +224,7 @@ msgstr "In die Zwischenablage kopiert"
|
||||
msgid "Custom Text"
|
||||
msgstr "Benutzerdefinierter Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:846
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:889
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
@ -256,7 +256,7 @@ msgstr "Herunterladen"
|
||||
msgid "Drag & drop your PDF here."
|
||||
msgstr "Ziehen Sie Ihr PDF hierher."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:976
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1019
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
@ -265,7 +265,7 @@ msgstr "Dropdown"
|
||||
msgid "Dropdown options"
|
||||
msgstr "Dropdown-Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:794
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:837
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:500
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:632
|
||||
@ -278,6 +278,10 @@ msgstr "E-Mail"
|
||||
msgid "Email Options"
|
||||
msgstr "E-Mail-Optionen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1082
|
||||
msgid "Empty field"
|
||||
msgstr "Leeres Feld"
|
||||
|
||||
#: packages/lib/constants/template.ts:8
|
||||
msgid "Enable Direct Link Signing"
|
||||
msgstr "Direktlink-Signierung aktivieren"
|
||||
@ -384,7 +388,7 @@ msgstr "Nachricht <0>(Optional)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:820
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:863
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:535
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:541
|
||||
@ -406,12 +410,12 @@ msgstr "Muss unterzeichnen"
|
||||
msgid "Needs to view"
|
||||
msgstr "Muss sehen"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:631
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:674
|
||||
#: 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:647
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:690
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "Keine Empfänger mit dieser Rolle"
|
||||
@ -436,7 +440,7 @@ msgstr "Kein Unterschriftsfeld gefunden"
|
||||
msgid "No value found."
|
||||
msgstr "Kein Wert gefunden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:898
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:941
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Nummer"
|
||||
@ -471,7 +475,7 @@ msgstr "Wählen Sie eine Zahl"
|
||||
msgid "Placeholder"
|
||||
msgstr "Platzhalter"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:924
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:967
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
@ -507,7 +511,7 @@ msgstr "Rot"
|
||||
msgid "Redirect URL"
|
||||
msgstr "Weiterleitungs-URL"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1027
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1069
|
||||
msgid "Remove"
|
||||
msgstr "Entfernen"
|
||||
|
||||
@ -578,7 +582,7 @@ msgstr "Erweiterte Einstellungen anzeigen"
|
||||
msgid "Sign"
|
||||
msgstr "Unterschreiben"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:742
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:785
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
@ -626,7 +630,7 @@ msgstr "Einreichen"
|
||||
msgid "Template title"
|
||||
msgstr "Vorlagentitel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:872
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:915
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
@ -687,7 +691,7 @@ msgstr "Der Name des Unterzeichners"
|
||||
msgid "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
msgstr "Dies kann überschrieben werden, indem die Authentifizierungsanforderungen im nächsten Schritt direkt für jeden Empfänger festgelegt werden."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:703
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:746
|
||||
msgid "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
msgstr "Dieses Dokument wurde bereits an diesen Empfänger gesendet. Sie können diesen Empfänger nicht mehr bearbeiten."
|
||||
|
||||
@ -699,17 +703,17 @@ msgstr "Dieses Dokument ist durch ein Passwort geschützt. Bitte geben Sie das P
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "Dieses Feld kann nicht geändert oder gelöscht werden. Wenn Sie den direkten Link dieser Vorlage teilen oder zu Ihrem öffentlichen Profil hinzufügen, kann jeder, der darauf zugreift, seinen Namen und seine E-Mail-Adresse eingeben und die ihm zugewiesenen Felder ausfüllen."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1007
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1050
|
||||
msgid "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
msgstr ""
|
||||
msgstr "Dieser Empfänger kann nicht mehr bearbeitet werden, da er ein Feld unterschrieben oder das Dokument abgeschlossen hat."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:195
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:165
|
||||
#~ msgid "This signer has already received the document."
|
||||
#~ msgstr "Dieser Unterzeichner hat das Dokument bereits erhalten."
|
||||
#~ msgstr "This signer has already received the document."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:194
|
||||
msgid "This signer has already signed the document."
|
||||
msgstr ""
|
||||
msgstr "Dieser Unterzeichner hat das Dokument bereits unterschrieben."
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:48
|
||||
msgid "This will override any global settings."
|
||||
@ -724,7 +728,7 @@ msgstr "Zeitzone"
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:990
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1033
|
||||
#: 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."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-16 14:04\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -223,6 +223,7 @@ msgstr "Aus dem Blog"
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:9
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:17
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:33
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:41
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:49
|
||||
@ -385,8 +386,8 @@ msgid "Our self-hosted option is great for small teams and individuals who need
|
||||
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."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
msgid "Part-Time"
|
||||
msgstr "Teilzeit"
|
||||
#~ msgid "Part-Time"
|
||||
#~ msgstr "Part-Time"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
|
||||
msgid "Premium Profile Name"
|
||||
@ -614,6 +615,6 @@ msgstr "Ja! Documenso wird unter der GNU AGPL V3 Open-Source-Lizenz angeboten. D
|
||||
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."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:265
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
|
||||
msgid "Your browser does not support the video tag."
|
||||
msgstr "Ihr Browser unterstützt das Video-Tag nicht."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-16 16:03\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -789,7 +789,7 @@ msgstr "Unterzeichnung abschließen"
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Betrachten abschließen"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:58
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:62
|
||||
#: apps/web/src/components/formatter/document-status.tsx:28
|
||||
msgid "Completed"
|
||||
msgstr "Abgeschlossen"
|
||||
@ -1316,7 +1316,7 @@ msgstr "Dokument wird dauerhaft gelöscht"
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||
#: apps/web/src/app/not-found.tsx:21
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||
@ -2143,7 +2143,7 @@ msgstr "Sobald Sie den QR-Code gescannt oder den Code manuell eingegeben haben,
|
||||
msgid "Oops! Something went wrong."
|
||||
msgstr "Hoppla! Etwas ist schief gelaufen."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:97
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:101
|
||||
msgid "Opened"
|
||||
msgstr "Geöffnet"
|
||||
|
||||
@ -3636,7 +3636,7 @@ msgstr "Unable to sign in"
|
||||
msgid "Unauthorized"
|
||||
msgstr "Unauthorized"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:112
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:116
|
||||
msgid "Uncompleted"
|
||||
msgstr "Uncompleted"
|
||||
|
||||
@ -3848,7 +3848,7 @@ msgstr "Teams ansehen"
|
||||
msgid "Viewed"
|
||||
msgstr "Angesehen"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:82
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:86
|
||||
msgid "Waiting"
|
||||
msgstr "Warten"
|
||||
|
||||
|
||||
@ -110,7 +110,7 @@ msgstr "Admin"
|
||||
msgid "Advanced Options"
|
||||
msgstr "Advanced Options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:527
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:565
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Advanced settings"
|
||||
@ -173,7 +173,7 @@ msgstr "CC'd"
|
||||
msgid "Character Limit"
|
||||
msgstr "Character Limit"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:950
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:993
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Checkbox"
|
||||
@ -202,7 +202,7 @@ msgstr "Close"
|
||||
msgid "Configure Direct Recipient"
|
||||
msgstr "Configure Direct Recipient"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:528
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:566
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Configure the {0} field"
|
||||
@ -219,7 +219,7 @@ msgstr "Copied to clipboard"
|
||||
msgid "Custom Text"
|
||||
msgstr "Custom Text"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:846
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:889
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
@ -251,7 +251,7 @@ msgstr "Download"
|
||||
msgid "Drag & drop your PDF here."
|
||||
msgstr "Drag & drop your PDF here."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:976
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1019
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Dropdown"
|
||||
@ -260,7 +260,7 @@ msgstr "Dropdown"
|
||||
msgid "Dropdown options"
|
||||
msgstr "Dropdown options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:794
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:837
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:500
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:632
|
||||
@ -273,6 +273,10 @@ msgstr "Email"
|
||||
msgid "Email Options"
|
||||
msgstr "Email Options"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1082
|
||||
msgid "Empty field"
|
||||
msgstr "Empty field"
|
||||
|
||||
#: packages/lib/constants/template.ts:8
|
||||
msgid "Enable Direct Link Signing"
|
||||
msgstr "Enable Direct Link Signing"
|
||||
@ -379,7 +383,7 @@ msgstr "Message <0>(Optional)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:820
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:863
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:535
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:541
|
||||
@ -401,12 +405,12 @@ msgstr "Needs to sign"
|
||||
msgid "Needs to view"
|
||||
msgstr "Needs to view"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:631
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:674
|
||||
#: 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:647
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:690
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "No recipients with this role"
|
||||
@ -431,7 +435,7 @@ msgstr "No signature field found"
|
||||
msgid "No value found."
|
||||
msgstr "No value found."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:898
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:941
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Number"
|
||||
@ -466,7 +470,7 @@ msgstr "Pick a number"
|
||||
msgid "Placeholder"
|
||||
msgstr "Placeholder"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:924
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:967
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
@ -502,7 +506,7 @@ msgstr "Red"
|
||||
msgid "Redirect URL"
|
||||
msgstr "Redirect URL"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1027
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1069
|
||||
msgid "Remove"
|
||||
msgstr "Remove"
|
||||
|
||||
@ -573,7 +577,7 @@ msgstr "Show advanced settings"
|
||||
msgid "Sign"
|
||||
msgstr "Sign"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:742
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:785
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
@ -621,7 +625,7 @@ msgstr "Submit"
|
||||
msgid "Template title"
|
||||
msgstr "Template title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:872
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:915
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
@ -682,7 +686,7 @@ msgstr "The signer's name"
|
||||
msgid "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
msgstr "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:703
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:746
|
||||
msgid "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
msgstr "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
|
||||
@ -694,7 +698,7 @@ msgstr "This document is password protected. Please enter the password to view t
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1007
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1050
|
||||
msgid "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
msgstr "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
|
||||
@ -719,7 +723,7 @@ msgstr "Time Zone"
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:990
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1033
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:828
|
||||
msgid "To proceed further, please set at least one value for the {0} field."
|
||||
msgstr "To proceed further, please set at least one value for the {0} field."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -218,6 +218,7 @@ msgstr "From the blog"
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:9
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:17
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:33
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:41
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:49
|
||||
@ -380,8 +381,8 @@ msgid "Our self-hosted option is great for small teams and individuals who need
|
||||
msgstr "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."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
msgid "Part-Time"
|
||||
msgstr "Part-Time"
|
||||
#~ msgid "Part-Time"
|
||||
#~ msgstr "Part-Time"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
|
||||
msgid "Premium Profile Name"
|
||||
@ -609,6 +610,6 @@ msgstr "Yes! Documenso is offered under the GNU AGPL V3 open source license. Thi
|
||||
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 "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."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:265
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
|
||||
msgid "Your browser does not support the video tag."
|
||||
msgstr "Your browser does not support the video tag."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -784,7 +784,7 @@ msgstr "Complete Signing"
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Complete Viewing"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:58
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:62
|
||||
#: apps/web/src/components/formatter/document-status.tsx:28
|
||||
msgid "Completed"
|
||||
msgstr "Completed"
|
||||
@ -1311,7 +1311,7 @@ msgstr "Document will be permanently deleted"
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||
#: apps/web/src/app/not-found.tsx:21
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||
@ -2138,7 +2138,7 @@ msgstr "Once you have scanned the QR code or entered the code manually, enter th
|
||||
msgid "Oops! Something went wrong."
|
||||
msgstr "Oops! Something went wrong."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:97
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:101
|
||||
msgid "Opened"
|
||||
msgstr "Opened"
|
||||
|
||||
@ -3631,7 +3631,7 @@ msgstr "Unable to sign in"
|
||||
msgid "Unauthorized"
|
||||
msgstr "Unauthorized"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:112
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:116
|
||||
msgid "Uncompleted"
|
||||
msgstr "Uncompleted"
|
||||
|
||||
@ -3843,7 +3843,7 @@ msgstr "View teams"
|
||||
msgid "Viewed"
|
||||
msgstr "Viewed"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:82
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:86
|
||||
msgid "Waiting"
|
||||
msgstr "Waiting"
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-19 09:18\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@ -115,7 +115,7 @@ msgstr "Administrateur"
|
||||
msgid "Advanced Options"
|
||||
msgstr "Options avancées"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:527
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:565
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:402
|
||||
msgid "Advanced settings"
|
||||
msgstr "Paramètres avancés"
|
||||
@ -157,6 +157,10 @@ msgstr "Annuler"
|
||||
msgid "Cannot remove signer"
|
||||
msgstr "Impossible de retirer le signataire"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:221
|
||||
#~ msgid "Cannot update signer because they have already signed a field"
|
||||
#~ msgstr "Cannot update signer because they have already signed a field"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:17
|
||||
msgid "Cc"
|
||||
msgstr "Cc"
|
||||
@ -174,7 +178,7 @@ msgstr "CC'd"
|
||||
msgid "Character Limit"
|
||||
msgstr "Limite de caractères"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:950
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:993
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:788
|
||||
msgid "Checkbox"
|
||||
msgstr "Case à cocher"
|
||||
@ -203,7 +207,7 @@ msgstr "Fermer"
|
||||
msgid "Configure Direct Recipient"
|
||||
msgstr "Configurer le destinataire direct"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:528
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:566
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:403
|
||||
msgid "Configure the {0} field"
|
||||
msgstr "Configurer le champ {0}"
|
||||
@ -220,7 +224,7 @@ msgstr "Copié dans le presse-papiers"
|
||||
msgid "Custom Text"
|
||||
msgstr "Texte personnalisé"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:846
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:889
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:684
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
@ -252,7 +256,7 @@ msgstr "Télécharger"
|
||||
msgid "Drag & drop your PDF here."
|
||||
msgstr "Faites glisser et déposez votre PDF ici."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:976
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1019
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:814
|
||||
msgid "Dropdown"
|
||||
msgstr "Liste déroulante"
|
||||
@ -261,7 +265,7 @@ msgstr "Liste déroulante"
|
||||
msgid "Dropdown options"
|
||||
msgstr "Options de liste déroulante"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:794
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:837
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:272
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:500
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:632
|
||||
@ -274,6 +278,10 @@ msgstr "Email"
|
||||
msgid "Email Options"
|
||||
msgstr "Options d'email"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1082
|
||||
msgid "Empty field"
|
||||
msgstr "Champ vide"
|
||||
|
||||
#: packages/lib/constants/template.ts:8
|
||||
msgid "Enable Direct Link Signing"
|
||||
msgstr "Activer la signature de lien direct"
|
||||
@ -380,7 +388,7 @@ msgstr "Message <0>(Optionnel)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:820
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:863
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:298
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:535
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:541
|
||||
@ -402,12 +410,12 @@ msgstr "Nécessite une signature"
|
||||
msgid "Needs to view"
|
||||
msgstr "Nécessite une visualisation"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:631
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:674
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:497
|
||||
msgid "No recipient matching this description was found."
|
||||
msgstr "Aucun destinataire correspondant à cette description n'a été trouvé."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:647
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:690
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:513
|
||||
msgid "No recipients with this role"
|
||||
msgstr "Aucun destinataire avec ce rôle"
|
||||
@ -432,7 +440,7 @@ msgstr "Aucun champ de signature trouvé"
|
||||
msgid "No value found."
|
||||
msgstr "Aucune valeur trouvée."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:898
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:941
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:736
|
||||
msgid "Number"
|
||||
msgstr "Numéro"
|
||||
@ -467,7 +475,7 @@ msgstr "Choisissez un numéro"
|
||||
msgid "Placeholder"
|
||||
msgstr "Espace réservé"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:924
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:967
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:762
|
||||
msgid "Radio"
|
||||
msgstr "Radio"
|
||||
@ -503,7 +511,7 @@ msgstr "Rouge"
|
||||
msgid "Redirect URL"
|
||||
msgstr "URL de redirection"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1027
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1069
|
||||
msgid "Remove"
|
||||
msgstr "Retirer"
|
||||
|
||||
@ -574,7 +582,7 @@ msgstr "Afficher les paramètres avancés"
|
||||
msgid "Sign"
|
||||
msgstr "Signer"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:742
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:785
|
||||
#: packages/ui/primitives/document-flow/add-signature.tsx:323
|
||||
#: packages/ui/primitives/document-flow/field-icon.tsx:52
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:580
|
||||
@ -622,7 +630,7 @@ msgstr "Soumettre"
|
||||
msgid "Template title"
|
||||
msgstr "Titre du modèle"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:872
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:915
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx:710
|
||||
msgid "Text"
|
||||
msgstr "Texte"
|
||||
@ -683,7 +691,7 @@ msgstr "Le nom du signataire"
|
||||
msgid "This can be overriden by setting the authentication requirements directly on each recipient in the next step."
|
||||
msgstr "Cela peut être remplacé par le paramétrage direct des exigences d'authentification pour chaque destinataire à l'étape suivante."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:703
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:746
|
||||
msgid "This document has already been sent to this recipient. You can no longer edit this recipient."
|
||||
msgstr "Ce document a déjà été envoyé à ce destinataire. Vous ne pouvez plus modifier ce destinataire."
|
||||
|
||||
@ -695,17 +703,17 @@ msgstr "Ce document est protégé par mot de passe. Veuillez entrer le mot de pa
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "Ce champ ne peut pas être modifié ou supprimé. Lorsque vous partagez le lien direct de ce modèle ou l'ajoutez à votre profil public, toute personne qui y accède peut saisir son nom et son email, et remplir les champs qui lui sont attribués."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1007
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1050
|
||||
msgid "This recipient can no longer be modified as they have signed a field, or completed the document."
|
||||
msgstr ""
|
||||
msgstr "Ce destinataire ne peut plus être modifié car il a signé un champ ou complété le document."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:195
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:165
|
||||
#~ msgid "This signer has already received the document."
|
||||
#~ msgstr "Ce signataire a déjà reçu le document."
|
||||
#~ msgstr "This signer has already received the document."
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx:194
|
||||
msgid "This signer has already signed the document."
|
||||
msgstr ""
|
||||
msgstr "Ce signataire a déjà signé le document."
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:48
|
||||
msgid "This will override any global settings."
|
||||
@ -720,7 +728,7 @@ msgstr "Fuseau horaire"
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:990
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx:1033
|
||||
#: 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 "Pour continuer, veuillez définir au moins une valeur pour le champ {0}."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-19 09:18\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@ -223,6 +223,7 @@ msgstr "Du blog"
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:9
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:17
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:33
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:41
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:49
|
||||
@ -385,8 +386,8 @@ msgid "Our self-hosted option is great for small teams and individuals who need
|
||||
msgstr "Notre option auto-hébergée est idéale pour les petites équipes et les individus qui ont besoin d'une solution simple. Vous pouvez utiliser notre configuration basée sur Docker pour commencer en quelques minutes. Prenez le contrôle avec une personnalisation complète et une propriété des données."
|
||||
|
||||
#: apps/marketing/src/app/(marketing)/open/data.ts:25
|
||||
msgid "Part-Time"
|
||||
msgstr "Temps partiel"
|
||||
#~ msgid "Part-Time"
|
||||
#~ msgstr "Part-Time"
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
|
||||
msgid "Premium Profile Name"
|
||||
@ -614,6 +615,6 @@ msgstr "Oui ! Documenso est proposé sous la licence open source GNU AGPL V3. Ce
|
||||
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 "Vous pouvez auto-héberger Documenso gratuitement ou utiliser notre version hébergée prête à l'emploi. La version hébergée est accompagnée d'une assistance supplémentaire, d'une montée en charge sans effort et d'autres avantages. Les premiers adoptants auront accès à toutes les fonctionnalités que nous construirons cette année, sans coût supplémentaire ! Pour toujours ! Oui, cela inclut plusieurs utilisateurs par compte à l'avenir. Si vous souhaitez Documenso pour votre entreprise, nous serons heureux de discuter de vos besoins."
|
||||
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:265
|
||||
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
|
||||
msgid "Your browser does not support the video tag."
|
||||
msgstr "Votre navigateur ne prend pas en charge la balise vidéo."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-19 09:18\n"
|
||||
"PO-Revision-Date: 2024-10-08 12:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@ -789,7 +789,7 @@ msgstr "Compléter la signature"
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Compléter la visualisation"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:58
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:62
|
||||
#: apps/web/src/components/formatter/document-status.tsx:28
|
||||
msgid "Completed"
|
||||
msgstr "Complété"
|
||||
@ -1316,7 +1316,7 @@ msgstr "Le document sera supprimé de manière permanente"
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||
#: apps/web/src/app/not-found.tsx:21
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||
@ -2143,7 +2143,7 @@ msgstr "Une fois que vous avez scanné le code QR ou saisi le code manuellement,
|
||||
msgid "Oops! Something went wrong."
|
||||
msgstr "Oups ! Quelque chose a mal tourné."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:97
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:101
|
||||
msgid "Opened"
|
||||
msgstr "Ouvert"
|
||||
|
||||
@ -3636,7 +3636,7 @@ msgstr "Impossible de se connecter"
|
||||
msgid "Unauthorized"
|
||||
msgstr "Non autorisé"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:112
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:116
|
||||
msgid "Uncompleted"
|
||||
msgstr "Non complet"
|
||||
|
||||
@ -3848,7 +3848,7 @@ msgstr "Voir les équipes"
|
||||
msgid "Viewed"
|
||||
msgstr "Vu"
|
||||
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:82
|
||||
#: apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx:86
|
||||
msgid "Waiting"
|
||||
msgstr "En attente"
|
||||
|
||||
|
||||
@ -362,7 +362,7 @@ export const formatDocumentAuditLogAction = (auditLog: TDocumentAuditLog, userId
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
||||
anonymous: `Email ${data.isResending ? 'resent' : 'sent'}`,
|
||||
identified: `${data.isResending ? 'resent' : 'sent'} an email`,
|
||||
identified: `${data.isResending ? 'resent' : 'sent'} an email to ${data.recipientEmail}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => {
|
||||
// Clear the prefix since this should be considered an 'anonymous' event.
|
||||
|
||||
@ -39,3 +39,27 @@ export const validateFieldsInserted = (fields: Field[]): boolean => {
|
||||
|
||||
return uninsertedFields.length === 0;
|
||||
};
|
||||
|
||||
export const validateFieldsUninserted = (): boolean => {
|
||||
const fieldCardElements = document.getElementsByClassName('react-draggable');
|
||||
|
||||
const errorElements: HTMLElement[] = [];
|
||||
|
||||
Array.from(fieldCardElements).forEach((element) => {
|
||||
const innerDiv = element.querySelector('div');
|
||||
const hasError = innerDiv?.getAttribute('data-error') === 'true';
|
||||
|
||||
if (hasError) {
|
||||
errorElements.push(element as HTMLElement);
|
||||
} else {
|
||||
element.removeAttribute('data-error');
|
||||
}
|
||||
});
|
||||
|
||||
if (errorElements.length > 0) {
|
||||
errorElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return false;
|
||||
}
|
||||
|
||||
return errorElements.length === 0;
|
||||
};
|
||||
|
||||
@ -105,13 +105,15 @@ export const unseedTeam = async (teamUrl: string) => {
|
||||
type SeedTeamMemberOptions = {
|
||||
teamId: number;
|
||||
role?: TeamMemberRole;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const seedTeamMember = async ({
|
||||
teamId,
|
||||
name,
|
||||
role = TeamMemberRole.ADMIN,
|
||||
}: SeedTeamMemberOptions) => {
|
||||
const user = await seedUser();
|
||||
const user = await seedUser({ name });
|
||||
|
||||
await prisma.teamMember.create({
|
||||
data: {
|
||||
|
||||
@ -21,8 +21,13 @@ export const seedUser = async ({
|
||||
password = 'password',
|
||||
verified = true,
|
||||
}: SeedUserOptions = {}) => {
|
||||
if (!name) {
|
||||
let url = name;
|
||||
|
||||
if (name) {
|
||||
url = nanoid();
|
||||
} else {
|
||||
name = nanoid();
|
||||
url = name;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
@ -35,7 +40,7 @@ export const seedUser = async ({
|
||||
email,
|
||||
password: hashSync(password),
|
||||
emailVerified: verified ? new Date() : undefined,
|
||||
url: name,
|
||||
url,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -101,7 +101,7 @@ export const templateRouter = router({
|
||||
.input(ZCreateDocumentFromTemplateMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { templateId, teamId } = input;
|
||||
const { templateId, teamId, recipients } = input;
|
||||
|
||||
const limits = await getServerLimits({ email: ctx.user.email, teamId });
|
||||
|
||||
@ -115,7 +115,7 @@ export const templateRouter = router({
|
||||
templateId,
|
||||
teamId,
|
||||
userId: ctx.user.id,
|
||||
recipients: input.recipients,
|
||||
recipients,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
|
||||
}}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0} open={!field.inserted}>
|
||||
<Tooltip delayDuration={0} open={!field.inserted || !field.fieldMeta}>
|
||||
<TooltipTrigger className="absolute inset-0 w-full"></TooltipTrigger>
|
||||
|
||||
<TooltipContent className={tooltipVariants({ color, className })} sideOffset={2}>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
"@types/luxon": "^3.3.2",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -65,12 +65,12 @@
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.2",
|
||||
"next": "15.0.0-canary.165",
|
||||
"next": "14.2.6",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react": "^18",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-dom": "19.0.0-rc-e740d4b1-20240919",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-pdf": "7.7.3",
|
||||
"react-rnd": "^10.4.1",
|
||||
|
||||
@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
Check,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
ZFieldMetaSchema,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { validateFieldsUninserted } from '@documenso/lib/utils/fields';
|
||||
import {
|
||||
canRecipientBeModified,
|
||||
canRecipientFieldsBeModified,
|
||||
@ -39,6 +41,7 @@ import {
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Alert, AlertDescription } from '../alert';
|
||||
@ -96,12 +99,6 @@ export type AddFieldsFormProps = {
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
/*
|
||||
I hate this, but due to TailwindCSS JIT, I couldnn't find a better way to do this for now.
|
||||
|
||||
TODO: Try to find a better way to do this.
|
||||
*/
|
||||
|
||||
export const AddFieldsFormPartial = ({
|
||||
documentFlow,
|
||||
hideRecipients = false,
|
||||
@ -195,6 +192,7 @@ export const AddFieldsFormPartial = ({
|
||||
const selectedSignerStyles = useSignerColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
||||
fields
|
||||
@ -228,6 +226,38 @@ export const AddFieldsFormPartial = ({
|
||||
const hasErrors =
|
||||
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
|
||||
|
||||
const fieldsWithError = useMemo(() => {
|
||||
const fields = localFields.filter((field) => {
|
||||
const hasError =
|
||||
((field.type === FieldType.CHECKBOX ||
|
||||
field.type === FieldType.RADIO ||
|
||||
field.type === FieldType.DROPDOWN) &&
|
||||
field.fieldMeta === undefined) ||
|
||||
(field.fieldMeta && 'values' in field.fieldMeta && field?.fieldMeta?.values?.length === 0);
|
||||
|
||||
return hasError;
|
||||
});
|
||||
|
||||
const mappedFields = fields.map((field) => ({
|
||||
id: field.nativeId ?? 0,
|
||||
secondaryId: field.formId,
|
||||
documentId: null,
|
||||
templateId: null,
|
||||
recipientId: 0,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: new Prisma.Decimal(field.pageX),
|
||||
positionY: new Prisma.Decimal(field.pageY),
|
||||
width: new Prisma.Decimal(field.pageWidth),
|
||||
height: new Prisma.Decimal(field.pageHeight),
|
||||
customText: '',
|
||||
inserted: true,
|
||||
fieldMeta: field.fieldMeta ?? null,
|
||||
}));
|
||||
|
||||
return mappedFields;
|
||||
}, [localFields]);
|
||||
|
||||
const isFieldsDisabled = useMemo(() => {
|
||||
if (!selectedSigner) {
|
||||
return true;
|
||||
@ -515,6 +545,14 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
if (!everySignerHasSignature) {
|
||||
setIsMissingSignatureDialogVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setValidateUninsertedFields(true);
|
||||
const isFieldsValid = validateFieldsUninserted();
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
} else {
|
||||
void onFormSubmit();
|
||||
}
|
||||
@ -566,6 +604,10 @@ export const AddFieldsFormPartial = ({
|
||||
{isDocumentPdfLoaded &&
|
||||
localFields.map((field, index) => {
|
||||
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
|
||||
const hasFieldError =
|
||||
emptyCheckboxFields.find((f) => f.formId === field.formId) ||
|
||||
emptyRadioFields.find((f) => f.formId === field.formId) ||
|
||||
emptySelectFields.find((f) => f.formId === field.formId);
|
||||
|
||||
return (
|
||||
<FieldItem
|
||||
@ -590,6 +632,7 @@ export const AddFieldsFormPartial = ({
|
||||
handleAdvancedSettings();
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
hasErrors={!!hasFieldError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -1018,7 +1061,6 @@ export const AddFieldsFormPartial = ({
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disableNextStep={hasErrors}
|
||||
onGoBackClick={() => {
|
||||
previousStep();
|
||||
remove();
|
||||
@ -1035,6 +1077,11 @@ export const AddFieldsFormPartial = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{validateUninsertedFields && fieldsWithError[0] && (
|
||||
<FieldToolTip key={fieldsWithError[0].id} field={fieldsWithError[0]} color="warning">
|
||||
<Trans>Empty field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -95,7 +95,7 @@ export const AddSettingsFormPartial = ({
|
||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||
DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat:
|
||||
DATE_FORMATS.find((format) => format.label === document.documentMeta?.dateFormat)
|
||||
DATE_FORMATS.find((format) => format.value === document.documentMeta?.dateFormat)
|
||||
?.value ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
redirectUrl: document.documentMeta?.redirectUrl ?? '',
|
||||
},
|
||||
|
||||
@ -44,6 +44,7 @@ export type FieldItemProps = {
|
||||
onBlur?: () => void;
|
||||
recipientIndex?: number;
|
||||
hideRecipients?: boolean;
|
||||
hasErrors?: boolean;
|
||||
};
|
||||
|
||||
export const FieldItem = ({
|
||||
@ -61,6 +62,7 @@ export const FieldItem = ({
|
||||
onAdvancedSettings,
|
||||
recipientIndex = 0,
|
||||
hideRecipients = false,
|
||||
hasErrors,
|
||||
}: FieldItemProps) => {
|
||||
const [active, setActive] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
@ -201,10 +203,15 @@ export const FieldItem = ({
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full items-center justify-center bg-white',
|
||||
!hasErrors && signerStyles.default.base,
|
||||
!hasErrors && signerStyles.default.fieldItem,
|
||||
{
|
||||
'rounded-lg border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]':
|
||||
hasErrors,
|
||||
},
|
||||
!fixedSize && '[container-type:size]',
|
||||
signerStyles.default.base,
|
||||
signerStyles.default.fieldItem,
|
||||
)}
|
||||
data-error={hasErrors ? 'true' : undefined}
|
||||
onClick={() => {
|
||||
setSettingsActive((prev) => !prev);
|
||||
onFocus?.();
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../card';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-caveat',
|
||||
});
|
||||
|
||||
export type ShowFieldItemProps = {
|
||||
field: Prisma.FieldGetPayload<null>;
|
||||
recipients: Prisma.RecipientGetPayload<null>[];
|
||||
@ -33,7 +43,8 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
|
||||
<Card className={cn('bg-background h-full w-full')}>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-2',
|
||||
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-2 text-xl',
|
||||
field.type === FieldType.SIGNATURE && fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
{FRIENDLY_FIELD_TYPE[field.type]}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
{
|
||||
"extends": "./packages/tsconfig/base.json",
|
||||
"include": ["packages/**/*", "apps/**/*"]
|
||||
}
|
||||
"include": [
|
||||
"packages/**/*",
|
||||
"apps/**/*",
|
||||
"infra/**/*"
|
||||
],
|
||||
}
|
||||
26
turbo.json
26
turbo.json
@ -2,20 +2,12 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"prebuild",
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
".next/**",
|
||||
"!.next/cache/**"
|
||||
]
|
||||
"dependsOn": ["prebuild", "^build"],
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"prebuild": {
|
||||
"cache": false,
|
||||
"dependsOn": [
|
||||
"^prebuild"
|
||||
]
|
||||
"dependsOn": ["^prebuild"]
|
||||
},
|
||||
"lint": {
|
||||
"cache": false
|
||||
@ -31,9 +23,7 @@
|
||||
"persistent": true
|
||||
},
|
||||
"start": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
@ -41,18 +31,14 @@
|
||||
"cache": false
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
},
|
||||
"translate:compile": {
|
||||
"cache": false
|
||||
}
|
||||
},
|
||||
"globalDependencies": [
|
||||
"**/.env.*local"
|
||||
],
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"globalEnv": [
|
||||
"APP_VERSION",
|
||||
"NEXT_PRIVATE_ENCRYPTION_KEY",
|
||||
|
||||
Reference in New Issue
Block a user