Compare commits

...

52 Commits

Author SHA1 Message Date
faf2bd5384 v1.7.2-rc.0 2024-10-08 21:56:44 +11:00
d40ed94b74 feat: highlight problematic fields (#1330) 2024-10-08 21:55:20 +11:00
cd3d9b701b fix: external id null for documents created from templates (#1362) 2024-10-08 21:45:16 +11:00
e40f47a73c feat: search documents by name or recipient name or recipient email (#1384) 2024-10-08 21:44:02 +11:00
64ea4a6f9f chore: add translation contribution docs (#1379) 2024-10-08 14:05:55 +11:00
18115e95d7 feat: add recipient email in activity log (#1386) 2024-10-08 14:05:12 +11:00
e736261056 fix: show the full count of documents (#1382)
![doc-count](https://github.com/user-attachments/assets/aad4fe92-e2d8-4b78-ac93-5f6ada73b03a)

A client requested it, and it makes sense showing the full count.

This is how it was before.

![CleanShot 2024-10-04 at 08 47
16@2x](https://github.com/user-attachments/assets/bd4c97a5-1805-4faa-bae7-feeb932ed614)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Updated document status tab counts to display actual numbers without
capping at 99 or using '+' symbols.

- **Bug Fixes**
- Improved clarity and accuracy of document status counts in the user
interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-04 16:07:20 +10:00
2e57da7549 chore: open page data update (#1380) 2024-10-04 13:14:54 +10:00
574454db0a chore: Go Fork Yourself blog article (#1375)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new blog post titled "Go Fork Yourself," discussing the
philosophy of open-source software and the significance of forking
within the OSS community, along with real-world examples and an
invitation for reader engagement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-03 14:25:32 +02:00
f05b670d93 fix: carousel slide change handling and video reset (#1364) 2024-10-03 10:52:43 +10:00
318149fbf3 chore: field fonts (#1350)
Before
![CleanShot 2024-09-16 at 12 25
44](https://github.com/user-attachments/assets/9ca7672d-b132-4c24-80b0-03fa13822e50)


After
![CleanShot 2024-09-16 at 12 24
07](https://github.com/user-attachments/assets/9e17b025-8064-4151-a9e2-817108b8da2a)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new font style for signature fields, enhancing visual
distinction.
	- Increased text size for signature fields to improve prominence.

- **Bug Fixes**
- Adjusted the text size for signature display on larger screens for
better visual hierarchy.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-02 17:45:16 +10:00
5f19dcf25c fix: dateformat bug (#1372)
## Description

It used the wrong property for finding the document's dateFormat in the
`DATE_FORMATS` array.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Improved date format selection in the settings form to ensure accurate
formatting based on value.
- Default timezone now automatically set to the user's local timezone
for better user experience.

- **Bug Fixes**
- Corrected initialization of the timezone field to enhance form
accuracy.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-02 17:42:15 +10:00
c99cf4b848 chore: prisma customer story on blog (#1366)
prisma customer story on blog

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a customer story blog post detailing why Prisma chose
Documenso for their signing needs, highlighting four key reasons.
- Added author information and metadata for enhanced content engagement.
- Included links to additional resources and social media for reader
interaction.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-27 18:35:50 +02:00
18ec40f6af fix: set lang cookie expiry (#1365)
## Description

Currently the language cookie is set to session, so restarting browser
will reset it.

This sets the expiry for two years.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced language preference functionality with extended cookie
lifespan for improved user experience.
  
- **Bug Fixes**
	- Resolved issues related to cookie expiration for language settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 13:10:24 +10:00
ddee8a8272 feat: allow editing pending documents (#1346)
## Description

Adds the ability for the document owner to edit recipients and their
fields after the document has been sent.

A recipient can only be updated or deleted if:
- The recipient has not inserted any fields
- Has not completed the document

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added new localization messages to clarify user actions regarding
document signing.
  - Enhanced French translations for improved user interaction.

- **Improvements**
- Updated localization strings in German and English for clearer
feedback on signer and recipient statuses.
- Improved overall structure of localization files for better
maintainability.

- **Dependency Updates**
- Upgraded `next-axiom` and `remeda` libraries to their latest versions,
potentially enhancing performance and stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Mythie <me@lucasjamessmith.me>
2024-09-20 13:58:21 +10:00
efb2bc94ab feat: add french (#1355)
## Description

Add initial French translations

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for the French language, enhancing accessibility for
French-speaking users.
- Introduced localized French messages for various application
functionalities, improving user experience.
  
- **Bug Fixes**
- Minor formatting updates in French translation files to remove
extraneous newline characters.

- **Chores**
- Updated line references in French translation files to maintain
alignment with code changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions <github-actions@documenso.com>
2024-09-19 19:37:17 +10:00
97ee69e7a0 chore: add translations (#1354)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions <github-actions@documenso.com>
2024-09-19 16:17:16 +10:00
3da344fc5f v1.7.1-rc.3 2024-09-19 13:55:35 +10:00
404ca3202f chore: update action auth 2024-09-19 13:45:39 +10:00
c043fa9c06 fix: add check for invalid locales (#1353)
## Description

Currently invalid or missing `accept-language` headers will cause issues
rendering Plural components since we do not validate them.

This adds a check to try filter out invalid locales.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced locale filtering process to ensure only valid locales are
processed.
  
- **Bug Fixes**
- Improved data integrity by preventing invalid locales from affecting
application functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-19 09:58:59 +10:00
9852e8971f v1.7.1-rc.2 2024-09-18 11:05:19 +10:00
5091112e4b fix: dont nullify externalId if not passed to update document settings 2024-09-18 11:00:48 +10:00
e76f732990 fix: completed signing page layout (#1349) 2024-09-18 10:54:00 +10:00
b7c3deb6cd chore: smaller text in signature pad (#1351) 2024-09-18 10:44:12 +10:00
08114f7b97 chore: add translations (#1327) 2024-09-18 10:43:43 +10:00
6e368cc333 chore: add document visibility section (#1352) 2024-09-18 02:41:57 +10:00
4ce4ca3f34 v1.7.1-rc.1 2024-09-17 15:26:38 +10:00
7644c0d855 feat: support smaller field bounds (#1344)
Currently this won't always display super well since
our insertion solution isn't amazing but our current
minimum bounds within the UI are a bit large and can be
smaller.

This change makes it smaller and uses container queries to
support dynamically displaying labels based on the container
size.
2024-09-17 00:29:42 +10:00
fa6453e811 feat: document visibility (#1262)
Adds the ability to set a visibility scope for documents within teams.
2024-09-17 00:14:16 +10:00
f7a20113e5 fix: fix passkeys page-breaking error (#1348) 2024-09-16 22:57:31 +10:00
3d644db286 feat: signing order (#1290)
Adds the ability to specify an optional signing order for documents.
When specified a document will be considered sequential with recipients
only being allowed to sign in the order that they were specified in.
2024-09-16 22:36:45 +10:00
357bdd374f feat: add language switcher (#1337)
## Description

Web changes:

- Enabled i18n for web
- Add option to change language in command menu
- Add option to change language in menu-switcher

Web and marketing changes:

- Stop setting 'en' preference into cookie if the user's language is not
supported
- Dropped middleware changes
- Rotated cookie from 'i18n' to 'language'

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a language switcher in the footer for improved language
selection.
	- Added dynamic language change functionality in the command menu.
- Implemented a dropdown menu item for quick access to the language
switcher.

- **Bug Fixes**
- Resolved issues related to language change notifications and state
management.

- **Translations**
- Added new translation entries for improved language support, including
"Search languages..." in English and German.
	- Updated existing translations to enhance clarity and accuracy.

- **Chores**
	- Simplified internationalization handling in middleware.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: github-actions <github-actions@documenso.com>
2024-09-11 13:22:43 +10:00
7b06b68572 v1.7.1-rc.0 2024-09-10 23:15:02 +10:00
9ee89346b1 fix: template with empty advanced fields backend (#1340)
extension of https://github.com/documenso/documenso/pull/1339
2024-09-10 21:35:35 +10:00
77da7847d9 fix: template with empty advanced fields (#1339)
Templates can be created and sent with advanced fields that have empty
values. That will cause an error when the user tries to sign the
document.

For example, you can create a template with a checkbox field and save
it. Then, you can click the "Use template" button and send the document
by clicking "Send document." However, this shouldn't be possible if the
advanced field doesn't have any values.
2024-09-10 16:23:16 +10:00
c36306d2c9 feat: add authOptions to the API (#1338)
Add the authOptions property to the document and
recipient related API endpoints.

These were previously missing so the only way API
users could set the authOptions was via templates
and using the generateTemplate endpoint.
2024-09-10 15:07:40 +10:00
f6f893fbf7 fix: prefill advanced field settings in templates (#1332)
## Description

Seems like I was overconfident in #1323 and I did not test properly.
Currently, the advanced settings for a field in a **template** is not
pre-filled with the current fieldMeta, although it is correctly
pre-filled in a **draft document** (That's probably where I messed up my
testing).

In this PR, I propose to directly use the fieldMeta provided by the
field prop in `FieldAdvancedSettings`, instead of multiplying tRPCs to
request the fieldMeta while we already have it.

I apologize for the wasted time in reviewing my previous PR which was
only correcting the display of the field label in the template view.

## Testing Performed

- This time, I correctly checked that the advanced settings for a field
is correctly pre-filled both in a document draft and in a template.
- `npm run build` builds correctly.
2024-09-10 13:22:03 +10:00
e1b2206d28 fix: select field ux (#1334)
When the Select field has a default value, it automatically signs with
it. If you change it, you need to refresh the page to re-sign again with
that value. This PR improves the UX by making the default value
"selectable" in the dropdown menu.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **New Features**
- Updated the `DropdownField` component to simplify the handling of
default values, ensuring the dropdown starts without a pre-selected
option.
- Improved the clarity of the placeholder text in the dropdown,
enhancing user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-10 13:15:47 +10:00
ad135b72d8 feat: marketing cta (#1335)
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---

## Description

<!--- Describe the changes introduced by this pull request. -->
<!--- Explain what problem it solves or what feature/fix it adds. -->

## Related Issue

<!--- If this pull request is related to a specific issue, reference it
here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->

## Changes Made

<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes.
-->

- Change 1
- Change 2
- ...

## Testing Performed

<!--- Describe the testing that you have performed to validate these
changes. -->
<!--- Include information about test cases, testing environments, and
results. -->

- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.

## Additional Notes

<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential
concerns, or anything else relevant. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new `CallToAction` component to enhance user engagement
in the self-hosting documentation.
- Added interactive call-to-action elements in the self-hosting
documentation pages to guide users towards specific actions.

- **Documentation**
- Updated self-hosting documentation to include the new call-to-action
feature, improving usability and interactivity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-10 12:42:12 +10:00
e81023f8d4 fix: refactor dates (#1321)
## Description

Refactor the current date formatting system to utilize Lingui.

## Changes Made

- Remove redundant `LocaleData` component with Lingui dates

## Important notes

For the internal pages for certificates, default to en-US to format any
dates.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **New Features**
- Enhanced internationalization support across various components by
utilizing the `i18n` object for date formatting.
- Streamlined locale management by removing cookie-based language
handling and adopting a more centralized approach.

- **Bug Fixes**
- Improved date formatting consistency by replacing the `LocaleDate`
component with direct calls to `i18n.date()` in multiple components.

- **Documentation**
- Updated localization strings in the `web.po` files to reflect recent
changes in the source code structure.

- **Chores**
- Minor formatting adjustments and code organization improvements across
various files to enhance readability and maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: github-actions <github-actions@documenso.com>
2024-09-10 12:38:08 +10:00
bfb09e7928 v1.7.0 2024-09-09 11:20:33 +10:00
d7e5aa1d26 v1.7.0-rc.5 2024-09-09 08:47:26 +10:00
8cb3ad4f3c chore: embedding blog post (#1329) 2024-09-06 22:11:55 +10:00
6c3acb1c2d feat: add and use NEXT_PRIVATE_INTERNAL_WEBAPP_URL (#1298)
Introduces `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` used for requesting the
app by itself (backend) [e.g. for background jobs]
2024-09-06 20:37:10 +10:00
3f82720383 feat: allow recipient to select signature color (#1325)
Allow users to select a colour for the signature. The allowed colours
are black, white, red, blue, and green.
2024-09-06 15:23:34 +10:00
a6f93698b4 fix: changed the default file path for local certs (#1277)
Changed the default value of NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH env
variable
2024-09-06 14:09:25 +10:00
bdc4ec1a31 chore: compile translations 2024-09-06 13:30:45 +10:00
bc471fcd9f chore: add translations (#1308)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **Chores**
- Updated the metadata revision dates in multiple German translation
files for consistency.
- Added extraneous blank lines in multiple translation files for cleaner
formatting.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-09-06 12:54:18 +10:00
f4e98ae03a feat: Added Elestio as one-click deploy option (#1302)
This PR introduces new one-click deployment option Elestio
2024-09-05 15:30:46 +10:00
0298e79e8c chore: add docs for translations (#1310)
Add developer documentation for translations.
2024-09-05 15:28:47 +10:00
8ab7464b84 fix: broken endpoint to update a field in a document (#1319)
Fixes #1318 and allows to submit a partial `fieldMeta` when updating a
field instead of replacing the current `fieldMeta`, to be consistent
with other endpoints.
2024-09-05 14:14:02 +10:00
ad4cff937d fix: show the existing field meta when editing a template fields (#1323)
Fixes #1259. I literally just used the exact same line of code than from
the document page so it is consistent.

The bug is extremely frustrating because if you make change to the
fields meta on the template and refresh the page, you can't see field
meta (label, default value, required), and neither do your co-workers.

Of course, I tested the change and it now displays correctly the
existing field meta on the template page.
2024-09-05 14:10:17 +10:00
210 changed files with 12641 additions and 2967 deletions

48
.cursorrules Normal file
View File

@ -0,0 +1,48 @@
Code Style and Structure:
- Write concise, technical TypeScript code with accurate examples
- Use functional and declarative programming patterns; avoid classes
- Prefer iteration and modularization over code duplication
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
- Structure files: exported component, subcomponents, helpers, static content, types
Naming Conventions:
- Use lowercase with dashes for directories (e.g., components/auth-wizard)
- Favor named exports for components
TypeScript Usage:
- Use TypeScript for all code; prefer interfaces over types
- Avoid enums; use maps instead
- Use functional components with TypeScript interfaces
Syntax and Formatting:
- Use the "function" keyword for pure functions
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements
- Use declarative JSX
Error Handling and Validation:
- Prioritize error handling: handle errors and edge cases early
- Use early returns and guard clauses
- Implement proper error logging and user-friendly messages
- Use Zod for form validation
- Model expected errors as return values in Server Actions
- Use error boundaries for unexpected errors
UI and Styling:
- Use Shadcn UI, Radix, and Tailwind Aria for components and styling
- Implement responsive design with Tailwind CSS; use a mobile-first approach
Performance Optimization:
- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC)
- Wrap client components in Suspense with fallback
- Use dynamic loading for non-critical components
- Optimize images: use WebP format, include size data, implement lazy loading
Key Conventions:
- Use 'nuqs' for URL search parameter state management
- Optimize Web Vitals (LCP, CLS, FID)
- Limit 'use client':
- Favor server components and Next.js SSR
- Use only for Web API access in small components
- Avoid for data fetching or state management
Follow Next.js docs for Data Fetching, Rendering, and Routing

View File

@ -27,6 +27,8 @@ NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
# [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
# URL used by the web app to request itself (e.g. local background jobs)
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
# [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"

View File

@ -32,6 +32,9 @@ jobs:
- name: Run Playwright tests
run: npm run ci
env:
# Needed since we use next start which will set the NODE_ENV to production
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: './example/cert.p12'
- uses: actions/upload-artifact@v4
if: always()

View File

@ -303,6 +303,10 @@ WantedBy=multi-user.target
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
## Elestio
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/documenso)
## Troubleshooting
### I'm not receiving any emails when using the developer quickstart.

View File

@ -0,0 +1,4 @@
{
"index": "Getting Started",
"contributing-translations": "Contributing Translations"
}

View File

@ -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!

View File

@ -1,5 +1,5 @@
---
title: Contributing Guide
title: Getting started
description: Learn how to contribute to Documenso and become part of our community.
---

View File

@ -3,5 +3,6 @@
"quickstart": "Developer Quickstart",
"manual": "Manual Setup",
"gitpod": "Gitpod",
"signing-certificate": "Signing Certificate"
}
"signing-certificate": "Signing Certificate",
"translations": "Translations"
}

View File

@ -0,0 +1,128 @@
---
title: Translations
description: Handling translations in code.
---
# About
Documenso uses the following stack to handle translations:
- [Lingui](https://lingui.dev/) - React i10n library
- [Crowdin](https://crowdin.com/) - Handles syncing translations
- [OpenAI](https://openai.com/) - Provides AI translations
Additional reading can be found in the [Lingui documentation](https://lingui.dev/introduction).
## Requirements
You **must** insert **`setupI18nSSR()`** when creating any of the following files:
- Server layout.tsx
- Server page.tsx
- Server loading.tsx
Server meaning it does not have `'use client'` in it.
```tsx
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
export default function SomePage() {
setupI18nSSR(); // Required if there are translations within the page, or nested in components.
// Rest of code...
}
```
Additional information can be found [here.](https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui)
## Quick guide
If you require more in-depth information, please see the [Lingui documentation](https://lingui.dev/introduction).
### HTML
Wrap all text to translate in **`<Trans></Trans>`** tags exported from **@lingui/macro** (not @lingui/react).
```html
<h1>
<Trans>Title</Trans>
</h1>
```
For text that is broken into elements, but represent a whole sentence, you must wrap it in a Trans tag so ensure the full message is extracted correctly.
```html
<h1>
<Trans>
This is one
<span className="text-foreground/60">full</span>
<a href="https://documenso.com">sentence</a>
</Trans>
</h1>
```
### Constants outside of react components
```tsx
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
// Wrap text in msg`text to translate` when it's in a constant here, or another file/package.
export const CONSTANT_WITH_MSG = {
foo: msg`Hello`,
bar: msg`World`,
};
export const SomeComponent = () => {
const { _ } = useLingui();
return (
<div>
{/* This will render the correct translated text. */}
<p>{_(CONSTANT_WITH_MSG.foo)}</p>
</div>
);
};
```
### Plurals
Lingui provides a Plural component to make it easy. See full documentation [here.](https://lingui.dev/ref/macro#plural-1)
```tsx
// Basic usage.
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
```
### Dates
Lingui provides a [DateTime instance](https://lingui.dev/ref/core#i18n.date) with the configured locale.
#### Server components
Note that the i18n instance is coming from **setupI18nSSR**.
```tsx
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
export const SomeComponent = () => {
const { i18n } = setupI18nSSR();
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
};
```
#### Client components
Note that the i18n instance is coming from the **import**.
```tsx
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
export const SomeComponent = () => {
return <Trans>The current date is {i18n.date(new Date(), { dateStyle: 'short' })}</Trans>;
};
```

View File

@ -5,6 +5,8 @@ description: Learn how to self-host Documenso on your server or cloud infrastruc
import { Callout, Steps } from 'nextra/components';
import { CallToAction } from '@documenso/ui/components/call-to-action';
# Self Hosting
We support various deployment methods and are actively working on adding more. Please let us know if you have a specific deployment method in mind!
@ -273,3 +275,5 @@ We offer several alternative deployment methods for Documenso if you need more o
## Koyeb
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
<CallToAction className="mt-12" utmSource="self-hosting" />

View File

@ -3,6 +3,10 @@ title: Getting Started with Self-Hosting
description: A step-by-step guide to setting up and hosting your own Documenso instance.
---
import { CallToAction } from '@documenso/ui/components/call-to-action';
# Getting Started with Self-Hosting
This is a step-by-step guide to setting up and hosting your own Documenso instance. Before getting started, [select the right license for you](/users/licenses).
<CallToAction className="mt-12" utmSource="self-hosting" />

View File

@ -10,6 +10,7 @@
"signing-documents": "Signing Documents",
"templates": "Templates",
"direct-links": "Direct Signing Links",
"document-visibility": "Document Visibility",
"-- Legal Overview": {
"type": "separator",
"title": "Legal Overview"

View File

@ -0,0 +1,18 @@
---
title: Document Visibility
description: Learn how to control the visibility of your team documents.
---
# Team's Document Visibility
By default, all documents created in a team are visible to all team members. However, you can control the visibility of your documents by changing the document's visibility settings.
To set the visibility of a document, click on the **Document visibility** dropdown in the document's settings panel.
![A screenshot of the Documenso's document editor page where you can update the document visibility](/document-visibility-settings.webp)
The document visibility can be set to one of the following options:
- **Everyone** - The document is visible to all team members.
- **Managers and above** - The document is visible to people with the role of Manager or above.
- **Admin only** - The document is only visible to the team's admins.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -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>
Weve crafted Documenso with a sleek, modern interface that makes it incredibly easy to use. Whether youre 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

View 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 dont presume this is the ideal path, but for me, its 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, Ive 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 youve 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 Ive found lacking in the discourse is the fact that Open Source is still being treated as socially proprietary. If its under an open-source license, you can fork it and try to improve upon the original, and theres nothing wrong with that. The same is true for closed-source startups. Yet in Open Source, theres a notion that its 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 wasnt 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 dont 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 isnt inherently exploitative; it can simply be a way to realize a projects full potential.
And then theres **MariaDB**, a fork of **MySQL**. After Oracle acquired MySQL, many in the community feared the projects open-source nature could be compromised. The fork preserved its spirit, and MariaDB has since grown to become a popular and thriving database. Its a reminder that sometimes, forking is not just acceptable—its 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 Documensos code is not “our” code. Its been co-owned by the world ever since we published the repo under AGPL V3. That is the whole point. Its 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.
## Documensos Approach: Co-Ownership and Community
So, if you want to fork Documenso and build a business on it, you can. Whether thats a cool thing to do is another matter. Whether you do a better job than us is also another matter (you wont). But if you do, Ill 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
Ive also thought a lot about question #3. I understand the impulse to fight anyone who doesnt appreciate this collaborative approach, but there is no part of this model that backs that up. You are free to “exploit” as long as its in a way that adds value. The fallacy is in considering someone else using the OSS part for their business as treason, which its not. Its 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 cant 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 dont like how we run things, go fork yourself and hold us accountable. We dont 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

View File

@ -0,0 +1,72 @@
---
title: 'Introducing Embedding Support for Documenso'
description: 'Embedding is now here! Learn how we built it and how it can be used to bring e-signing to your own applications.'
authorName: 'Lucas Smith'
authorImage: '/blog/blog-author-lucas.png'
authorRole: 'Co-Founder'
date: 2024-09-06
tags:
- Development
---
When we first launched Documenso, one of the most requested features was embedding. We knew it was important and aligned with our desire to not just be a e-signing application but to instead provide the e-signature infrastructure for the web and beyond.
With that said, we decided to hold off initially so we could focus on building a solid, well-featured core application. Looking back, this was definitely the right call. Embedding is only as good as the features behind it, and we didn't want to release something that wasn't ready to meet user and developer expectations.
Over the past year, we've been busy adding tons of new features and reaching new levels of compliance, like 21 CFR Part 11. We've also introduced [new fields](/blog/introducing-advanced-signing-fields), [built out an API](/blog/public-api), [added webhooks, integrations with Zapier](/blog/launch-week-2-day-4), and a lot more.
Now that we've laid a solid foundation, it's finally time to focus on embedding, the top-requested feature from both our users and those self-hosting our platform.
## Why Embedding Took Time
In previous projects, Ive often seen embedding built by bundling components for use in a clients website or app. This method gives users maximum flexibility for styling and behavior, while avoiding certain cross-origin issues. However, it can also introduce problems like code conflicts or performance bottlenecks. For example, third-party tools such as Google Tag Manager (GTM) or other marketing scripts can interfere with your SDK. Additionally, the SDK must remain lightweight to avoid slowing down the clients page.
For Documenso, we decided to explore a different approach. After carefully researching our options, we opted for an iframe-based solution. While iframes are typically less flexible—especially when it comes to theming or passing pre-filled data containing personally identifiable information (PII)—we identified ways to mitigate these concerns.
One of the biggest challenges was ensuring that we could pass sensitive data, like emails for pre-filling forms, without exposing PII to our server. To solve this, we used [fragment identifiers](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) in the URL, which are processed client-side and never sent in network requests. This method ensures that PII is protected and not logged by our server or any intermediate web services.
### Using the PostMessage API for Communication
To maintain a high level of interactivity, our iframes communicate with the parent window using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This allows us to notify the parent app when specific events occur inside the iframe, creating a more dynamic user experience and bridging the gap between our iframe-based solution and typical fat SDKs.
Additionally, props are passed into the iframe via the [fragment identifier](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) of the URL. This avoids the need for complex two-way data synchronization between the parent and child frames, making the system stable and more reliable.
### Building the Embeds with Mitosis
Given that our iframe solution is quite lightweight, we saw this as a great opportunity to experiment with [Mitosis](https://mitosis.builder.io/) which would let us do something truly special. For those unfamiliar, Mitosis is a project by Builder.io that lets you write components once and then transpile them into a variety of frameworks like React, Vue, and Svelte.
We used Mitosis to build two key components: a direct template embed and a document signing embed. The direct template allows users to use a template as if it were an evergreen document—meaning that, when someone completes the template, a new document is automatically generated. This is the use case we expect most users to adopt for embedding. For more advanced workflows, we also offer a document signing embed, which can handle multi-recipient workflows and other complex scenarios intended for use in deeper, rich integrations.
Mitosis allowed us to quickly target several popular frameworks, including [React](https://www.npmjs.com/package/@documenso/embed-react), [Preact](https://www.npmjs.com/package/@documenso/embed-preact), [Vue](https://www.npmjs.com/package/@documenso/embed-vue), [Svelte](https://www.npmjs.com/package/@documenso/embed-svelte), and [SolidJS](https://www.npmjs.com/package/@documenso/embed-solid).
I had also hoped to include Angular, but while Mitosis makes it really easy to transpile component, we still have to take care of bundling and packaging the resulting component ourselves. While the above frameworks can all be bundled using Vite.js, Angular still has it's own set of tooling that we would need to learn and use. Given this constraint we opted to put Angular on hold for now while we wait for the newer Vite.js support to mature.
### Challenges and Lessons with Mitosis and more
While our experience with Mitosis was largely positive, there were some challenges along the way. For instance, certain state properties with the same names as props caused issues during the transpilation process, leading to type errors and unexpected transpilation results with some targets.
This was also a challenge since our initial implementation of the two components had some minor separation of concerns which also resulted in some transpilation issues with some targets. We addressed this by removing the separation of concerns for now since it was mostly for show rather than out of necessity.
On top of that, packaging and publishing the embeds posed its own set of challenges, particularly given the growing complexity of JavaScript package management. Tools like [Publint](https://www.npmjs.com/package/publint) helped streamline the process by ensuring we followed best practices for both CommonJS and ESM formats.
### To the Future, The Documenso Platform
With the embedding feature now in place, we're excited to continue expanding Documenso's capabilities. Embeds are just the beginning of what we're calling the Documenso platform. Through our user research, we've learned that while many businesses appreciate having a flexible e-signature solution, they're even more interested in using our tools to build signing functionality directly into their own apps—without worrying about the technical complexities of compliance and security that come with e-signing.
Over the coming months, we'll be working on enhancing our API, strengthening integrations with tools like Zapier, and improving our webhook system. Our goal is to give users the ability to embed e-signatures and document management wherever they need it, whether that's through self-hosting or by using Documenso as a platform. We can't wait to see how our users and self-hosters leverage these new capabilities!
### Ready to Get Started?
If you're ready to embed document signing into your own app or website, check out our [Embedding Documentation](https://docs.documenso.com/developers/embedding?utm_source=blog&utm_campaign=introducing-embedding) to see how easy it is to get started. You'll find everything you need to get started today!
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
autoPlay
loop
controls
/>
We're always here to help! If you have questions or need support, join our [Discord](https://documen.so/discord) or [book a demo](https://documen.so/book-a-demo). We'd love to hear how you're using Documenso or wanting to use Documenso to enhance your workflow.
Stay tuned for more updates as we continue to evolve the Documenso platform and make it even easier to bring document signing into your workflows.

View File

@ -8,6 +8,80 @@ Check out what's new in the latest version and read our thoughts on it. For more
---
# Documenso v1.7.0: Embedded Signing, Copy and Paste, and More
We're thrilled to announce the release of Documenso v1.7.0, packed with exciting new features and improvements that enhance document signing flexibility, user experience, and global accessibility.
We're excited to see what you'll create with this release and we'd love to hear your feedback. Let's dive into the highlights:
## 🌟 Key Features
### Embedded Signing Experience
Take your document signing to the next level with our new embedded signing feature. Now you can seamlessly integrate Documenso's signing process directly into your own website or application, providing a smooth, branded experience for your users.
<video
src="/blog/introducing-embedding/embedding-demo.mp4"
className="aspect-video w-full"
controls
/>
Check out our [Embedding documentation](https://docs.documenso.com/developers/embedding) to learn more about how to get started.
### Copy and Paste Fields
Streamline your document preparation with our new copy and paste functionality for fields. This feature allows you to quickly duplicate fields across your document, saving time and ensuring consistency in your templates.
### Customizable Signature Colors
Recipients can now select a signature color from our list of available colors, supporting workflows where specific colors are required for each recipient, location, or document.
### Enhanced Internationalization (i18n)
Following on from our last release we've now expanded our i18n support to the main web application. We haven't yet added support for any additional languages but that will be coming quickly now that we have completed the hard work of wrapping all of our content in our new i18n system.
These enhancements make Documenso more accessible to users worldwide.
## 🔧 Other Improvements
- **API Enhancements**:
- New endpoint to prefill fields via API
- Updated createFields API endpoint for more flexibility
- Automatically set public profile URL for OIDC users
- **Security and Performance**:
- Document sealing moved to a background job for improved performance
- Disable 2FA with backup codes for enhanced account recovery options
- Extended lifespan for invites and confirmations
- **User Experience**:
- Updated email templates to reflect team-specific information
- Fixed issues with dialog closing on page refresh
- Improved field editing in document templates
- **Other Items**:
- Added Elestio as a one-click deploy option
- Updated README for manual self-hosting
- New environment variable for internal webapp URL configuration
## 📚 New Content
- [Advanced fields article to help you make the most of Documenso's capabilities](/blog/introducing-advanced-signing-fields)
- [Embedding blog post to guide you through how we implemented embedding](/blog/introducing-embedding)
## 👏 Community Contributions
A big thank you to our vibrant community! This release includes contributions from several new contributors, further enriching Documenso's capabilities.
We're excited to see how you'll use these new features to streamline your document workflows. As always, we appreciate your feedback and support in making Documenso the best open-source document signing solution available.
Enjoy exploring v1.7.0!
---
# Documenso v1.6.1: Internationalization, Enhanced OIDC, and More
We're excited to announce the release of Documenso v1.6.1, which brings several improvements to enhance your document signing experience. Here are the key updates:

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.7.0-rc.4",
"version": "1.7.2-rc.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -34,7 +34,7 @@
"micro": "^10.0.1",
"next": "14.2.6",
"next-auth": "4.24.5",
"next-axiom": "^1.1.1",
"next-axiom": "^1.5.1",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0",

View File

@ -2,7 +2,8 @@ declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

View File

@ -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',
},
{

View File

@ -168,6 +168,7 @@ export const SinglePlayerClient = () => {
sendStatus: 'NOT_SENT',
role: 'SIGNER',
authOptions: null,
signingOrder: null,
};
const onFileDrop = async (file: File) => {

View File

@ -1,7 +1,6 @@
import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { cookies, headers } from 'next/headers';
import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env';
@ -10,8 +9,6 @@ import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/featur
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -59,25 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags();
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
// Should be safe to remove when we upgrade NextJS.
// https://github.com/vercel/next.js/pull/65008
// Currently if the middleware sets the cookie, it's not accessible in the cookies
// during the same render.
// So we go the roundabout way of checking the header for the set-cookie value.
if (!cookies().get('i18n')) {
const setCookieValue = headers().get('set-cookie');
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
if (i18nCookie) {
const i18n = i18nCookie.split('=')[1];
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
}
}
const { lang, i18n } = setupI18nSSR(overrideLang);
const { lang, locales, i18n } = setupI18nSSR();
return (
<html
@ -105,7 +84,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<PlausibleProvider>
<TrpcProvider>
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
<I18nClientProvider
initialLocaleData={{ lang, locales }}
initialMessages={i18n.messages}
>
{children}
</I18nClientProvider>
</TrpcProvider>

View File

@ -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();

View File

@ -1,6 +1,6 @@
'use client';
import type { HTMLAttributes } from 'react';
import { type HTMLAttributes, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
@ -9,15 +9,15 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { FaXTwitter } from 'react-icons/fa6';
import { LiaDiscord } from 'react-icons/lia';
import { LuGithub } from 'react-icons/lu';
import { LuGithub, LuLanguages } from 'react-icons/lu';
import LogoImage from '@documenso/assets/logo.png';
import { cn } from '@documenso/ui/lib/utils';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
import { I18nSwitcher } from '~/components/(marketing)/i18n-switcher';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
// import { StatusWidgetContainer } from './status-widget-container';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
@ -44,7 +44,9 @@ const FOOTER_LINKS = [
];
export const Footer = ({ className, ...props }: FooterProps) => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
return (
<div className={cn('border-t py-12', className)} {...props}>
@ -97,13 +99,22 @@ export const Footer = ({ className, ...props }: FooterProps) => {
</p>
<div className="flex flex-row-reverse items-center sm:flex-row">
<I18nSwitcher className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2" />
<Button
className="text-muted-foreground ml-2 rounded-full font-normal sm:mr-2"
variant="ghost"
onClick={() => setLanguageSwitcherOpen(true)}
>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[i18n.locale]?.full || i18n.locale}
</Button>
<div className="flex flex-wrap">
<ThemeSwitcher />
</div>
</div>
</div>
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
</div>
);
};

View File

@ -1,71 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { CheckIcon } from 'lucide-react';
import { LuLanguages } from 'react-icons/lu';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
CommandDialog,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@documenso/ui/primitives/command';
type I18nSwitcherProps = {
className?: string;
};
export const I18nSwitcher = ({ className }: I18nSwitcherProps) => {
const { i18n, _ } = useLingui();
const [open, setOpen] = useState(false);
const [value, setValue] = useState(i18n.locale);
const setLanguage = async (lang: string) => {
setValue(lang);
setOpen(false);
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
};
return (
<>
<Button className={className} variant="ghost" onClick={() => setOpen(true)}>
<LuLanguages className="mr-1.5 h-4 w-4" />
{SUPPORTED_LANGUAGES[value]?.full || i18n.locale}
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={_(msg`Search languages...`)} />
<CommandList>
<CommandGroup>
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
key={language.short}
value={language.full}
onSelect={async () => setLanguage(language.short)}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
value === language.short ? 'opacity-100' : 'opacity-0',
)}
/>
{SUPPORTED_LANGUAGES[language.short].full}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
};

View File

@ -1,39 +0,0 @@
import { cookies } from 'next/headers';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
export default function middleware(req: NextRequest) {
const lang = extractSupportedLanguage({
headers: req.headers,
cookies: cookies(),
});
const response = NextResponse.next();
response.cookies.set('i18n', lang);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - ingest (analytics)
* - site.webmanifest
*/
{
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.7.0-rc.4",
"version": "1.7.2-rc.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -37,7 +37,7 @@
"micro": "^10.0.1",
"next": "14.2.6",
"next-auth": "4.24.5",
"next-axiom": "^1.1.1",
"next-axiom": "^1.5.1",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"papaparse": "^5.4.1",
@ -53,7 +53,7 @@
"react-icons": "^4.11.0",
"react-rnd": "^10.4.1",
"recharts": "^2.7.2",
"remeda": "^1.27.1",
"remeda": "^2.12.1",
"sharp": "0.32.6",
"ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37",

View File

@ -2,6 +2,7 @@ declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_WEBAPP_URL?: string;
NEXT_PUBLIC_MARKETING_URL?: string;
NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
NEXT_PRIVATE_DATABASE_URL: string;

View File

@ -12,7 +12,6 @@ import {
import { Badge } from '@documenso/ui/primitives/badge';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { AdminActions } from './admin-actions';
import { RecipientItem } from './recipient-item';
@ -25,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
};
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
setupI18nSSR();
const { i18n } = setupI18nSSR();
const document = await getEntireDocument({ id: Number(params.id) });
@ -46,12 +45,11 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<div className="text-muted-foreground mt-4 text-sm">
<div>
<Trans>Created on</Trans>:{' '}
<LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
<Trans>Created on</Trans>: {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
</div>
<div>
<Trans>Last updated at</Trans>:{' '}
<LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
<Trans>Last updated at</Trans>: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
</div>
</div>

View File

@ -21,12 +21,11 @@ import { Input } from '@documenso/ui/primitives/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
// export type AdminDocumentResultsProps = {};
export const AdminDocumentResults = () => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
@ -62,7 +61,7 @@ export const AdminDocumentResults = () => {
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: _(msg`Title`),
@ -122,7 +121,7 @@ export const AdminDocumentResults = () => {
{
header: 'Last updated',
accessorKey: 'updatedAt',
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
cell: ({ row }) => i18n.date(row.original.updatedAt),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);

View File

@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = {
@ -24,21 +23,9 @@ export const DocumentPageViewInformation = ({
}: DocumentPageViewInformationProps) => {
const isMounted = useIsMounted();
const { locale } = useLocale();
const { _ } = useLingui();
const { _, i18n } = useLingui();
const documentInformation = useMemo(() => {
let createdValue = DateTime.fromJSDate(document.createdAt).toFormat('MMMM d, yyyy');
let lastModifiedValue = DateTime.fromJSDate(document.updatedAt).toRelative();
if (!isMounted) {
createdValue = DateTime.fromJSDate(document.createdAt)
.setLocale(locale)
.toFormat('MMMM d, yyyy');
lastModifiedValue = DateTime.fromJSDate(document.updatedAt).setLocale(locale).toRelative();
}
return [
{
description: msg`Uploaded by`,
@ -46,15 +33,19 @@ export const DocumentPageViewInformation = ({
},
{
description: msg`Created`,
value: createdValue,
value: DateTime.fromJSDate(document.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('MMMM d, yyyy'),
},
{
description: msg`Last modified`,
value: lastModifiedValue,
value: DateTime.fromJSDate(document.updatedAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toRelative(),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted, document, locale, userId]);
}, [isMounted, document, userId]);
return (
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">

View File

@ -12,10 +12,12 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus } from '@documenso/prisma/client';
import type { Team, TeamEmail } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -39,7 +41,7 @@ export type DocumentPageViewProps = {
params: {
id: string;
};
team?: Team & { teamEmail: TeamEmail | null };
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
};
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
@ -62,11 +64,35 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
teamId: team?.id,
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
let canAccessDocument = true;
if (team && !isRecipient) {
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
.otherwise(() => false);
}
const isDocumentHistoryEnabled = await getServerComponentFlag(
'app_document_page_view_history_sheet',
);
if (!document || !document.documentData) {
if (!document || !document.documentData || (team && !canAccessDocument)) {
redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
}

View File

@ -85,6 +85,20 @@ export const EditDocumentForm = ({
},
});
const { mutateAsync: setSigningOrderForDocument } =
trpc.document.setSigningOrderForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
);
},
});
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newFields) => {
@ -177,6 +191,7 @@ export const EditDocumentForm = ({
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null,
},
@ -204,15 +219,22 @@ export const EditDocumentForm = ({
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
try {
await addSigners({
documentId: document.id,
teamId: team?.id,
signers: data.signers.map((signer) => ({
...signer,
// Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null,
})),
});
await Promise.all([
setSigningOrderForDocument({
documentId: document.id,
signingOrder: data.signingOrder,
}),
addSigners({
documentId: document.id,
teamId: team?.id,
signers: data.signers.map((signer) => ({
...signer,
// Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null,
})),
}),
]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
@ -339,6 +361,7 @@ export const EditDocumentForm = ({
key={recipients.length}
documentFlow={documentFlow.settings}
document={document}
currentTeamMemberRole={team?.currentTeamMember?.role}
recipients={recipients}
fields={fields}
isDocumentEnterprise={isDocumentEnterprise}
@ -350,6 +373,7 @@ export const EditDocumentForm = ({
key={recipients.length}
documentFlow={documentFlow.signers}
recipients={recipients}
signingOrder={document.documentMeta?.signingOrder}
fields={fields}
isDocumentEnterprise={isDocumentEnterprise}
onSubmit={onAddSignersFormSubmit}

View File

@ -3,14 +3,17 @@ import { redirect } from 'next/navigation';
import { Plural, Trans } from '@lingui/macro';
import { ChevronLeft, Users2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
@ -21,7 +24,7 @@ export type DocumentEditPageViewProps = {
params: {
id: string;
};
team?: Team;
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
};
export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
@ -43,10 +46,34 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
teamId: team?.id,
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
let canAccessDocument = true;
if (!isRecipient) {
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
.otherwise(() => false);
}
if (!document) {
redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
}
if (document.status === InternalDocumentStatus.COMPLETED) {
redirect(`${documentRootPath}/${documentId}`);
}

View File

@ -20,8 +20,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
export type DocumentLogsDataTableProps = {
documentId: number;
};
@ -32,7 +30,7 @@ const dateFormat: DateTimeFormatOptions = {
};
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -78,7 +76,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
{
header: _(msg`Time`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
},
{
header: _(msg`User`),
@ -106,9 +104,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => (
<span>
{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}
</span>
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
),
},
{

View File

@ -9,7 +9,6 @@ import { DateTime } from 'luxon';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Recipient, Team } from '@documenso/prisma/client';
@ -32,9 +31,7 @@ export type DocumentLogsPageViewProps = {
};
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
const { _ } = useLingui();
const locale = getLocale();
const { _, i18n } = useLingui();
const { id } = params;
@ -87,13 +84,13 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
{
description: msg`Date created`,
value: DateTime.fromJSDate(document.createdAt)
.setLocale(locale)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
},
{
description: msg`Last updated`,
value: DateTime.fromJSDate(document.updatedAt)
.setLocale(locale)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
},
{

View File

@ -18,7 +18,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown';
@ -41,8 +40,9 @@ export const DocumentsDataTable = ({
showSenderColumn,
team,
}: DocumentsDataTableProps) => {
const { _, i18n } = useLingui();
const { data: session } = useSession();
const { _ } = useLingui();
const [isPending, startTransition] = useTransition();
@ -53,12 +53,8 @@ export const DocumentsDataTable = ({
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => (
<LocaleDate
date={row.original.createdAt}
format={{ ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }}
/>
),
cell: ({ row }) =>
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
},
{
header: _(msg`Title`),
@ -88,8 +84,7 @@ export const DocumentsDataTable = ({
{
header: _(msg`Actions`),
cell: ({ row }) =>
(!row.original.deletedAt ||
row.original.status === ExtendedDocumentStatus.COMPLETED) && (
(!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
<div className="flex items-center gap-x-4">
<DataTableActionButton team={team} row={row.original} />
<DataTableActionDropdown team={team} row={row.original} />

View File

@ -10,12 +10,13 @@ import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stat
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team, TeamEmail } from '@documenso/prisma/client';
import type { Team, TeamEmail, TeamMemberRole } from '@documenso/prisma/client';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
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 };
};
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
}
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
const { user } = await getRequiredServerComponentSession();
@ -44,13 +46,16 @@ 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;
const currentTeamMemberRole = team?.currentTeamMember?.role;
const getStatOptions: GetStatsInput = {
user,
period,
search,
};
if (team) {
@ -58,6 +63,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
teamId: team.id,
teamEmail: team.teamEmail?.email,
senderIds,
currentTeamMemberRole,
currentUserEmail: user.email,
userId: user.id,
};
}
@ -75,6 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
perPage,
period,
senderIds,
search,
});
const getTabHref = (value: typeof status) => {
@ -131,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>
@ -147,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>

View File

@ -16,8 +16,6 @@ import { type Stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { LocaleDate } from '~/components/formatter/locale-date';
import { BillingPlans } from './billing-plans';
import { BillingPortalButton } from './billing-portal-button';
@ -26,7 +24,7 @@ export const metadata: Metadata = {
};
export default async function BillingSettingsPage() {
setupI18nSSR();
const { i18n } = setupI18nSSR();
let { user } = await getRequiredServerComponentSession();
@ -104,12 +102,12 @@ export default async function BillingSettingsPage() {
{subscription.cancelAtPeriodEnd ? (
<span>
end on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
</span>
) : (
<span>
automatically renew on{' '}
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
<span className="font-semibold">{i18n.date(subscription.periodEnd)}.</span>
</span>
)}
</span>

View File

@ -20,15 +20,13 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
hourCycle: 'h12',
};
export const UserSecurityActivityDataTable = () => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const pathname = usePathname();
const router = useRouter();
@ -71,7 +69,7 @@ export const UserSecurityActivityDataTable = () => {
{
header: _(msg`Date`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
},
{
header: _(msg`Device`),

View File

@ -73,7 +73,7 @@ export const UserPasskeysDataTable = () => {
cell: ({ row }) =>
row.original.lastUsedAt
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
: msg`Never`,
: _(msg`Never`),
},
{
id: 'actions',

View File

@ -7,11 +7,10 @@ import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-use
import { Button } from '@documenso/ui/primitives/button';
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { ApiTokenForm } from '~/components/forms/token';
export default async function ApiTokensPage() {
setupI18nSSR();
const { i18n } = setupI18nSSR();
const { user } = await getRequiredServerComponentSession();
@ -65,13 +64,11 @@ export default async function ApiTokensPage() {
<h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>Created on</Trans>{' '}
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
<Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
</p>
{token.expires ? (
<p className="text-muted-foreground mt-1 text-xs">
<Trans>Expires on</Trans>{' '}
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
<Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
</p>
) : (
<p className="text-muted-foreground mt-1 text-xs">

View File

@ -16,10 +16,9 @@ import { Button } from '@documenso/ui/primitives/button';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
export default function WebhookPage() {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
@ -86,10 +85,7 @@ export default function WebhookPage() {
</p>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
</p>
</div>

View File

@ -103,6 +103,19 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: setSigningOrderForTemplate } =
trpc.template.setSigningOrderForTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
{
id: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
},
});
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
@ -160,11 +173,19 @@ export const EditTemplateForm = ({
data: TAddTemplatePlacholderRecipientsFormSchema,
) => {
try {
await addTemplateSigners({
templateId: template.id,
teamId: team?.id,
signers: data.signers,
});
await Promise.all([
setSigningOrderForTemplate({
templateId: template.id,
teamId: team?.id,
signingOrder: data.signingOrder,
}),
addTemplateSigners({
templateId: template.id,
teamId: team?.id,
signers: data.signers,
}),
]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
@ -262,6 +283,7 @@ export const EditTemplateForm = ({
documentFlow={documentFlow.signers}
recipients={recipients}
fields={fields}
signingOrder={template.templateMeta?.signingOrder}
templateDirectLink={template.directLink}
onSubmit={onAddTemplatePlaceholderFormSubmit}
isEnterprise={isEnterprise}

View File

@ -17,7 +17,6 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { LocaleDate } from '~/components/formatter/locale-date';
import { TemplateType } from '~/components/formatter/template-type';
import { DataTableActionDropdown } from './data-table-action-dropdown';
@ -48,7 +47,7 @@ export const TemplatesDataTable = ({
const updateSearchParams = useUpdateSearchParams();
const { _ } = useLingui();
const { _, i18n } = useLingui();
const { remaining } = useLimits();
const columns = useMemo(() => {
@ -56,7 +55,7 @@ export const TemplatesDataTable = ({
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: _(msg`Title`),
@ -81,8 +80,8 @@ export const TemplatesDataTable = ({
<p>
<Trans>
Public templates are connected to your public profile. Any modifications
to public templates will also appear in your public profile.
Public templates are connected to your public profile. Any modifications to
public templates will also appear in your public profile.
</Trans>
</p>
</li>
@ -94,9 +93,9 @@ export const TemplatesDataTable = ({
<p>
<Trans>
Direct link templates contain one dynamic recipient placeholder. Anyone
with access to this link can sign the document, and it will then appear
on your documents page.
Direct link templates contain one dynamic recipient placeholder. Anyone with
access to this link can sign the document, and it will then appear on your
documents page.
</Trans>
</p>
</li>
@ -109,8 +108,8 @@ export const TemplatesDataTable = ({
<p>
{teamId ? (
<Trans>
Team only templates are not linked anywhere and are visible only to
your team.
Team only templates are not linked anywhere and are visible only to your
team.
</Trans>
) : (
<Trans>Private templates can only be modified and viewed by you.</Trans>

View File

@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { UAParser } from 'ua-parser-js';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import {
@ -15,8 +16,6 @@ import {
TableRow,
} from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[];
};
@ -49,7 +48,9 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
{logs.map((log, i) => (
<TableRow className="break-inside-avoid" key={i}>
<TableCell>
<LocaleDate format={dateFormat} date={log.createdAt} />
{DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</TableCell>
<TableCell>

View File

@ -2,7 +2,9 @@ import React from 'react';
import { redirect } from 'next/navigation';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { DateTime } from 'luxon';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
@ -10,7 +12,6 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Logo } from '~/components/branding/logo';
import { LocaleDate } from '~/components/formatter/locale-date';
import { AuditLogDataTable } from './data-table';
@ -21,8 +22,6 @@ type AuditLogProps = {
};
export default async function AuditLog({ searchParams }: AuditLogProps) {
setupI18nSSR();
const { d } = searchParams;
if (typeof d !== 'string' || !d) {
@ -89,7 +88,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">Created At</span>
<span className="mt-1 block">
<LocaleDate date={document.createdAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
{DateTime.fromJSDate(document.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
</span>
</p>
@ -97,7 +98,9 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">Last Updated</span>
<span className="mt-1 block">
<LocaleDate date={document.updatedAt} format="yyyy-mm-dd hh:mm:ss a (ZZZZ)" />
{DateTime.fromJSDate(document.updatedAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
</span>
</p>

View File

@ -2,10 +2,11 @@ import React from 'react';
import { redirect } from 'next/navigation';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import {
RECIPIENT_ROLES_DESCRIPTION_ENG,
RECIPIENT_ROLE_SIGNING_REASONS_ENG,
@ -27,7 +28,6 @@ import {
} from '@documenso/ui/primitives/table';
import { Logo } from '~/components/branding/logo';
import { LocaleDate } from '~/components/formatter/locale-date';
type SigningCertificateProps = {
searchParams: {
@ -41,8 +41,6 @@ const FRIENDLY_SIGNING_REASONS = {
};
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
setupI18nSSR();
const { d } = searchParams;
if (typeof d !== 'string' || !d) {
@ -231,42 +229,33 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Sent:</span>{' '}
<span className="inline-block">
{logs.EMAIL_SENT[0] ? (
<LocaleDate
date={logs.EMAIL_SENT[0].createdAt}
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
/>
) : (
'Unknown'
)}
{logs.EMAIL_SENT[0]
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
</span>
</p>
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Viewed:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_OPENED[0] ? (
<LocaleDate
date={logs.DOCUMENT_OPENED[0].createdAt}
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
/>
) : (
'Unknown'
)}
{logs.DOCUMENT_OPENED[0]
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
</span>
</p>
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Signed:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] ? (
<LocaleDate
date={logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt}
format="yyyy-MM-dd hh:mm:ss a (ZZZZ)"
/>
) : (
'Unknown'
)}
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
</span>
</p>

View File

@ -267,14 +267,14 @@ export const CheckboxField = ({
)}
{field.inserted && (
<div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-y-1">
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`;
return (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="h-4 w-4"
className="h-3 w-3"
checkClassName="text-white"
id={`checkbox-${index}`}
checked={field.customText
@ -283,7 +283,7 @@ export const CheckboxField = ({
disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>
<Label htmlFor={`checkbox-${index}`}>
<Label htmlFor={`checkbox-${index}`} className="text-xs">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>

View File

@ -204,25 +204,29 @@ export default async function CompletedSigningPage({
</div>
</div>
{canSignUp && (
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
<Trans>Need to sign documents?</Trans>
</h2>
<div className="flex flex-col items-center">
{canSignUp && (
<div className="flex max-w-xl flex-col items-center justify-center p-4 md:p-12">
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
<Trans>Need to sign documents?</Trans>
</h2>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
<Trans>Create your account and start using state-of-the-art document signing.</Trans>
</p>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
<Trans>
Create your account and start using state-of-the-art document signing.
</Trans>
</p>
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div>
)}
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div>
)}
{isLoggedIn && (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
<Trans>Go Back Home</Trans>
</Link>
)}
{isLoggedIn && (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600">
<Trans>Go Back Home</Trans>
</Link>
)}
</div>
</div>
<PollUntilDocumentCompleted document={document} />

View File

@ -150,7 +150,7 @@ export const DateField = ({
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-sm duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{localDateString}
</p>
)}

View File

@ -1,11 +1,11 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { DateTime } from 'luxon';
import { signOut } from 'next-auth/react';
import { RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { DialogFooter } from '@documenso/ui/primitives/dialog';
@ -25,22 +25,19 @@ export const DocumentActionAuthAccount = ({
}: DocumentActionAuthAccountProps) => {
const { recipient } = useRequiredDocumentAuthContext();
const [isSigningOut, setIsSigningOut] = useState(false);
const router = useRouter();
const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email: string) => {
try {
setIsSigningOut(true);
const encryptedEmail = await encryptSecondaryData({
data: email,
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
await signOut({
redirect: false,
});
await signOut({
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
});
router.push(`/signin#email=${email}`);
} catch {
setIsSigningOut(false);

View File

@ -127,7 +127,7 @@ export const DropdownField = ({
await removeSignedFieldWithToken(payload);
}
setLocalChoice(parsedFieldMeta.defaultValue ?? '');
setLocalChoice('');
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
@ -179,7 +179,7 @@ export const DropdownField = ({
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
<Select value={parsedFieldMeta.defaultValue} onValueChange={handleSelectItem}>
<Select value={localChoice} onValueChange={handleSelectItem}>
<SelectTrigger
className={cn(
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0',
@ -189,7 +189,10 @@ export const DropdownField = ({
},
)}
>
<SelectValue placeholder={`-- ${_(msg`Select`)} --`} />
<SelectValue
className="text-[clamp(0.625rem,1cqw,0.825rem)]"
placeholder={`${_(msg`Select`)}`}
/>
</SelectTrigger>
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
{parsedFieldMeta?.values?.map((item, index) => (
@ -203,7 +206,7 @@ export const DropdownField = ({
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{field.customText}
</p>
)}

View File

@ -128,7 +128,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{field.customText}
</p>
)}

View File

@ -29,9 +29,16 @@ export type SigningFormProps = {
recipient: Recipient;
fields: Field[];
redirectUrl?: string | null;
isRecipientsTurn: boolean;
};
export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
export const SigningForm = ({
document,
recipient,
fields,
redirectUrl,
isRecipientsTurn,
}: SigningFormProps) => {
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
@ -150,6 +157,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>
@ -213,6 +221,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>

View File

@ -131,7 +131,7 @@ export const InitialsField = ({
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{field.customText}
</p>
)}

View File

@ -172,7 +172,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{field.customText}
</p>
)}

View File

@ -259,7 +259,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 duration-200">
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.625rem,1cqw,0.825rem)] duration-200">
{field.customText}
</p>
)}
@ -267,7 +267,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
<DialogContent>
<DialogTitle>
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add number</Trans>}
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
</DialogTitle>
<div>

View File

@ -9,6 +9,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
@ -42,6 +43,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
return redirect(`/sign/${token}/waiting`);
}
const [document, fields, recipient, completedFields] = await Promise.all([
getDocumentAndSenderByToken({
token,
@ -146,6 +153,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
/>
</DocumentAuthProvider>
</SigningProvider>

View File

@ -173,16 +173,16 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
)}
{field.inserted && (
<RadioGroup>
<RadioGroup className="gap-y-1">
{values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className=""
className="h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.value === field.customText}
/>
<Label htmlFor={`option-${index}`}>
<Label htmlFor={`option-${index}`} className="text-xs">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>

View File

@ -23,6 +23,7 @@ export type SignDialogProps = {
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
role: RecipientRole;
disabled?: boolean;
};
export const SignDialog = ({
@ -32,6 +33,7 @@ export const SignDialog = ({
fieldsValidated,
onSignatureComplete,
role,
disabled = false,
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
const truncatedTitle = truncateTitle(documentTitle);
@ -54,6 +56,7 @@ export const SignDialog = ({
size="lg"
onClick={fieldsValidated}
loading={isSubmitting}
disabled={disabled}
>
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
</Button>

View File

@ -2,12 +2,12 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { signOut } from 'next-auth/react';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
@ -20,24 +20,19 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
const { _ } = useLingui();
const { toast } = useToast();
const [isSigningOut, setIsSigningOut] = useState(false);
const router = useRouter();
const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email: string) => {
try {
setIsSigningOut(true);
const encryptedEmail = await encryptSecondaryData({
data: email,
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
await signOut({
redirect: false,
});
await signOut({
callbackUrl: emailHasAccount
? `/signin?email=${encodeURIComponent(encryptedEmail)}`
: `/signup?email=${encodeURIComponent(encryptedEmail)}`,
});
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -128,7 +128,7 @@ export const SigningFieldContainer = ({
};
return (
<div className={cn(type === 'Checkbox' ? 'group' : '')}>
<div className={cn('[container-type:size]', type === 'Checkbox' ? 'group' : '')}>
<FieldRootContainer field={field}>
{!field.inserted && !loading && !readOnlyField && (
<button

View File

@ -39,6 +39,7 @@ export type SigningPageViewProps = {
recipient: Recipient;
fields: Field[];
completedFields: CompletedField[];
isRecipientsTurn: boolean;
};
export const SigningPageView = ({
@ -46,6 +47,7 @@ export const SigningPageView = ({
recipient,
fields,
completedFields,
isRecipientsTurn,
}: SigningPageViewProps) => {
const { documentData, documentMeta } = document;
@ -99,6 +101,7 @@ export const SigningPageView = ({
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
/>
</div>
</div>

View File

@ -253,7 +253,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
>
<span className="flex items-center justify-center gap-x-1">
<Type />
{fieldDisplayName || <Trans>Add text</Trans>}
{fieldDisplayName || <Trans>Text</Trans>}
</span>
</p>
)}
@ -269,7 +269,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
<DialogContent>
<DialogTitle>
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add Text</Trans>}
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Text</Trans>}
</DialogTitle>
<div>

View File

@ -0,0 +1,100 @@
import Link from 'next/link';
import { notFound, redirect } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
type WaitingForTurnToSignPageProps = {
params: { token?: string };
};
export default async function WaitingForTurnToSignPage({
params: { token },
}: WaitingForTurnToSignPageProps) {
setupI18nSSR();
if (!token) {
return notFound();
}
const { user } = await getServerComponentSession();
const [document, recipient] = await Promise.all([
getDocumentAndSenderByToken({ token }).catch(() => null),
getRecipientByToken({ token }).catch(() => null),
]);
if (!document || !recipient) {
return notFound();
}
if (document.status === DocumentStatus.COMPLETED) {
return redirect(`/sign/${token}/complete`);
}
let isOwnerOrTeamMember = false;
let team: Team | null = null;
if (user) {
isOwnerOrTeamMember = await getDocumentById({
id: document.id,
userId: user.id,
teamId: document.teamId ?? undefined,
})
.then((document) => !!document)
.catch(() => false);
if (document.teamId) {
team = await getTeamById({
userId: user.id,
teamId: document.teamId,
});
}
}
return (
<div className="relative flex flex-col items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
<div className="w-full max-w-md text-center">
<h2 className="tracking-tigh text-3xl font-bold">
<Trans>Waiting for Your Turn</Trans>
</h2>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
It's currently not your turn to sign. You will receive an email with instructions once
it's your turn to sign the document.
</Trans>
</p>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>Please check your email for updates.</Trans>
</p>
<div className="mt-4">
{isOwnerOrTeamMember ? (
<Button variant="link" asChild>
<Link href={`${formatDocumentsPath(team?.url)}/${document.id}`}>
<Trans>Were you trying to edit this document instead?</Trans>
</Link>
</Button>
) : (
<Button variant="link" asChild>
<Link href="/documents">Return Home</Link>
</Button>
)}
</div>
</div>
</div>
);
}

View File

@ -12,7 +12,6 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { Button } from '@documenso/ui/primitives/button';
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { ApiTokenForm } from '~/components/forms/token';
type ApiTokensPageProps = {
@ -22,7 +21,7 @@ type ApiTokensPageProps = {
};
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
setupI18nSSR();
const { i18n } = setupI18nSSR();
const { teamUrl } = params;
@ -98,13 +97,17 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>Created on</Trans>{' '}
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
<Trans>
Created on
{i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
</Trans>
</p>
{token.expires ? (
<p className="text-muted-foreground mt-1 text-xs">
<Trans>Expires on</Trans>{' '}
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
<Trans>
Expires on
{i18n.date(token.expires, DateTime.DATETIME_FULL)}
</Trans>
</p>
) : (
<p className="text-muted-foreground mt-1 text-xs">

View File

@ -16,11 +16,10 @@ import { Button } from '@documenso/ui/primitives/button';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { useCurrentTeam } from '~/providers/team';
export default function WebhookPage() {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const team = useCurrentTeam();
@ -91,10 +90,7 @@ export default function WebhookPage() {
</p>
<p className="text-muted-foreground mt-2 text-xs">
<Trans>
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
<Trans>Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}</Trans>
</p>
</div>

View File

@ -1,6 +1,5 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
@ -11,7 +10,6 @@ import {
IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { SignInForm } from '~/components/forms/signin';
@ -19,24 +17,11 @@ export const metadata: Metadata = {
title: 'Sign In',
};
type SignInPageProps = {
searchParams: {
email?: string;
};
};
export default function SignInPage({ searchParams }: SignInPageProps) {
export default function SignInPage() {
setupI18nSSR();
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
if (!email && rawEmail) {
redirect('/signin');
}
return (
<div className="w-screen max-w-lg px-4">
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
@ -50,7 +35,6 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
<hr className="-mx-6 my-4" />
<SignInForm
initialEmail={email || undefined}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
oidcProviderLabel={OIDC_PROVIDER_LABEL}

View File

@ -5,7 +5,6 @@ import { env } from 'next-runtime-env';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { SignUpFormV2 } from '~/components/forms/v2/signup';
@ -13,13 +12,7 @@ export const metadata: Metadata = {
title: 'Sign Up',
};
type SignUpPageProps = {
searchParams: {
email?: string;
};
};
export default function SignUpPage({ searchParams }: SignUpPageProps) {
export default function SignUpPage() {
setupI18nSSR();
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
@ -28,17 +21,9 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
redirect('/signin');
}
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
if (!email && rawEmail) {
redirect('/signup');
}
return (
<SignUpFormV2
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
initialEmail={email || undefined}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
/>

View File

@ -1,5 +1,8 @@
import { z } from 'zod';
export const ZBaseEmbedDataSchema = z.object({
css: z.string().optional().transform(value => value || undefined),
css: z
.string()
.optional()
.transform((value) => value || undefined),
});

View File

@ -18,15 +18,18 @@ export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentComplet
<div className="mt-8 w-full max-w-md">
<SigningCard3D
className='w-full mx-auto'
className="mx-auto w-full"
name={name || 'Documenso'}
signature={signature}
signingCelebrationImage={signingCelebration}
/>
</div>
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
<Trans>The document is now completed, please follow any instructions provided within the parent application.</Trans>
<p className="text-muted-foreground mt-8 max-w-[50ch] text-center text-sm">
<Trans>
The document is now completed, please follow any instructions provided within the parent
application.
</Trans>
</p>
</div>
);

View File

@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
@ -14,7 +15,7 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { FieldType, type DocumentData, type Field } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
@ -34,7 +35,6 @@ import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { Logo } from '~/components/branding/logo';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields';
@ -307,7 +307,7 @@ export const EmbedDirectTemplateClientPage = ({
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
@ -318,26 +318,26 @@ export const EmbedDirectTemplateClientPage = ({
{/* Widget */}
<div
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="w-full border-border bg-widget flex md:min-h-[min(calc(100dvh-2rem),48rem)] h-fit flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
@ -354,7 +354,7 @@ export const EmbedDirectTemplateClientPage = ({
</div>
{/* Form */}
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
@ -408,9 +408,9 @@ export const EmbedDirectTemplateClientPage = ({
</div>
</div>
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>

View File

@ -1,3 +1,3 @@
export default function EmbedDirectTemplateNotFound() {
return <div>Not Found</div>
return <div>Not Found</div>;
}

View File

@ -73,11 +73,7 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
return (
<SigningProvider
email={user?.email}
fullName={user?.name}
signature={user?.signature}
>
<SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentAuthProvider
documentAuthOptions={template.authOptions}
recipient={recipient}

View File

@ -13,7 +13,7 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { FieldType, type Field } from '@documenso/prisma/client';
import { type Field, FieldType } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type {
TRemovedSignedFieldWithTokenMutationSchema,

View File

@ -1,5 +1,7 @@
export const EmbedPaywall = () => {
return <div>
<h1>Paywall</h1>
</div>
}
return (
<div>
<h1>Paywall</h1>
</div>
);
};

View File

@ -1,15 +1,17 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useEffect, useState } from 'react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -20,9 +22,9 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields';
@ -185,7 +187,7 @@ export const EmbedSignDocumentClientPage = ({
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
@ -196,26 +198,26 @@ export const EmbedSignDocumentClientPage = ({
{/* Widget */}
<div
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="w-full border-border bg-widget flex flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
@ -232,7 +234,7 @@ export const EmbedSignDocumentClientPage = ({
</div>
{/* Form */}
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
@ -285,9 +287,9 @@ export const EmbedSignDocumentClientPage = ({
</div>
</div>
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>

View File

@ -1,3 +1,3 @@
export default function EmbedDirectTemplateNotFound() {
return <div>Not Found</div>
return <div>Not Found</div>;
}

View File

@ -4,8 +4,12 @@ import { match } from 'ts-pattern';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus } from '@documenso/prisma/client';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
@ -13,10 +17,6 @@ import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall';
import { EmbedSignDocumentClientPage } from './client';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { DocumentStatus } from '@documenso/prisma/client';
export type EmbedSignDocumentPageProps = {
params: {
@ -66,7 +66,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
.exhaustive();
if (!isAccessAuthValid) {
return <EmbedAuthenticateView email={user?.email || recipient.email} returnTo={`/embed/direct/${token}`} />;
return (
<EmbedAuthenticateView
email={user?.email || recipient.email}
returnTo={`/embed/direct/${token}`}
/>
);
}
return (

View File

@ -1,7 +1,6 @@
import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { cookies, headers } from 'next/headers';
import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env';
@ -9,12 +8,8 @@ import { PublicEnvScript } from 'next-runtime-env';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { IS_APP_WEB_I18N_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
@ -61,32 +56,7 @@ export function generateMetadata() {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags();
const locale = getLocale();
let overrideLang: (typeof SUPPORTED_LANGUAGE_CODES)[number] | undefined;
// Should be safe to remove when we upgrade NextJS.
// https://github.com/vercel/next.js/pull/65008
// Currently if the middleware sets the cookie, it's not accessible in the cookies
// during the same render.
// So we go the roundabout way of checking the header for the set-cookie value.
if (!cookies().get('i18n')) {
const setCookieValue = headers().get('set-cookie');
const i18nCookie = setCookieValue?.split(';').find((cookie) => cookie.startsWith('i18n='));
if (i18nCookie) {
const i18n = i18nCookie.split('=')[1];
overrideLang = ZSupportedLanguageCodeSchema.parse(i18n);
}
}
// Disable i18n for now until we get translations.
if (!IS_APP_WEB_I18N_ENABLED) {
overrideLang = 'en';
}
const { lang, i18n } = setupI18nSSR(overrideLang);
const { i18n, lang, locales } = setupI18nSSR();
return (
<html
@ -110,21 +80,22 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</Suspense>
<body>
<LocaleProvider locale={locale}>
<FeatureFlagProvider initialFlags={flags}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>
<I18nClientProvider initialLocale={lang} initialMessages={i18n.messages}>
{children}
</I18nClientProvider>
</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
<FeatureFlagProvider initialFlags={flags}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>
<I18nClientProvider
initialLocaleData={{ lang, locales }}
initialMessages={i18n.messages}
>
{children}
</I18nClientProvider>
</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
<Toaster />
</FeatureFlagProvider>
</body>
</html>
);

View File

@ -7,10 +7,11 @@ import { useRouter } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import {
DOCUMENTS_PAGE_SHORTCUT,
SETTINGS_PAGE_SHORTCUT,
@ -20,7 +21,10 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import {
CommandDialog,
CommandEmpty,
@ -31,6 +35,7 @@ import {
CommandShortcut,
} from '@documenso/ui/primitives/command';
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
import { useToast } from '@documenso/ui/primitives/use-toast';
const DOCUMENTS_PAGES = [
{
@ -207,6 +212,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
<Commands push={push} pages={SETTINGS_PAGES} />
</CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
Change language
</CommandItem>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
Change theme
</CommandItem>
@ -218,7 +226,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
)}
</>
)}
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
{currentPage === 'language' && <LanguageCommands />}
</CommandList>
</CommandDialog>
);
@ -269,3 +279,46 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
</CommandItem>
));
};
const LanguageCommands = () => {
const { i18n, _ } = useLingui();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const setLanguage = async (lang: string) => {
if (isLoading || lang === i18n.locale) {
return;
}
setIsLoading(true);
try {
await dynamicActivate(i18n, lang);
await switchI18NLanguage(lang);
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(msg`Unable to change the language at this time. Please try again later.`),
});
}
setIsLoading(false);
};
return Object.values(SUPPORTED_LANGUAGES).map((language) => (
<CommandItem
disabled={isLoading}
key={language.full}
onSelect={async () => setLanguage(language.short)}
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
>
<CheckIcon
className={cn('mr-2 h-4 w-4', i18n.locale === language.short ? 'opacity-100' : 'opacity-0')}
/>
{language.full}
</CommandItem>
));
};

View File

@ -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)}
/>
);
};

View File

@ -1,5 +1,7 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
@ -17,6 +19,7 @@ import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
import { cn } from '@documenso/ui/lib/utils';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -41,6 +44,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
const pathname = usePathname();
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
const isUserAdmin = isAdmin(user);
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
@ -274,6 +279,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-muted-foreground px-4 py-2"
onClick={() => setLanguageSwitcherOpen(true)}
>
<Trans>Language</Trans>
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive/90 hover:!text-destructive px-4 py-2"
onSelect={async () =>
@ -285,6 +297,8 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<Trans>Sign Out</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
</DropdownMenu>
);
};

View File

@ -22,12 +22,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
export const CurrentUserTeamsDataTable = () => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -91,7 +89,7 @@ export const CurrentUserTeamsDataTable = () => {
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
id: 'actions',

View File

@ -18,13 +18,11 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
export const PendingUserTeamsDataTable = () => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -79,7 +77,7 @@ export const PendingUserTeamsDataTable = () => {
{
header: _(msg`Created on`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
id: 'actions',

View File

@ -27,8 +27,6 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { LocaleDate } from '~/components/formatter/locale-date';
export type TeamMemberInvitesDataTableProps = {
teamId: number;
};
@ -37,7 +35,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const { _ } = useLingui();
const { _, i18n } = useLingui();
const { toast } = useToast();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
@ -129,7 +127,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
{
header: _(msg`Invited At`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: _(msg`Actions`),

View File

@ -29,8 +29,6 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
@ -47,7 +45,7 @@ export const TeamMembersDataTable = ({
teamId,
teamName,
}: TeamMembersDataTableProps) => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -114,7 +112,7 @@ export const TeamMembersDataTable = ({
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: _(msg`Actions`),

View File

@ -3,7 +3,9 @@
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ArrowRightIcon, Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
@ -18,8 +20,6 @@ import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
export type DocumentHistorySheetProps = {
@ -37,6 +37,8 @@ export const DocumentHistorySheet = ({
onMenuOpenChange,
children,
}: DocumentHistorySheetProps) => {
const { i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
const {
@ -153,7 +155,9 @@ export const DocumentHistorySheet = ({
{formatDocumentAuditLogActionString(auditLog, userId)}
</p>
<p className="text-foreground/50 text-xs">
<LocaleDate date={auditLog.createdAt} format="d MMM, yyyy HH:MM a" />
{DateTime.fromJSDate(auditLog.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('d MMM, yyyy HH:MM a')}
</p>
</div>
</div>
@ -331,6 +335,23 @@ export const DocumentHistorySheet = ({
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.exhaustive()}
{isUserDetailsVisible && (

View File

@ -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,
})}

View File

@ -1,49 +0,0 @@
'use client';
import type { HTMLAttributes } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
export type LocaleDateProps = HTMLAttributes<HTMLSpanElement> & {
date: string | number | Date;
format?: DateTimeFormatOptions | string;
};
/**
* Formats the date based on the user locale.
*
* Will use the estimated locale from the user headers on SSR, then will use
* the client browser locale once mounted.
*/
export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
const { locale } = useLocale();
const formatDateTime = useCallback(
(date: DateTime) => {
if (typeof format === 'string') {
return date.toFormat(format);
}
return date.toLocaleString(format);
},
[format],
);
const [localeDate, setLocaleDate] = useState(() =>
formatDateTime(DateTime.fromJSDate(new Date(date)).setLocale(locale)),
);
useEffect(() => {
setLocaleDate(formatDateTime(DateTime.fromJSDate(new Date(date))));
}, [date, format, formatDateTime]);
return (
<span className={className} {...props}>
{localeDate}
</span>
);
};

View File

@ -1,6 +1,6 @@
'use client';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
@ -307,6 +307,18 @@ export const SignInForm = ({
}
};
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
const email = params.get('email');
if (email) {
form.setValue('email', email);
}
}, [form]);
return (
<Form {...form}>
<form

View File

@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
@ -203,6 +203,18 @@ export const SignUpFormV2 = ({
}
};
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
const email = params.get('email');
if (email) {
form.setValue('email', email);
}
}, [form]);
return (
<div className={cn('flex justify-center gap-x-12', className)}>
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">

View File

@ -52,8 +52,6 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
import { LocaleDate } from '../formatter/locale-date';
export type ManagePublicTemplateDialogProps = {
directTemplates: (Template & {
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
@ -93,7 +91,7 @@ export const ManagePublicTemplateDialog = ({
onIsOpenChange,
...props
}: ManagePublicTemplateDialogProps) => {
const { _ } = useLingui();
const { _, i18n } = useLingui();
const { toast } = useToast();
const [open, onOpenChange] = useState(isOpen);
@ -300,7 +298,7 @@ export const ManagePublicTemplateDialog = ({
</TableCell>
<TableCell className="text-muted-foreground text-sm">
<LocaleDate date={row.createdAt} />
{i18n.date(row.createdAt)}
</TableCell>
<TableCell>

View File

@ -5,7 +5,6 @@ import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
import { extractSupportedLanguage } from '@documenso/lib/utils/i18n';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
async function middleware(req: NextRequest): Promise<NextResponse> {
@ -82,7 +81,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
// Allow third parties to iframe the document.
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('Content-Security-Policy', "frame-ancestors *");
res.headers.set('Content-Security-Policy', 'frame-ancestors *');
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
res.headers.set('X-Content-Type-Options', 'nosniff');
res.headers.set('X-Frame-Options', 'ALLOW-ALL');
@ -96,12 +95,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
export default async function middlewareWrapper(req: NextRequest) {
const response = await middleware(req);
const lang = extractSupportedLanguage({
headers: req.headers,
cookies: cookies(),
});
response.cookies.set('i18n', lang);
// Can place anything that needs to be set on the response here.
return response;
}

Some files were not shown because too many files have changed in this diff Show More