Compare commits

...

29 Commits

Author SHA1 Message Date
Lucas Smith 191170923a v2.11.0 2026-05-13 22:21:57 +10:00
Lucas Smith 4078c6b46d chore: add translations (#2805) 2026-05-13 17:06:56 +10:00
github-actions[bot] abbca79b48 chore: extract translations (#2804) 2026-05-13 16:34:21 +10:00
Gaurav goswami d6dd2b3292 perf: compress signing-celebration.png from 20MB to 4MB (#2781) 2026-05-13 15:46:32 +10:00
David Nguyen cfaad6efc9 feat: add admin org deletion (#2795) 2026-05-13 15:28:27 +10:00
github-actions[bot] 9a45b3564f chore: extract translations (#2796) 2026-05-13 15:20:04 +10:00
David Nguyen 8b171c9a30 chore: update docs to use editor instead of authoring (#2800)
## Description

Update docs to use the term "Editor" instead of "Authoring" to reduce
confusion.
2026-05-13 15:17:55 +10:00
Lucas Smith a8efb6f495 fix: remove translation tag from css textarea placeholder (#2803) 2026-05-13 15:17:34 +10:00
Lucas Smith bc184d445f feat: support DOCX uploads via Gotenberg (#2801)
Uploaded .docx files are converted to PDF on the server using a
Gotenberg
sidecar before entering the normal envelope pipeline. The feature is
opt-in via NEXT_PRIVATE_DOCUMENT_CONVERSION_URL; when unset, only PDF
uploads are accepted.

A per-process circuit breaker opens for 30s after a conversion failure
to shed load.

Ships a dev Dockerfile that layers Microsoft Core Fonts and additional
language fonts
onto the upstream Gotenberg image for better fidelity.

Co-authored-by: Ephraim Duncan
<55143799+ephraimduncan@users.noreply.github.com>

Co-authored-by: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com>
2026-05-13 15:06:21 +10:00
David Nguyen 8dfd548c08 chore: remove github action caches (#2802) 2026-05-13 15:06:06 +10:00
Anish Patil 73a7335c89 refactor: remove unnecessary DateRange type assertion (#2790) 2026-05-13 13:11:13 +10:00
Timur Ercan be3e45427f chore: update fair use policy (#2798)
## Description

refined fair use policy with examples and guidelines
2026-05-12 14:58:39 +02:00
Abdelhamid Henni 57eb40d6aa chore: update French translations (#2717) 2026-05-12 15:29:52 +10:00
David Nguyen 684fab1909 chore: add section on personal organisations for SSO users (#2793) 2026-05-12 15:20:05 +10:00
Lucas Smith d794ceb8da chore: add translations (#2788) 2026-05-12 14:28:10 +10:00
github-actions[bot] 87315adb0f chore: extract translations (#2786) 2026-05-11 17:27:06 +10:00
Ephraim Duncan 0a7794be61 feat: protect signing URLs from indexing, caching, and embedding (#2469) 2026-05-11 17:24:58 +10:00
Ephraim Duncan f15d6f0150 perf: dynamically import posthog (#2622) 2026-05-11 15:58:15 +10:00
Lucas Smith 0b86ece1d5 feat: add custom branding for signing pages (#2785)
Platform-plan organisations and teams can now customise non-embed
signing pages with six brand colour tokens, a border-radius, and
a free-text custom CSS block (up to 256 KB).

- Stored on OrganisationGlobalSettings / TeamGlobalSettings;
  teams inherit from the org via brandingEnabled === null.
- CSS is sanitised on save (PostCSS) so we can inline it at SSR
  with no per-render parsing.
- Rendered via a nonce'd <style> scoped under .documenso-branded,
  using native CSS nesting so user selectors don't need scoping.
- Gated on the existing embedSigningWhiteLabel claim (or
  self-hosted) — reuses the embed white-label decision.
2026-05-11 13:03:02 +10:00
Ephraim Duncan a197bf113f feat: add granular signup disable flags (#2765) 2026-05-09 01:16:13 +00:00
Lucas Smith ec8728b33e chore: add translations (#2774) 2026-05-08 16:22:32 +10:00
github-actions[bot] 22122f51da chore: extract translations (#2772) 2026-05-08 16:22:08 +10:00
David Nguyen 8671f269e8 fix: lint project (#2693) 2026-05-08 16:04:22 +10:00
David Nguyen edbf65969b fix: replace linter with biome (#2645) 2026-05-08 15:40:31 +10:00
David Nguyen 207135d6f3 feat: add new field overflow methods (#2715) 2026-05-08 15:14:27 +10:00
Lucas Smith 4877d1964a chore: add translations (#2771) 2026-05-07 15:32:14 +10:00
Lucas Smith f66751668a fix: paginate and search member/group pickers (#2768) 2026-05-07 15:03:38 +10:00
github-actions[bot] bc3aa9c858 chore: extract translations (#2737) 2026-05-07 11:39:39 +10:00
Catalin Pit b79b4bd111 feat: add DD-MM-YYYY date format variants (#2767) 2026-05-06 23:34:05 +10:00
1535 changed files with 23676 additions and 28468 deletions
@@ -0,0 +1,138 @@
---
date: 2026-05-06
title: Platform Signing Page Branding
---
## What
Platform-plan organisations (and their teams) can customise the **non-embed
signing pages** (`/sign/:token`, `/d/:token`, and the sibling
complete/expired/rejected/waiting pages) with:
- Six brand colour tokens (background, foreground, primary, primary-foreground,
border, ring) plus a border-radius length.
- A free-text custom CSS block (up to 256 KB).
Settings live on `OrganisationGlobalSettings` and `TeamGlobalSettings`. Teams
inherit from the org via the existing `brandingEnabled === null` mechanism.
## Why
- Embed customers already have white-label CSS; Platform customers want the
same coverage on direct signing URLs that they iframe or link to.
- Persisting on org/team (not per envelope) means it's set-and-forget.
- Sanitising **on save** lets us inline the verbatim string at SSR — no
per-render parsing cost, no `<style>.innerHTML` injection on the client.
- Reusing the existing `embedSigningWhiteLabel` claim flag keeps "if you can
white-label an embed, you can white-label this" as one decision.
## How
### Storage (`packages/prisma/schema.prisma`)
Two new fields on each settings model. No new tables.
| Field | Org type | Team type |
| ---------------- | ------------------ | ------------------ |
| `brandingColors` | `Json?` (nullable) | `Json?` (nullable) |
| `brandingCss` | `String @default("")` | `String?` |
Colours are validated against `ZCssVarsSchema`. The team's `null` means
"inherit"; an empty colour object is collapsed to `null` server-side so a
team toggling `brandingEnabled = true` without filling in colours doesn't
silently override the org's defaults with nothing.
### Sanitiser (`packages/lib/utils/sanitize-branding-css.ts`)
PostCSS + `postcss-selector-parser`. Runs on save only.
- Drops selectors containing `::before`/`::after`/`::backdrop`/`::marker` or
the universal `*`.
- Drops integrity-breaking properties (`display`, `position`, `transform`,
layout-affecting dimensions, text-hiding properties).
- Drops declaration values containing `url(`, `expression(`, `@import`,
`javascript:`.
- Strips `!important`.
- Allows `@media` only; drops other at-rules.
- **Does not** rewrite selectors. Scoping happens at render time via native
CSS nesting under `.documenso-branded { ... }`.
- Final-pass tripwire: if a literal `</style` somehow survives serialization,
reject the entire output. PostCSS already escapes `<` to `\3c` whenever it
would form `</...`; the explicit check is belt-and-braces in case a future
serializer regresses.
- Returns `{ css, warnings[] }`. Warnings are surfaced in the UI.
Border-radius is the only token interpolated raw into a `<style>` block; it
is regex-validated (`CSS_LENGTH_REGEX`) at both the Zod schema and the
runtime `toNativeCssVars` call. Belt-and-braces against schema drift.
### Render (`apps/remix/app/components/general/recipient-branding.tsx`)
Each recipient loader calls `loadRecipientBrandingByTeamId` and threads the
payload through to `<RecipientBranding>`, which emits a single
nonce-attributed `<style>`:
```
.documenso-branded {
--background: ...; ...
<user css>
}
```
Native CSS nesting expands user rules under the wrapper. The body class is
applied unconditionally to recipient routes in `root.tsx` via `useMatches()`
so portaled Radix content (dialogs, popovers, tooltips, dropdowns) inherits
the scope.
CSP for recipient routes already supports `<style nonce>`; no policy
changes needed.
### Plan gate
`organisationClaim.flags.embedSigningWhiteLabel || !IS_BILLING_ENABLED()`.
Self-hosted instances always allow. The outer paywall for logo/URL/details
stays on `allowCustomBranding` (Team plan and up); only the new
colour/CSS section is Platform-only.
### UI (`apps/remix/app/components/forms/branding-preferences-form.tsx`)
Extends the existing branding form. Six `<ColorPicker showHex>` (rewritten
to use the native `<input type="color">` instead of `react-colorful`, which
was removed) in a 2-col grid, plus a free-text radius input and an
`<Accordion>` revealing a mono `<Textarea>`. Defaults come from
`packages/lib/constants/theme.ts` (light-mode hex mirror of `theme.css`).
Warnings from the sanitiser are surfaced in an `<Alert variant="warning">`
after save, and the `brandingCss` textarea is re-synced from the persisted
value so the user sees exactly what was stored. Other fields are
deliberately NOT reset on settings refetch — that would clobber in-flight
edits.
### TRPC
`update-organisation-settings` and `update-team-settings` accept the new
fields, run them through `sanitizeBrandingCss` + `normalizeBrandingColors`,
and return any sanitiser warnings to the client. The team route treats
`null` as "inherit"; an empty post-sanitisation string is collapsed to
`null` (team) so an empty override doesn't mask the org's CSS.
## Known accepted limitations
- The sanitiser does not prevent hostile-but-syntactically-valid CSS
(`color: transparent`, low-contrast values, etc.). The customer is
branding **their own** signing pages — we focus on integrity (no
overlay/hide/exfiltrate), not aesthetic policing.
- User rules targeting `body`/`html`/`:root` no-op once nested under the
wrapper class. Documented for users.
- CSS nesting baseline is Chrome 120+ / Firefox 117+ / Safari 16.5+.
Acceptable for the Platform-tier audience.
- No automated `theme.css``theme.ts` sync check; fat comment in
`theme.ts` reminds devs to update both.
- Per-section team inherit is coarse — `brandingEnabled = null` inherits
everything from the org. Per-field inherit toggles are deferred.
## Out of scope
Live preview, embed-route sanitiser unification, email/PDF certificate
branding, custom font upload, the full ~30 colour tokens in the picker UI,
wiring `hidePoweredBy` through to the actual footer.
+23 -1
View File
@@ -160,8 +160,16 @@ NEXT_PRIVATE_REDIS_PREFIX="documenso"
NEXT_PUBLIC_POSTHOG_KEY=""
# OPTIONAL: Leave blank to disable billing.
NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
# OPTIONAL: Leave blank to allow users to signup through /signup page.
# OPTIONAL: Set to "true" to disable all signup methods (email, Google, Microsoft, OIDC, including the organisation OIDC portal).
NEXT_PUBLIC_DISABLE_SIGNUP=
# OPTIONAL: Set to "true" to disable email/password signup only.
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through Google. Existing linked users can still sign in.
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through Microsoft. Existing linked users can still sign in.
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through OIDC (including the organisation portal).
NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
@@ -203,3 +211,17 @@ NEXT_PRIVATE_LOGGER_FILE_PATH=
# [[PLAIN SUPPORT]]
NEXT_PRIVATE_PLAIN_API_KEY=
# [[DOCUMENT CONVERSION]]
# OPTIONAL: Base URL of a Gotenberg-compatible service used to convert uploaded
# DOCX files to PDF on the server. When unset, DOCX uploads are disabled and
# only PDF is accepted. The dev docker compose exposes Gotenberg on port 3005.
# NEXT_PRIVATE_DOCUMENT_CONVERSION_URL="http://localhost:3005"
# OPTIONAL: Per-request timeout in milliseconds for the conversion service.
# Defaults to 30000 (30s) if unset.
# NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS=30000
# OPTIONAL: HTTP Basic auth credentials for the conversion service. Set both
# when the service is started with `--api-enable-basic-auth` (the dev compose
# does this; the matching values there are `documenso` / `password`).
# NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
# NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=password
-8
View File
@@ -1,8 +0,0 @@
# Config files
*.config.js
*.config.cjs
# Statically hosted javascript files
apps/*/public/*.js
apps/*/public/*.cjs
scripts/
-16
View File
@@ -1,16 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@documenso/eslint-config'],
rules: {
'@next/next/no-img-element': 'off',
'no-unreachable': 'error',
'react-hooks/exhaustive-deps': 'off',
},
settings: {
next: {
rootDir: ['apps/*/'],
},
},
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
};
+1 -19
View File
@@ -1,4 +1,4 @@
name: 'Setup node and cache node_modules'
name: 'Setup node'
inputs:
node_version:
required: false
@@ -16,25 +16,7 @@ runs:
shell: bash
run: corepack enable npm
- name: Cache npm
uses: actions/cache@v3
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: modules-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
run: |
npm ci --no-audit
+1 -12
View File
@@ -1,19 +1,8 @@
name: Install playwright binaries
description: 'Install playwright, cache and restore if necessary'
description: 'Install playwright'
runs:
using: 'composite'
steps:
- name: Cache playwright
id: cache-playwright
uses: actions/cache@v3
with:
path: |
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: playwright-${{ hashFiles('**/package-lock.json') }}
restore-keys: playwright-
- name: Install playwright
if: steps.cache-playwright.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
shell: bash
-18
View File
@@ -41,14 +41,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build Docker Image
uses: docker/build-push-action@v5
with:
@@ -56,13 +48,3 @@ jobs:
context: .
file: ./docker/Dockerfile
tags: documenso-${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
@@ -20,7 +20,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '18'
cache: npm
- name: Install Octokit
run: npm install @octokit/rest@18
+1 -1
View File
@@ -1,7 +1,7 @@
name: 'PR Labeler'
on:
- pull_request_target
- pull_request
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
-1
View File
@@ -20,7 +20,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '18'
cache: npm
- name: Install Octokit
run: npm install @octokit/rest@18
+1 -1
View File
@@ -1,7 +1,7 @@
name: 'Validate PR Name'
on:
pull_request_target:
pull_request:
types:
- opened
- reopened
-20
View File
@@ -1,20 +0,0 @@
node_modules
.next
public
**/**/node_modules
**/**/.next
**/**/public
packages/lib/translations/**/*.js
*.lock
*.log
*.test.ts
.gitignore
.npmignore
.prettierignore
.DS_Store
.eslintignore
# Docs MDX - Prettier strips indentation from code blocks inside components
apps/docs/content/**/*.mdx
+18 -6
View File
@@ -1,11 +1,11 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true,
@@ -15,8 +15,20 @@
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"prisma.pinToPrisma6": true
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"prisma.pinToPrisma6": true,
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
- `npm run test:e2e` - Run E2E tests with Playwright
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
- `npm run format` - Format code with Prettier
- `npm run format` - Format code with Biome
- `npm run dev` - Start development server for Remix app
**Important:** Do not run `npm run build` to verify changes unless explicitly asked. Builds take a long time (~2 minutes). Use `npx tsc --noEmit` for type checking specific packages if needed.
-2
View File
@@ -65,8 +65,6 @@ Documenso is an open-source document signing platform built as a **monorepo** us
| Package | Description |
| ---------------------------- | ------------------------- |
| `@documenso/app-tests` | E2E tests (Playwright) |
| `@documenso/eslint-config` | Shared ESLint config |
| `@documenso/prettier-config` | Shared Prettier config |
| `@documenso/tailwind-config` | Shared Tailwind config |
| `@documenso/tsconfig` | Shared TypeScript configs |
+1 -1
View File
@@ -10,4 +10,4 @@
"baseDir": "src",
"uiLibrary": "radix-ui",
"commands": {}
}
}
+1 -7
View File
@@ -1,10 +1,4 @@
{
"title": "Concepts",
"pages": [
"document-lifecycle",
"recipient-roles",
"field-types",
"signing-workflow",
"signing-certificates"
]
"pages": ["document-lifecycle", "recipient-roles", "field-types", "signing-workflow", "signing-certificates"]
}
@@ -1,13 +1,4 @@
{
"title": "API Reference",
"pages": [
"documents",
"recipients",
"fields",
"templates",
"teams",
"rate-limits",
"versioning",
"developer-mode"
]
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
}
@@ -1,24 +1,24 @@
---
title: Authoring
title: Editor
description: Embed document, template, and envelope creation directly in your application.
---
import { Callout } from 'fumadocs-ui/components/callout';
In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents, templates, and envelopes without leaving your application.
In addition to embedding signing, Documenso supports embedded editor. It allows your users to create and edit documents, templates, and envelopes without leaving your application.
<Callout type="warn">
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
Contact sales for access.
</Callout>
## Versions
Embedded authoring is available in two versions:
Embedded editor is available in two versions:
- **[V1 Authoring](/docs/developers/embedding/authoring/v1)** — Works with V1 Documents and Templates.
- **[V2 Authoring](/docs/developers/embedding/authoring/v2)** — Works with Envelopes, which are the unified model for documents and templates.
- **[V1 Editor](/docs/developers/embedding/editor/v1)** — Works with V1 Documents and Templates.
- **[V2 Editor](/docs/developers/embedding/editor/v2)** — Works with Envelopes, which are the unified model for documents and templates.
### Comparison
@@ -32,7 +32,7 @@ Embedded authoring is available in two versions:
## Presign Tokens
Before using any authoring component, obtain a presign token from your backend:
Before using any editor component, obtain a presign token from your backend:
```
POST /api/v2/embedding/create-presign-token
@@ -50,7 +50,7 @@ See the [API documentation](https://openapi.documenso.com/reference#tag/embeddin
## Next Steps
- [V1 Authoring](/docs/developers/embedding/authoring/v1) — Create and edit documents and templates using V1 components
- [V2 Authoring](/docs/developers/embedding/authoring/v2) — Create and edit envelopes using V2 components
- [V1 Editor](/docs/developers/embedding/editor/v1) — Create and edit documents and templates using V1 components
- [V2 Editor](/docs/developers/embedding/editor/v2) — Create and edit envelopes using V2 components
- [CSS Variables](/docs/developers/embedding/css-variables) — Customize the appearance of embedded components
- [SDKs](/docs/developers/embedding/sdks) — Framework-specific SDK documentation
@@ -1,4 +1,4 @@
{
"title": "Authoring",
"title": "Editor",
"pages": ["v1", "v2"]
}
@@ -1,21 +1,21 @@
---
title: V1 Authoring
title: V1 Editor
description: Embed V1 document and template creation directly in your application.
---
import { Callout } from 'fumadocs-ui/components/callout';
V1 authoring components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
V1 editor components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
<Callout type="warn">
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
Contact sales for access.
</Callout>
## Components
The SDK provides four V1 authoring components:
The SDK provides four V1 editor components:
| Component | Purpose |
| ----------------------- | ----------------------- |
@@ -29,7 +29,7 @@ The SDK provides four V1 authoring components:
## Presign Tokens
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens.
<Callout type="warn">
@@ -131,7 +131,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
## Props
### All Authoring Components
### All Editor Components
| Prop | Type | Required | Description |
| ------------------ | --------- | -------- | -------------------------------------------------------- |
@@ -143,7 +143,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
| `className` | `string` | No | CSS class for the iframe |
| `features` | `object` | No | Feature toggles for the authoring experience |
| `features` | `object` | No | Feature toggles for the editor experience |
### Update Components Only
@@ -157,7 +157,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
## Feature Toggles
Customize what options are available in the authoring experience:
Customize what options are available in the editor experience:
```jsx
<EmbedCreateDocumentV1
@@ -294,7 +294,7 @@ Pass extra props to the iframe for testing experimental features:
## See Also
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
- [V2 Authoring](/docs/developers/embedding/authoring/v2) - V2 envelope authoring
- [V2 Editor](/docs/developers/embedding/editor/v2) - V2 envelope editor
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Documents API](/docs/developers/api/documents) - Create documents via API
- [Templates API](/docs/developers/api/templates) - Create templates via API
@@ -1,21 +1,21 @@
---
title: V2 Authoring
title: V2 Editor
description: Embed envelope creation and editing directly in your application.
---
import { Callout } from 'fumadocs-ui/components/callout';
V2 authoring components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API.
V2 editor components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API.
<Callout type="warn">
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
Contact sales for access.
</Callout>
## Components
The SDK provides two V2 authoring components:
The SDK provides two V2 editor components:
| Component | Purpose |
| ---------------------- | ------------------------ |
@@ -26,7 +26,7 @@ The SDK provides two V2 authoring components:
## Presign Tokens
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens.
<Callout type="warn">
A presigned token is NOT an API token
@@ -100,7 +100,7 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => {
## Props
### All V2 Authoring Components
### All V2 Editor Components
| Prop | Type | Required | Description |
| ---------------- | --------- | -------- | -------------------------------------------------------- |
@@ -113,7 +113,7 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => {
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
| `className` | `string` | No | CSS class for the iframe |
| `user` | `object` | No | Current user info. When provided, enables the "Add Myself" button in the recipients list. Object with optional `email` and `name` fields |
| `features` | `object` | No | Feature toggles for the authoring experience |
| `features` | `object` | No | Feature toggles for the editor experience |
### Create Component Only
@@ -132,7 +132,7 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => {
## Feature Toggles
V2 authoring provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the authoring experience — any omitted fields will use their defaults.
V2 editor provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the editor experience — any omitted fields will use their defaults.
```jsx
<EmbedCreateEnvelope
@@ -160,7 +160,7 @@ V2 authoring provides rich, structured feature toggles organized into sections.
### General
Controls the overall authoring flow and UI:
Controls the overall editor flow and UI:
| Property | Type | Default | Description |
| ------------------------------- | --------- | ------- | ------------------------------------------------ |
@@ -188,7 +188,7 @@ Controls envelope configuration options. Set to `null` to hide envelope settings
### Actions
Controls available actions during authoring:
Controls available actions during editing:
| Property | Type | Default | Description |
| ------------------ | --------- | ------- | ------------------------ |
@@ -221,7 +221,7 @@ Controls recipient configuration options. Set to `null` to prevent any recipient
### Disabling Steps
You can also disable entire steps of the authoring flow. This allows you to skip steps that are not relevant to your use case:
You can also disable entire steps of the editor flow. This allows you to skip steps that are not relevant to your use case:
```jsx
<EmbedCreateEnvelope
@@ -338,7 +338,7 @@ const EnvelopeManager = ({ presignToken }) => {
## See Also
- [Authoring Overview](/docs/developers/embedding/authoring) - V1 vs V2 comparison and presign tokens
- [V1 Authoring](/docs/developers/embedding/authoring/v1) - V1 document and template authoring
- [Editor Overview](/docs/developers/embedding/editor) - V1 vs V2 comparison and presign tokens
- [V1 Editor](/docs/developers/embedding/editor/v1) - V1 document and template editor
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
@@ -6,14 +6,14 @@ description: Embed document signing experiences directly in your application usi
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
## Embedded Signing vs Embedded Authoring
## Embedded Signing vs Embedded Editor
Documenso offers two types of embedding:
- **Embedded Signing** lets you embed the signing experience in your application. Your users sign documents without leaving your site. Available on Teams Plan and above.
- **Embedded Authoring** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Authoring](/docs/developers/embedding/authoring) guide.
- **Embedded Editor** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Editor](/docs/developers/embedding/editor) guide.
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Authoring](/docs/developers/embedding/authoring).
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Editor](/docs/developers/embedding/editor).
---
@@ -229,9 +229,9 @@ Receives an object with:
href="/docs/developers/embedding/css-variables"
/>
<Card
title="Authoring"
title="Editor"
description="Embed document and template creation."
href="/docs/developers/embedding/authoring"
href="/docs/developers/embedding/editor"
/>
</Cards>
@@ -1,4 +1,4 @@
{
"title": "Embedding",
"pages": ["sdks", "direct-links", "css-variables", "authoring"]
"pages": ["sdks", "direct-links", "css-variables", "editor"]
}
@@ -89,4 +89,4 @@ export class SigningComponent {
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -93,4 +93,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -133,4 +133,4 @@ const DocumentSigning = ({ token }: { token: string }) => {
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -93,4 +93,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -101,4 +101,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -104,4 +104,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
- [Editor](/docs/developers/embedding/editor) - Embed document creation
@@ -53,8 +53,8 @@ The Enterprise Edition is required when you:
- Document Action Reauthentication (Passkeys and 2FA)
- 21 CFR Part 11 Compliance
- Email Domains (custom sender addresses)
- Embed Authoring
- Embed Authoring White Label
- Embed Editor
- Embed Editor White Label
- Custom signing certificates
- Priority feature requests
+11 -8
View File
@@ -19,16 +19,19 @@ Use the limitless plans as much as you like. They are meant to offer a lot. Plea
### Do
- Sign as many documents as you need with the individual plan for your single business or organisation
- Use the API and automation tools to automate your signing workflows
- Experiment with plans and integrations while testing what you want to build
- Use team or platform plans to run your workflows, even with significant volume, as long as it aligns with the plans intended purpose.
- Experiment and automate freely within the plan features.
- If volume grows beyond whats sustainable on your plan, well reach out to discuss an upgrade.
- Assume that extreme usage will lead to us contacting you. You can scale up—or scale back. Its about finding the right fit.
### Don't
- Use an individual account API to power a platform or product
- Run a large company signing thousands of documents per day on a small team plan
- Expect enterprise-level support on a fair support plan
- Overthink this policy — if you are a paying customer, we want you to win
- Use an individual account's API to power a platform or product.
- Run a large company signing thousands of documents per day on a small team plan.
- Expect enterprise-level support on a fair support plan (i.e. business edition).
- Use a team plan to power an external platform or commercial product or platform beyond moderate testing.
- Expect a platform plan to support enterprise-level volumes indefinitely without a conversation.
- Dont expect the platform plan to cover enterprise-scale volume or support. If you reach that point, well reach out to guide you to the right fit.
- Dont overthink this if youre building something valuable, we want to see you succeed. If we need to talk, we will.
## Rate Limits
@@ -1,5 +1,5 @@
---
title: AI Recipient & Field Detection (Self-hosting)
title: AI Recipient & Field Detection
description: Configure Google Vertex AI so Documenso can detect recipients and fields automatically.
---
@@ -0,0 +1,408 @@
---
title: Document Conversion
description: Enable DOCX uploads on a self-hosted Documenso instance by running a Gotenberg sidecar that converts Word documents to PDF.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
Documenso can accept `.docx` uploads in addition to PDFs. When a user uploads a Word document, the Documenso server sends it to a [Gotenberg](https://gotenberg.dev) service which uses LibreOffice to convert it to PDF. The converted PDF is what gets stored, signed, and downloaded. The original DOCX is discarded.
This feature is **opt-in for self-hosted instances**. When the conversion service is not configured, DOCX uploads are rejected in the UI and only PDFs are accepted.
| Property | Value |
| ----------------------- | -------------------------------------------------------------------- |
| Conversion engine | [Gotenberg](https://gotenberg.dev) + LibreOffice |
| Input format | `.docx` (Office Open XML Word documents) |
| Output format | PDF |
| Network requirement | Documenso must reach the Gotenberg HTTP API |
| Default request timeout | 30 seconds per file |
| Failure handling | An internal circuit breaker opens for 30 seconds after a failure |
<Callout type="info">
Only `.docx` is accepted. Legacy `.doc`, `.odt`, `.rtf`, and other LibreOffice-supported formats
are rejected at the upload step even when Gotenberg is configured.
</Callout>
---
## Requirements
- A running Gotenberg 8 instance with the LibreOffice module (`gotenberg/gotenberg:8-libreoffice` or newer).
- Network reachability from the Documenso container to the Gotenberg HTTP API.
- A version of Documenso that includes the document conversion feature.
## Build the Gotenberg Image
The upstream `gotenberg/gotenberg:8-libreoffice` image works out of the box, but it ships only **metric-compatible font substitutes** (Carlito for Calibri, Liberation for Arial/Times/Courier). Layout widths are preserved but documents will look noticeably different from Word.
For better fidelity, especially for non-Latin scripts, build a derived image that adds Microsoft Core Fonts and additional language fonts. The Documenso repository ships a reference Dockerfile at [`docker/development/Dockerfile.gotenberg`](https://github.com/documenso/documenso/blob/main/docker/development/Dockerfile.gotenberg) that you can use as a starting point:
```dockerfile
FROM gotenberg/gotenberg:8-libreoffice
USER root
RUN echo "deb http://deb.debian.org/debian trixie contrib non-free" \
> /etc/apt/sources.list.d/contrib.list \
&& echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" \
| debconf-set-selections \
&& apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
ca-certificates \
ttf-mscorefonts-installer \
fonts-symbola \
fonts-noto-extra \
fonts-hosny-amiri \
fonts-thai-tlwg \
fonts-sil-padauk \
fonts-sarai \
fonts-samyak-taml \
culmus \
libfribidi0 \
libharfbuzz0b \
&& fc-cache -f \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
USER gotenberg
```
<Callout type="warn">
`ttf-mscorefonts-installer` accepts the Microsoft Core Fonts EULA on your behalf via debconf. By
installing this image you are agreeing to those licence terms. Review them before publishing the
image.
</Callout>
Build and publish the image to a registry you control:
```bash
docker build -t registry.example.com/documenso/gotenberg:8 \
-f Dockerfile.gotenberg .
docker push registry.example.com/documenso/gotenberg:8
```
If you do not need extra fonts, skip the build step entirely and reference `gotenberg/gotenberg:8-libreoffice` directly in the next section.
## Deploy the Service
The Gotenberg service should run **alongside** your Documenso container, not exposed to the public internet. The conversion service has no built-in authorisation beyond HTTP Basic auth, so it should sit on a private network or behind your existing reverse proxy.
<Tabs items={['Docker Compose', 'Kubernetes', 'External Instance']}>
<Tab value="Docker Compose">
Add a `gotenberg` service to the `compose.yml` you use for Documenso:
```yaml
services:
gotenberg:
image: registry.example.com/documenso/gotenberg:8
# Or use upstream directly:
# image: gotenberg/gotenberg:8-libreoffice
restart: unless-stopped
environment:
GOTENBERG_API_BASIC_AUTH_USERNAME: ${GOTENBERG_USERNAME}
GOTENBERG_API_BASIC_AUTH_PASSWORD: ${GOTENBERG_PASSWORD}
command:
- gotenberg
- --api-enable-basic-auth
- --libreoffice-deny-private-ips
- --api-timeout=500s
- --libreoffice-auto-start
- --libreoffice-start-timeout=300s
- --pdfengines-disable-routes
- --webhook-disable
healthcheck:
test: ['CMD', 'curl', '-fsS', 'http://localhost:3000/health']
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
documenso:
# existing config
environment:
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL: http://gotenberg:3000
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME: ${GOTENBERG_USERNAME}
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD: ${GOTENBERG_PASSWORD}
depends_on:
gotenberg:
condition: service_healthy
```
Do **not** publish Gotenberg's port (`3000`) to the host. Documenso reaches it over the internal Docker network using the service name (`http://gotenberg:3000`).
</Tab>
<Tab value="Kubernetes">
Create a Deployment, Service, and Secret. Example manifests:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: gotenberg-auth
namespace: documenso
stringData:
username: documenso
password: replace-me-with-a-strong-password
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gotenberg
namespace: documenso
spec:
replicas: 1
selector:
matchLabels: { app: gotenberg }
template:
metadata:
labels: { app: gotenberg }
spec:
containers:
- name: gotenberg
image: registry.example.com/documenso/gotenberg:8
args:
- gotenberg
- --api-enable-basic-auth
- --libreoffice-deny-private-ips
- --api-timeout=500s
- --libreoffice-auto-start
- --libreoffice-start-timeout=300s
- --pdfengines-disable-routes
- --webhook-disable
env:
- name: GOTENBERG_API_BASIC_AUTH_USERNAME
valueFrom: { secretKeyRef: { name: gotenberg-auth, key: username } }
- name: GOTENBERG_API_BASIC_AUTH_PASSWORD
valueFrom: { secretKeyRef: { name: gotenberg-auth, key: password } }
ports:
- containerPort: 3000
readinessProbe:
httpGet: { path: /health, port: 3000 }
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: gotenberg
namespace: documenso
spec:
selector: { app: gotenberg }
ports:
- port: 3000
targetPort: 3000
```
Then reference the in-cluster URL from Documenso's environment:
```
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg.documenso.svc.cluster.local:3000
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
```
</Tab>
<Tab value="External Instance">
Documenso does not have to colocate with Gotenberg. You can point it at any reachable Gotenberg deployment: a managed instance, a shared internal service, or a Gotenberg-compatible API.
```bash
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=https://gotenberg.internal.example.com
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
```
The remote instance must:
- Expose the LibreOffice route `/forms/libreoffice/convert`.
- Be reachable from the Documenso container with low enough latency that the 30 second per-request timeout is comfortable.
- Be on a private network or require authentication. Uploaded documents are sent to it as multipart form data and may contain sensitive content.
</Tab>
</Tabs>
## Recommended Gotenberg Flags
The flags in the examples above are not arbitrary. Each one matters for a production deployment.
| Flag | Why it matters |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--api-enable-basic-auth` | Requires HTTP Basic credentials on every API route. Without this, anyone with network access to the container can convert arbitrary documents. |
| `--libreoffice-deny-private-ips` | Rejects any outbound fetch LibreOffice tries to make to private, loopback, link-local, or cloud-metadata addresses while processing a document. Mitigates SSRF via malicious `.docx` files that embed `TargetMode="External"` references. Requires Gotenberg 8.32.0. |
| `--api-timeout=500s` | Server-side request ceiling. Documenso aborts at 30 s by default, so this is a safety net for very large documents. |
| `--libreoffice-auto-start` | Starts LibreOffice at container boot so the first request is not slow. |
| `--libreoffice-start-timeout=300s`| Allows LibreOffice up to 5 minutes to come up under load. |
| `--pdfengines-disable-routes` | Disables the PDF engines routes Documenso does not use. Shrinks the attack surface. |
| `--webhook-disable` | Disables webhook callbacks. Documenso uses synchronous requests only. |
## Configure Documenso
Set the following environment variables on the Documenso container and restart it.
### Required
| Variable | Description |
| ------------------------------------- | ---------------------------------------------------------------------- |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL`| Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Leave unset to disable the feature. |
### Optional
| Variable | Default | Description |
| ------------------------------------------- | ------- | -------------------------------------------------------------------------------------------- |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | | HTTP Basic auth username. Set when Gotenberg runs with `--api-enable-basic-auth`. |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | | HTTP Basic auth password. Set together with the username. |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS`| `30000` | Per-request timeout in milliseconds. Increase for very large documents. |
<Callout type="info">
When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is set, the public flag
`NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically on server start. You do not
need to set it yourself, and setting it manually has no effect.
</Callout>
### Example `.env` Snippet
```bash
# Document conversion (DOCX -> PDF)
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg:3000
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
# NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS=60000
```
## Verify the Setup
{/* prettier-ignore */}
<Steps>
<Step>
### Restart the Documenso container
Restart so the new environment variables are picked up.
</Step>
<Step>
### Confirm Gotenberg is healthy
From a shell inside the Documenso container or another container on the same network:
```bash
curl -fsS http://gotenberg:3000/health
```
The endpoint is exempt from basic auth and should return `200 OK`.
</Step>
<Step>
### Upload a test DOCX
In the Documenso web UI, open **Documents** and try uploading a small `.docx` file. The upload dropzone should accept it, and after a few seconds the editor should open with the converted PDF.
</Step>
<Step>
### Check the server logs
Successful conversions log a `document_conversion_attempt` event with `result: "success"`, the duration, and the file size. Failures log the same event with `result: "error"` and an error code (`CONVERSION_SERVICE_UNAVAILABLE`, `CONVERSION_FAILED`, or `UNSUPPORTED_FILE_TYPE`).
</Step>
</Steps>
## Security Considerations
- **Treat the conversion service as untrusted internal infrastructure.** Documents pass through Gotenberg in plain form. Run it on a private network and require HTTP Basic auth.
- **Run with `--libreoffice-deny-private-ips`.** Without this flag, a malicious `.docx` can trigger LibreOffice to fetch URLs from your internal network (SSRF).
- **Disable unused routes.** `--pdfengines-disable-routes` and `--webhook-disable` reduce attack surface. Documenso only uses the LibreOffice convert route.
- **Do not expose Gotenberg to the public internet.** Even with basic auth, this is a document-processing service with a non-trivial CPU and memory footprint; exposing it invites abuse.
- **Rotate credentials.** Rotating the basic auth secret is a config change in both Gotenberg and Documenso, followed by a restart of each.
## Resource Sizing
Conversion is CPU- and memory-bound on LibreOffice. As a starting point:
| Workload | Suggested resources |
| ----------------------------- | ------------------------------------ |
| Light (a few DOCX per minute) | 1 vCPU, 1 GB RAM |
| Moderate (sustained uploads) | 2 vCPU, 2 GB RAM |
| Heavy / multi-tenant | Horizontally scale Gotenberg replicas behind a load balancer |
Gotenberg is stateless. Each container handles one or more concurrent requests independently. Scale horizontally rather than vertically once a single replica is saturated.
## Troubleshooting
<Accordions type="multiple">
<Accordion title="DOCX uploads are rejected with 'Only PDF and DOCX files are allowed'">
The Documenso server does not see `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL`. Check the value is set
on the running container (`docker exec documenso printenv | grep DOCUMENT_CONVERSION`) and
restart after changing it.
</Accordion>
<Accordion title="Uploads fail with 'Document conversion service is currently unavailable'">
Documenso could not reach Gotenberg. Verify:
- The URL in `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is resolvable from the Documenso container
(use the Docker service name or in-cluster DNS, not `localhost`).
- Gotenberg's `/health` endpoint returns `200`.
- Basic auth credentials match between the two services.
After repeated failures, an internal circuit breaker opens for 30 seconds. Subsequent uploads
will fail fast during that window; this is intentional and self-recovers.
</Accordion>
<Accordion title="Uploads fail with 'Failed to convert document to PDF'">
Gotenberg was reachable but returned a non-2xx response. Check the Gotenberg container logs:
```bash
docker compose logs -f gotenberg
```
Common causes: corrupted `.docx` file, exotic embedded objects LibreOffice cannot render, or a
file that genuinely exceeded the conversion timeout. Increase
`NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` for very large documents.
</Accordion>
<Accordion title="Converted PDFs look different from the Word document">
LibreOffice is not byte-identical to Microsoft Word. Layout, font metrics, and complex elements
(Charts, SmartArt, ActiveX controls) may differ. To improve fidelity:
- Use the custom Dockerfile in this guide to install Microsoft Core Fonts and additional
language fonts.
- Make sure any custom fonts referenced by your documents are installed in the Gotenberg image.
- For pixel-perfect output, ask users to export to PDF from Word before uploading.
</Accordion>
<Accordion title="Form controls in the DOCX appear blank or missing">
Documenso disables Gotenberg's `exportFormFields` flag during conversion. Word content controls
(`<w:sdt>`) become static graphics in the output PDF, which prevents Documenso's later
flattening step from making them invisible. This is intentional. Use Documenso fields
(signature, text, date, etc.) for anything that needs to be filled in by signers.
</Accordion>
<Accordion title="Conversion is slow on the first request">
LibreOffice starts lazily by default. Pass `--libreoffice-auto-start` to Gotenberg so it warms
up at container boot. Allow up to a minute on first start before considering the service
unhealthy.
</Accordion>
<Accordion title="The circuit breaker keeps opening">
Repeated failures open an in-process circuit breaker for 30 seconds. If you see this in
production, the underlying problem is the Gotenberg service. Check its logs, resource usage,
and connectivity. The breaker is per-process and resets on restart.
</Accordion>
</Accordions>
---
## See Also
- [Upload Documents (User Guide)](/docs/users/documents/upload) - End-user view of DOCX uploads
- [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Compose-based deployment patterns
- [Gotenberg Documentation](https://gotenberg.dev/docs/getting-started/introduction) - Upstream Gotenberg docs
@@ -1,6 +1,6 @@
---
title: Advanced
description: Optional configuration for OAuth providers, AI features, and other advanced settings.
description: Optional configuration for OAuth providers, AI features, document conversion, and other advanced settings.
---
<Cards>
@@ -14,4 +14,9 @@ description: Optional configuration for OAuth providers, AI features, and other
description="Enable AI-powered recipient and field detection."
href="/docs/self-hosting/configuration/advanced/ai-features"
/>
<Card
title="Document Conversion"
description="Accept DOCX uploads by running a Gotenberg sidecar that converts Word documents to PDF."
href="/docs/self-hosting/configuration/advanced/document-conversion"
/>
</Cards>
@@ -1,4 +1,4 @@
{
"title": "Advanced",
"pages": ["oauth-providers", "ai-features"]
"pages": ["oauth-providers", "document-conversion", "ai-features"]
}
@@ -224,28 +224,41 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
## Feature Flags
| Variable | Description | Default |
| ------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration entirely | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
| Variable | Description | Default |
| -------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Disable all signup methods application-wide | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only. SSO signup is unaffected | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google. Existing Google-linked users can still sign in | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
### Signup Restrictions
You can control who is allowed to create accounts on your instance using two environment variables:
You can control who is allowed to create accounts on your instance with the following environment variables:
- **`NEXT_PUBLIC_DISABLE_SIGNUP`**: Set to `true` to block all new signups. Existing users can still sign in. This applies to both email/password and OAuth signups.
- **`NEXT_PUBLIC_DISABLE_SIGNUP`** (master switch): Set to `true` to block all new signups across every method (email/password, Google, Microsoft, OIDC). When set, this also blocks new-account creation through the organisation OIDC authentication portal.
- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`**: Set to `true` to disable email/password signup only. SSO signup is still allowed.
- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`**: Set to `true` to block brand-new account creation through the matching SSO provider. Existing users with the provider already linked can still sign in, and existing users can still link the provider to their account. `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` also blocks new-account creation through the organisation authentication portal.
- **`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`**: Restrict signups to specific email domains. When set, only users whose email address matches one of the listed domains can create an account. Leave empty to allow all domains.
Both restrictions apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error.
Sign-in for existing users is never affected, only the creation of brand-new accounts.
When both variables are set, `NEXT_PUBLIC_DISABLE_SIGNUP` takes precedence. Signups are blocked regardless of the domain list.
Both the master switch and the domain allowlist apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error.
When both the master switch and the domain allowlist are set, the master switch takes precedence. Signups are blocked regardless of the domain list.
```bash
# Allow signups only from specific domains
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
# Allow OIDC signup only; block email/password, Google, Microsoft
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
# Or disable signups entirely
NEXT_PUBLIC_DISABLE_SIGNUP="true"
```
@@ -266,6 +279,23 @@ AI features must also be enabled in organisation/team settings after configurati
---
## Document Conversion
Documenso can accept `.docx` uploads by sending them to a [Gotenberg](https://gotenberg.dev) service that converts them to PDF. When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is unset, DOCX uploads are rejected and only PDFs are accepted.
| Variable | Description | Default |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------- |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` | Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Unset disables the feature. | |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | HTTP Basic auth username. Required when Gotenberg runs with `--api-enable-basic-auth`. | |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | HTTP Basic auth password. Set together with the username. | |
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` | Per-request timeout in milliseconds. Increase for very large documents. | `30000` |
The public flag `NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically from `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` on server start. Do not set it manually.
For setup, image-build instructions, and security recommendations, see [Document Conversion](/docs/self-hosting/configuration/advanced/document-conversion).
---
## Background Jobs
Documenso supports multiple background job providers for processing emails, documents, webhooks, and scheduled tasks.
@@ -329,7 +359,7 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
## Enterprise Features
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed authoring, and 21 CFR Part 11 compliance.
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
| Variable | Description |
| ------------------------------------ | ------------------------------------------------ |
@@ -371,6 +401,10 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
# Signup restrictions (optional)
# NEXT_PUBLIC_DISABLE_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
```
@@ -155,7 +155,13 @@ PORT=3000
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
# Signup restrictions (optional)
# Master switch — disables every signup method
NEXT_PUBLIC_DISABLE_SIGNUP=false
# Per-method switches (optional). Each disables brand-new account creation through that method.
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=true
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
```
@@ -252,7 +258,10 @@ Navigate to the signup page and create your account. Verify your email address
<Callout type="info">
All accounts created through signup are regular user accounts. Admin access must be granted
directly in the database. Once your accounts are set up, consider disabling public signups by
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`, or restrict signups to specific email domains with
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`. For finer control, use the per-method switches
`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`, `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`,
`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`, `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`, or restrict
signups to specific email domains with
`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
</Callout>
@@ -100,7 +100,11 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
@@ -153,7 +153,11 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
| Variable | Description | Default |
| --------------------------------- | ---------------------------------- | ------- |
| `PORT` | Application port | `3000` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
@@ -122,7 +122,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
## Enterprise Edition
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed authoring white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
@@ -11,16 +11,41 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
| Limitation | Value |
| ----------------------- | ----------------------------------- |
| Supported format | PDF only |
| Supported formats | PDF, DOCX |
| Maximum file size | 50MB (configurable for self-hosted) |
| Encrypted PDFs | Not supported |
| Password-protected PDFs | Not supported |
| Legacy `.doc` files | Not supported (convert to DOCX) |
<Callout type="warn">
Documenso does not support password-protected or encrypted PDF files. Remove encryption before
uploading.
</Callout>
## Supported Formats
Documenso accepts two file formats:
- **PDF** (`.pdf`): used as-is. **Recommended.**
- **Word** (`.docx`): converted to PDF on the server during upload. The converted PDF is what recipients sign.
Other formats (`.doc`, `.odt`, `.rtf`, images) are not supported. Convert them to PDF or DOCX before uploading.
<Callout type="warn">
**Upload a PDF whenever you can.** DOCX files are converted to PDF using LibreOffice, which is not
byte-identical to Microsoft Word. Spacing, line breaks, fonts, and complex elements (tables,
charts, headers, footers) can shift in the converted PDF. For the final document to look exactly
the way you designed it, export to PDF from Word, Google Docs, or Pages and upload the PDF
directly.
</Callout>
<Callout type="info">
DOCX support requires the document conversion service. It is enabled on
[documenso.com](https://app.documenso.com). Self-hosted instances must
[configure it](/docs/self-hosting/configuration/advanced/document-conversion) before DOCX uploads
are accepted.
</Callout>
## Upload Methods
![Documents dashboard](/document-signing/documenso-documents-dashboard.webp)
@@ -38,15 +63,15 @@ You can upload documents in two ways:
</Step>
<Step>
### Drag and drop your PDF
### Drag and drop your file
Drag a PDF file from your computer and drop it anywhere on the page.
Drag a PDF or DOCX file from your computer and drop it anywhere on the page.
</Step>
<Step>
### Wait for the upload to complete
The document will process and the editor will open when ready.
The document will process and the editor will open when ready. DOCX files take a few extra seconds while they are converted to PDF.
</Step>
</Steps>
@@ -70,7 +95,7 @@ You can upload documents in two ways:
<Step>
### Select your file
Choose a PDF file from your computer.
Choose a PDF or DOCX file from your computer.
</Step>
<Step>
@@ -81,16 +106,32 @@ You can upload documents in two ways:
</Step>
</Steps>
## DOCX Conversion
We always recommend uploading a PDF rather than a DOCX. If you have the original document open in Word, Google Docs, or Pages, export to PDF from there and upload the PDF. The result is guaranteed to match what you see on screen.
If you do upload a `.docx` file, Documenso converts it to PDF before adding it to the envelope. The original `.docx` is discarded. Only the converted PDF is stored, signed, and downloaded.
Things to keep in mind when uploading DOCX:
- **The converted PDF will not be pixel-identical to your Word document.** Conversion uses LibreOffice, which renders most documents faithfully but differs from Microsoft Word in subtle ways. Spacing, font metrics, line breaks, and complex layout features can shift.
- **Always review the converted PDF before adding fields or sending.** Open the document in the editor and scroll through every page to confirm it looks the way you expect.
- **Form controls are flattened.** Word content controls (drop-downs, date pickers, checkboxes) become static text or graphics. Use Documenso fields for anything that needs to be filled in.
- **Fonts not installed on the server fall back to substitutes.** On documenso.com, common fonts (Calibri, Arial, Times New Roman, etc.) are installed. On self-hosted instances, font fidelity depends on the operator's setup.
- **Tracked changes and comments are preserved as they appear in Word.** Accept or reject changes and remove comments before uploading if you do not want them in the final document.
If the converted PDF does not match what you expect, export the document to PDF from Word, Google Docs, or another tool and upload the PDF directly.
## Uploading Multiple Documents
You can upload multiple PDF files at once to create a single envelope containing multiple documents. The number of files you can upload per envelope depends on your plan.
You can upload multiple files at once to create a single envelope containing multiple documents. The number of files you can upload per envelope depends on your plan.
To upload multiple files:
- Select multiple PDF files when using the file picker, or
- Drag and drop multiple PDF files at once
- Select multiple PDF or DOCX files when using the file picker, or
- Drag and drop multiple files at once
All files in the same upload become part of the same envelope and share the same recipients and signing workflow.
You can mix PDF and DOCX files in the same upload. All files become part of the same envelope and share the same recipients and signing workflow.
<Callout type="info">
If you need separate signing workflows for each document, upload them individually.
@@ -114,15 +155,37 @@ The document remains in `Draft` status until you send it. You can close the edit
<Accordion title="File is larger than 50MB">
Reduce the file size before uploading:
- Compress images within the PDF
- Compress images within the document
- Remove unnecessary pages
- Use a PDF compression tool
- Use a PDF compression tool (for PDFs) or save with images downsampled (for DOCX)
</Accordion>
<Accordion title="Only PDF files are allowed">
Convert your document to PDF before uploading. Most applications (Word, Google Docs, etc.) can
export to PDF format.
<Accordion title="Only PDF and DOCX files are allowed">
Documenso accepts PDF and DOCX. For other formats (`.doc`, `.odt`, `.rtf`, etc.), export to PDF
from your editor (Word, Google Docs, Pages) and upload the PDF.
If you are self-hosted and DOCX is rejected, the [document conversion
service](/docs/self-hosting/configuration/advanced/document-conversion) is not configured on your
instance.
</Accordion>
<Accordion title="DOCX upload fails with a conversion error">
The document conversion service was reachable but could not convert the file. Common causes:
- The `.docx` file is corrupted. Open it in Word, save a new copy, and try again.
- The file uses very unusual fonts or embedded objects that LibreOffice cannot render.
- The file is unusually large or complex and exceeded the conversion timeout.
If the problem persists, export the document to PDF from Word and upload the PDF directly.
</Accordion>
<Accordion title="DOCX upload fails with 'conversion service unavailable'">
The document conversion service is down or temporarily unreachable. Try again in a minute. If you
self-host, check the [document conversion
service](/docs/self-hosting/configuration/advanced/document-conversion) logs.
</Accordion>
<Accordion title="You cannot upload encrypted PDFs">
@@ -1,13 +1,4 @@
{
"title": "Organisations",
"pages": [
"overview",
"create-team",
"members",
"groups",
"email-domains",
"preferences",
"single-sign-on",
"billing"
]
"pages": ["overview", "create-team", "members", "groups", "email-domains", "preferences", "single-sign-on", "billing"]
}
@@ -134,6 +134,13 @@ Leave empty to allow any domain authenticated by your identity provider.
team.
</Callout>
### Allow Personal Organisations
Controls whether users signing in via SSO for the first time also receive their own personal organisation in addition to joining your organisation.
- **Enabled**: New SSO users get a personal organisation where they can create and manage their own documents independently.
- **Disabled**: New SSO users only join your organisation and do not receive a personal organisation.
## User Provisioning
When a user signs in through your SSO portal for the first time:
+17 -2
View File
@@ -296,12 +296,27 @@ const config = {
},
{
source: '/developers/embedding/authoring',
destination: '/docs/developers/embedding/authoring',
destination: '/docs/developers/embedding/editor',
permanent: true,
},
{
source: '/developers/embedding/authoring/:path*',
destination: '/docs/developers/embedding/editor/:path*',
permanent: true,
},
{
source: '/developers/embedded-authoring',
destination: '/docs/developers/embedding/authoring',
destination: '/docs/developers/embedding/editor',
permanent: true,
},
{
source: '/docs/developers/embedding/authoring',
destination: '/docs/developers/embedding/editor',
permanent: true,
},
{
source: '/docs/developers/embedding/authoring/:path*',
destination: '/docs/developers/embedding/editor/:path*',
permanent: true,
},
+1 -1
View File
@@ -1,5 +1,5 @@
import { baseOptions } from '@/lib/layout.shared';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
+43 -56
View File
@@ -1,16 +1,7 @@
import { BookOpenIcon, CodeIcon, FileTextIcon, GithubIcon, ServerIcon, ShieldCheckIcon, UserIcon } from 'lucide-react';
import type { Metadata } from 'next';
import Link from 'next/link';
import {
BookOpenIcon,
CodeIcon,
FileTextIcon,
GithubIcon,
ServerIcon,
ShieldCheckIcon,
UserIcon,
} from 'lucide-react';
export const metadata: Metadata = {
title: 'Documenso Docs',
description:
@@ -22,21 +13,21 @@ export default function HomePage() {
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Hero */}
<div className="mb-16 pt-6 text-center">
<h1 className="mb-4 text-4xl font-bold tracking-tight">Documenso Documentation</h1>
<p className="text-fd-muted-foreground mx-auto mb-8 max-w-2xl text-lg">
The open-source document signing platform. Send documents for signatures, integrate with
your apps, or self-host with full control.
<h1 className="mb-4 font-bold text-4xl tracking-tight">Documenso Documentation</h1>
<p className="mx-auto mb-8 max-w-2xl text-fd-muted-foreground text-lg">
The open-source document signing platform. Send documents for signatures, integrate with your apps, or
self-host with full control.
</p>
<div className="flex flex-wrap justify-center gap-3">
<Link
href="/docs/users"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso-dark/90 inline-flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso-dark/90"
>
Get Started
</Link>
<a
href="https://github.com/documenso/documenso"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
>
<GithubIcon className="size-4" />
View on GitHub
@@ -48,64 +39,60 @@ export default function HomePage() {
<div className="mb-16 grid gap-4 md:grid-cols-3">
<Link
href="/docs/users"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<UserIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">User Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
<h2 className="mb-2 font-semibold text-lg">User Guide</h2>
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Send documents, create templates, and manage your team using the web application.
</p>
<span className="text-fd-primary text-sm font-medium">Get started </span>
<span className="font-medium text-fd-primary text-sm">Get started </span>
</Link>
<Link
href="/docs/developers"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400">
<CodeIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">Developer Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
Integrate document signing into your applications with the REST API, webhooks, and
embedding.
<h2 className="mb-2 font-semibold text-lg">Developer Guide</h2>
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Integrate document signing into your applications with the REST API, webhooks, and embedding.
</p>
<span className="text-fd-primary text-sm font-medium">View API docs </span>
<span className="font-medium text-fd-primary text-sm">View API docs </span>
</Link>
<Link
href="/docs/self-hosting"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-purple-500/10 text-purple-600 dark:text-purple-400">
<ServerIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">Self-Hosting Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
<h2 className="mb-2 font-semibold text-lg">Self-Hosting Guide</h2>
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Deploy your own Documenso instance with Docker, Kubernetes, or Railway.
</p>
<span className="text-fd-primary text-sm font-medium">Deploy now </span>
<span className="font-medium text-fd-primary text-sm">Deploy now </span>
</Link>
</div>
{/* Quick Start & Core Concepts */}
<div className="mb-16 grid gap-8 md:grid-cols-2">
<div className="bg-fd-card/50 rounded-xl border p-6">
<div className="rounded-xl border bg-fd-card/50 p-6">
<h3 className="mb-4 flex items-center gap-2 font-semibold">
<BookOpenIcon className="text-fd-muted-foreground size-5" />
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
Quick Start
</h3>
<div className="space-y-4">
<div>
<h4 className="mb-2 text-sm font-medium">Send your first document</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<h4 className="mb-2 font-medium text-sm">Send your first document</h4>
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li>
<Link
href="/docs/users/getting-started/create-account"
className="text-fd-primary hover:underline"
>
<Link href="/docs/users/getting-started/create-account" className="text-fd-primary hover:underline">
Create an account
</Link>
</li>
@@ -120,8 +107,8 @@ export default function HomePage() {
</ol>
</div>
<div>
<h4 className="mb-2 text-sm font-medium">Integrate with the API</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<h4 className="mb-2 font-medium text-sm">Integrate with the API</h4>
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li>
<Link
href="/docs/developers/getting-started/authentication"
@@ -141,8 +128,8 @@ export default function HomePage() {
</ol>
</div>
<div>
<h4 className="mb-2 text-sm font-medium">Deploy self-hosted</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<h4 className="mb-2 font-medium text-sm">Deploy self-hosted</h4>
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li>
<Link
href="/docs/self-hosting/getting-started/requirements"
@@ -164,36 +151,36 @@ export default function HomePage() {
</div>
</div>
<div className="bg-fd-card/50 rounded-xl border p-6">
<div className="rounded-xl border bg-fd-card/50 p-6">
<h3 className="mb-4 flex items-center gap-2 font-semibold">
<BookOpenIcon className="text-fd-muted-foreground size-5" />
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
Core Concepts
</h3>
<div className="grid grid-cols-2 gap-3">
<Link
href="/docs/concepts/document-lifecycle"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
>
<div className="mb-1 font-medium">Document Lifecycle</div>
<div className="text-fd-muted-foreground text-xs">Draft to completed</div>
</Link>
<Link
href="/docs/concepts/recipient-roles"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
>
<div className="mb-1 font-medium">Recipient Roles</div>
<div className="text-fd-muted-foreground text-xs">Signers and approvers</div>
</Link>
<Link
href="/docs/concepts/field-types"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
>
<div className="mb-1 font-medium">Field Types</div>
<div className="text-fd-muted-foreground text-xs">Signatures and inputs</div>
</Link>
<Link
href="/docs/concepts/signing-certificates"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
>
<div className="mb-1 font-medium">Signing Certificates</div>
<div className="text-fd-muted-foreground text-xs">Digital verification</div>
@@ -206,7 +193,7 @@ export default function HomePage() {
<div className="mb-16 grid gap-4 md:grid-cols-2">
<Link
href="/docs/compliance"
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400">
<ShieldCheckIcon className="size-5" />
@@ -221,7 +208,7 @@ export default function HomePage() {
<Link
href="/docs/policies"
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-slate-500/10 text-slate-600 dark:text-slate-400">
<FileTextIcon className="size-5" />
@@ -236,22 +223,22 @@ export default function HomePage() {
</div>
{/* Community CTA */}
<div className="from-fd-primary/5 to-fd-primary/10 rounded-xl border bg-gradient-to-r p-8 text-center">
<h3 className="mb-2 text-lg font-semibold">Join the Community</h3>
<p className="text-fd-muted-foreground mb-6 text-sm">
<div className="rounded-xl border bg-gradient-to-r from-fd-primary/5 to-fd-primary/10 p-8 text-center">
<h3 className="mb-2 font-semibold text-lg">Join the Community</h3>
<p className="mb-6 text-fd-muted-foreground text-sm">
Documenso is open source. Contribute, ask questions, or share feedback.
</p>
<div className="flex flex-wrap justify-center gap-3">
<a
href="https://github.com/documenso/documenso"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
>
<GithubIcon className="size-4" />
GitHub
</a>
<a
href="https://documen.so/discord"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
>
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
@@ -260,7 +247,7 @@ export default function HomePage() {
</a>
<a
href="https://app.documenso.com/signup"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-4 py-2 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
>
Try Documenso
</a>
+1 -1
View File
@@ -1,5 +1,5 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
+1 -2
View File
@@ -1,10 +1,9 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
import { getPageImage, source } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
const gitConfig = {
user: 'documenso',
+11 -16
View File
@@ -1,16 +1,14 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/cn';
import { baseOptions } from '@/lib/layout.shared';
import { getFilteredPageTree, source } from '@/lib/source';
import type * as PageTree from 'fumadocs-core/page-tree';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { CodeIcon, ServerIcon, UserIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
import { cn } from '@/lib/cn';
import { baseOptions } from '@/lib/layout.shared';
import { getFilteredPageTree, source } from '@/lib/source';
const ROOT_SECTIONS = [
{
@@ -44,7 +42,9 @@ function getFirstPageUrl(children: PageTree.Node[]): string | undefined {
}
if (child.type === 'folder' && child.children.length > 0) {
const url = getFirstPageUrl(child.children);
if (url) return url;
if (url) {
return url;
}
}
}
return undefined;
@@ -69,13 +69,8 @@ function SectionSwitcher({ activeSection }: { activeSection: string | null }) {
>
<Icon className={cn('mt-0.5 size-4 shrink-0', isActive ? 'text-fd-primary' : '')} />
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{section.label}</span>
<span
className={cn(
'text-xs',
isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70',
)}
>
<span className="font-medium text-sm">{section.label}</span>
<span className={cn('text-xs', isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70')}>
{section.subtitle}
</span>
</div>
+13 -17
View File
@@ -1,6 +1,6 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/shadcn.css';
@import 'fumadocs-ui/css/preset.css';
@import "tailwindcss";
@import "fumadocs-ui/css/shadcn.css";
@import "fumadocs-ui/css/preset.css";
@theme {
/* Brand utility colors */
@@ -43,13 +43,11 @@
--sidebar-border: hsl(223.8136 0.0001% 89.8161%);
--sidebar-ring: hsl(223.8136 0% 63.0163%);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-x: 0;
--shadow-y: 1px;
@@ -103,13 +101,11 @@
--sidebar-border: hsl(223.8136 0% 15.5096%);
--sidebar-ring: hsl(223.8136 0% 32.1993%);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-x: 0;
--shadow-y: 1px;
+2 -4
View File
@@ -1,7 +1,6 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { RootProvider } from 'fumadocs-ui/provider/next';
import PlausibleProvider from 'next-plausible';
import './global.css';
@@ -16,8 +15,7 @@ export const metadata: Metadata = {
template: '%s | Documenso Docs',
default: 'Documenso Docs',
},
description:
'The official documentation for Documenso, the open-source document signing platform.',
description: 'The official documentation for Documenso, the open-source document signing platform.',
openGraph: {
siteName: 'Documenso Docs',
type: 'website',
+4 -4
View File
@@ -3,20 +3,20 @@ import Link from 'next/link';
export default function NotFound() {
return (
<main className="mx-auto flex max-w-xl flex-col items-center justify-center px-4 py-32 text-center">
<h1 className="text-4xl font-bold tracking-tight">Page not found</h1>
<p className="text-fd-muted-foreground mt-4 text-lg">
<h1 className="font-bold text-4xl tracking-tight">Page not found</h1>
<p className="mt-4 text-fd-muted-foreground text-lg">
The page you are looking for may have moved. Our documentation was recently restructured.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<Link
href="/docs/users"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
className="inline-flex items-center rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
>
Browse documentation
</Link>
<Link
href="/"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
className="inline-flex items-center rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
>
Go to homepage
</Link>
+84 -93
View File
@@ -1,21 +1,16 @@
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { getPageImage, source } from '@/lib/source';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { getPageImage, source } from '@/lib/source';
export const revalidate = false;
const loadAssets = async () => {
const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([
readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))),
readFile(
fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url)),
),
readFile(
fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url)),
),
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url))),
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url))),
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-bold.ttf', import.meta.url))),
]);
@@ -40,104 +35,100 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
const { logoSrc, fonts } = await loadAssets();
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
backgroundColor: 'white',
padding: '60px 80px',
fontFamily: 'Inter',
position: 'relative',
}}
>
{/* Green accent bar */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '6px',
backgroundColor: '#6DC947',
}}
/>
{/* Top: Logo */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logoSrc} alt="Documenso" height="28" />
<span
style={{
color: '#D4D4D8',
fontSize: '28px',
fontWeight: 400,
}}
>
|
</span>
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
</div>
{/* Middle: Title + description */}
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
backgroundColor: 'white',
padding: '60px 80px',
fontFamily: 'Inter',
position: 'relative',
flex: 1,
justifyContent: 'center',
gap: '16px',
}}
>
{/* Green accent bar */}
<div
<h1
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '6px',
backgroundColor: '#6DC947',
}}
/>
{/* Top: Logo */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
color: '#18181B',
fontSize: page.data.title.length > 40 ? '48px' : '56px',
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: '-0.025em',
margin: 0,
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logoSrc} alt="Documenso" height="28" />
<span
{page.data.title}
</h1>
{page.data.description && (
<p
style={{
color: '#D4D4D8',
fontSize: '28px',
color: '#71717A',
fontSize: '22px',
fontWeight: 400,
}}
>
|
</span>
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
</div>
{/* Middle: Title + description */}
<div
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
justifyContent: 'center',
gap: '16px',
}}
>
<h1
style={{
color: '#18181B',
fontSize: page.data.title.length > 40 ? '48px' : '56px',
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: '-0.025em',
lineHeight: 1.4,
margin: 0,
maxWidth: '900px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{page.data.title}
</h1>
{page.data.description && (
<p
style={{
color: '#71717A',
fontSize: '22px',
fontWeight: 400,
lineHeight: 1.4,
margin: 0,
maxWidth: '900px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{page.data.description}
</p>
)}
</div>
{/* Bottom: URL */}
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>
docs.documenso.com{page.url}
</span>
</div>
{page.data.description}
</p>
)}
</div>
),
{/* Bottom: URL */}
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>docs.documenso.com{page.url}</span>
</div>
</div>,
{
width: 1200,
height: 630,
+12 -22
View File
@@ -1,12 +1,11 @@
'use client';
import { useMemo, useState } from 'react';
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
import { useMemo, useState } from 'react';
import { cn } from '@/lib/cn';
const cache = new Map<string, string>();
@@ -21,7 +20,9 @@ export function LLMCopyButton({
const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached);
if (cached) {
return navigator.clipboard.writeText(cached);
}
setLoading(true);
@@ -48,7 +49,7 @@ export function LLMCopyButton({
buttonVariants({
color: 'secondary',
size: 'sm',
className: '[&_svg]:text-fd-muted-foreground gap-2 [&_svg]:size-3.5',
className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',
}),
)}
onClick={onClick}
@@ -74,8 +75,7 @@ export function ViewOptions({
githubUrl: string;
}) {
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
const fullMarkdownUrl = typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [
@@ -96,12 +96,7 @@ export function ViewOptions({
q,
})}`,
icon: (
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
@@ -113,12 +108,7 @@ export function ViewOptions({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Anthropic</title>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
@@ -146,7 +136,7 @@ export function ViewOptions({
)}
>
Open
<ChevronDown className="text-fd-muted-foreground size-3.5" />
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
</PopoverTrigger>
<PopoverContent className="flex flex-col">
{items.map((item) => (
@@ -155,11 +145,11 @@ export function ViewOptions({
href={item.href}
rel="noreferrer noopener"
target="_blank"
className="hover:text-fd-accent-foreground hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg p-2 text-sm [&_svg]:size-4"
className="inline-flex items-center gap-2 rounded-lg p-2 text-sm hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-4"
>
{item.icon}
{item.title}
<ExternalLinkIcon className="text-fd-muted-foreground ms-auto size-3.5" />
<ExternalLinkIcon className="ms-auto size-3.5 text-fd-muted-foreground" />
</a>
))}
</PopoverContent>
+1 -2
View File
@@ -1,8 +1,7 @@
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { useTheme } from 'next-themes';
import { useEffect, useId, useRef, useState } from 'react';
export const Mermaid = ({ chart }: { chart: string }) => {
const [mounted, setMounted] = useState(false);
+3 -7
View File
@@ -1,7 +1,7 @@
import { docs } from 'fumadocs-mdx:collections/server';
import type * as PageTree from 'fumadocs-core/page-tree';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docs } from 'fumadocs-mdx:collections/server';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
@@ -30,9 +30,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
// Find the main section folder
const rootFolder = fullTree.children.find(
(child): child is PageTree.Folder =>
child.type === 'folder' &&
typeof child.name === 'string' &&
child.name.toLowerCase() === rootName.toLowerCase(),
child.type === 'folder' && typeof child.name === 'string' && child.name.toLowerCase() === rootName.toLowerCase(),
);
if (!rootFolder) {
@@ -42,9 +40,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
// Find shared section folders
const sharedFolders = fullTree.children.filter(
(child): child is PageTree.Folder =>
child.type === 'folder' &&
typeof child.name === 'string' &&
SHARED_SECTIONS.includes(child.name.toLowerCase()),
child.type === 'folder' && typeof child.name === 'string' && SHARED_SECTIONS.includes(child.name.toLowerCase()),
);
// Create separator for main section
+1 -1
View File
@@ -1,7 +1,7 @@
import { Mermaid } from '@/components/mdx/mermaid';
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
import { Mermaid } from '@/components/mdx/mermaid';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMDXComponents(components?: MDXComponents): any {
+6 -22
View File
@@ -2,11 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -20,12 +16,8 @@
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": [
"./src/*"
],
"fumadocs-mdx:collections/*": [
".source/*"
]
"@/*": ["./src/*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"plugins": [
{
@@ -33,14 +25,6 @@
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
+1 -4
View File
@@ -10,10 +10,7 @@ export type TransformedData = {
const FORMAT = 'MMM yyyy';
export const addZeroMonth = (
transformedData: TransformedData,
isCumulative = false,
): TransformedData => {
export const addZeroMonth = (transformedData: TransformedData, isCumulative = false): TransformedData => {
const result: TransformedData = {
labels: [...transformedData.labels],
datasets: transformedData.datasets.map((dataset) => ({
+15 -8
View File
@@ -60,7 +60,9 @@ async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginF
const reqOrigin = req.headers.get('Origin') || undefined;
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
if (!value) return;
if (!value) {
return;
}
return getOriginHeaders(reqOrigin, value);
}
@@ -85,12 +87,17 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
const { headers } = res;
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
const mergeHeaders = (v: string, k: string) => {
if (k === 'Vary') headers.append(k, v);
else headers.set(k, v);
if (k === 'Vary') {
headers.append(k, v);
} else {
headers.set(k, v);
}
};
// If there's no origin we won't touch the response
if (!originHeaders) return res;
if (!originHeaders) {
return res;
}
originHeaders.forEach(mergeHeaders);
@@ -98,9 +105,7 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
headers.set('Access-Control-Allow-Credentials', 'true');
}
const exposed = Array.isArray(opts.exposedHeaders)
? opts.exposedHeaders.join(',')
: opts.exposedHeaders;
const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(',') : opts.exposedHeaders;
if (exposed) {
headers.set('Access-Control-Expose-Headers', exposed);
@@ -120,7 +125,9 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
headers.set('Access-Control-Max-Age', String(opts.maxAge));
}
if (opts.preflightContinue) return res;
if (opts.preflightContinue) {
return res;
}
headers.set('Content-Length', '0');
return new Response(null, { status: opts.optionsSuccessStatus, headers });
@@ -1,8 +1,7 @@
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { addZeroMonth } from '../add-zero-month';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
@@ -30,9 +29,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative'
datasets: [
{
label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
},
],
};
@@ -40,6 +37,4 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative'
return addZeroMonth(transformedData, type === 'cumulative');
};
export type GetCompletedDocumentsMonthlyResult = Awaited<
ReturnType<typeof getCompletedDocumentsMonthly>
>;
export type GetCompletedDocumentsMonthlyResult = Awaited<ReturnType<typeof getCompletedDocumentsMonthly>>;
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
import { addZeroMonth } from '../add-zero-month';
@@ -29,9 +28,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' =
datasets: [
{
label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
},
],
};
@@ -39,6 +36,4 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' =
return addZeroMonth(transformedData, type === 'cumulative');
};
export type GetSignerConversionMonthlyResult = Awaited<
ReturnType<typeof getSignerConversionMonthly>
>;
export type GetSignerConversionMonthlyResult = Awaited<ReturnType<typeof getSignerConversionMonthly>>;
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
import { addZeroMonth } from '../add-zero-month';
@@ -26,9 +25,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count
datasets: [
{
label: type === 'count' ? 'New Users' : 'Total Users',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
},
],
};
+2 -8
View File
@@ -1,6 +1,6 @@
import { DateTime } from 'luxon';
import { type TransformedData, addZeroMonth } from './add-zero-month';
import { addZeroMonth, type TransformedData } from './add-zero-month';
type MetricKeys = {
stars: number;
@@ -24,13 +24,7 @@ const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
earlyAdopters: 'Customers',
};
export function transformData({
data,
metric,
}: {
data: DataEntry;
metric: MetricKey;
}): TransformedData {
export function transformData({ data, metric }: { data: DataEntry; metric: MetricKey }): TransformedData {
try {
if (!data || Object.keys(data).length === 0) {
return {
+1 -7
View File
@@ -22,12 +22,6 @@
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
+18 -18
View File
@@ -1,9 +1,9 @@
@import '@documenso/ui/styles/theme.css';
@import "@documenso/ui/styles/theme.css";
/* Inter Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variablefont_opsz,wght.ttf') format('truetype-variations');
font-family: "Inter";
src: url("/fonts/inter-variablefont_opsz,wght.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -11,8 +11,8 @@
/* Inter Italic Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-italic-variablefont_opsz,wght.ttf') format('truetype-variations');
font-family: "Inter";
src: url("/fonts/inter-italic-variablefont_opsz,wght.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: italic;
font-display: swap;
@@ -20,16 +20,16 @@
/* Caveat Variable Font */
@font-face {
font-family: 'Caveat';
src: url('/fonts/caveat-variablefont_wght.ttf') format('truetype-variations');
font-family: "Caveat";
src: url("/fonts/caveat-variablefont_wght.ttf") format("truetype-variations");
font-weight: 400 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Noto Sans';
src: url('/fonts/noto-sans.ttf') format('truetype-variations');
font-family: "Noto Sans";
src: url("/fonts/noto-sans.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -37,8 +37,8 @@
/* Korean noto sans */
@font-face {
font-family: 'Noto Sans Korean';
src: url('/fonts/noto-sans-korean.ttf') format('truetype-variations');
font-family: "Noto Sans Korean";
src: url("/fonts/noto-sans-korean.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -46,8 +46,8 @@
/* Japanese noto sans */
@font-face {
font-family: 'Noto Sans Japanese';
src: url('/fonts/noto-sans-japanese.ttf') format('truetype-variations');
font-family: "Noto Sans Japanese";
src: url("/fonts/noto-sans-japanese.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -55,8 +55,8 @@
/* Chinese noto sans */
@font-face {
font-family: 'Noto Sans Chinese';
src: url('/fonts/noto-sans-chinese.ttf') format('truetype-variations');
font-family: "Noto Sans Chinese";
src: url("/fonts/noto-sans-chinese.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -64,8 +64,8 @@
@layer base {
:root {
--font-sans: 'Inter';
--font-signature: 'Caveat';
--font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese';
--font-sans: "Inter";
--font-signature: "Caveat";
--font-noto: "Noto Sans", "Noto Sans Korean", "Noto Sans Japanese", "Noto Sans Chinese";
}
}
@@ -1,9 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { trpc } from '@documenso/trpc/react';
@@ -21,6 +15,10 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
export type AccountDeleteDialogProps = {
className?: string;
@@ -36,8 +34,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } = trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => {
try {
@@ -63,18 +60,15 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>
<Trans>Delete Account</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Delete your account and all its contents, including completed documents. This action
is irreversible and will cancel your subscription, so proceed with caution.
Delete your account and all its contents, including completed documents. This action is irreversible and
will cancel your subscription, so proceed with caution.
</Trans>
</AlertDescription>
</div>
@@ -109,10 +103,8 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
<DialogDescription>
<Trans>
Documenso will delete{' '}
<span className="font-semibold">all of your documents</span>, along with all of
your completed documents, signatures, and all other resources belonging to your
Account.
Documenso will delete <span className="font-semibold">all of your documents</span>, along with all
of your completed documents, signatures, and all other resources belonging to your Account.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -121,9 +113,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
<div>
<Label>
<Trans>
Please type{' '}
<span className="text-muted-foreground font-semibold">{user.email}</span> to
confirm.
Please type <span className="font-semibold text-muted-foreground">{user.email}</span> to confirm.
</Trans>
</Label>
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useNavigate } from 'react-router';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@@ -19,6 +12,11 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useNavigate } from 'react-router';
export type AdminDocumentDeleteDialogProps = {
envelopeId: string;
@@ -32,8 +30,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
const [reason, setReason] = useState('');
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
trpc.admin.document.delete.useMutation();
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } = trpc.admin.document.delete.useMutation();
const handleDeleteDocument = async () => {
try {
@@ -64,18 +61,13 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
return (
<div>
<div>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>
<Trans>Delete Document</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Delete the document. This action is irreversible so proceed with caution.
</Trans>
<Trans>Delete the document. This action is irreversible so proceed with caution.</Trans>
</AlertDescription>
</div>
@@ -105,12 +97,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
<Trans>To confirm, please enter the reason</Trans>
</DialogDescription>
<Input
className="mt-2"
type="text"
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
<Input className="mt-2" type="text" value={reason} onChange={(e) => setReason(e.target.value)} />
</div>
<DialogFooter>
@@ -1,13 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import type { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/create-admin-organisation.types';
@@ -22,16 +12,16 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import type { z } from 'zod';
export type OrganisationCreateDialogProps = {
trigger?: React.ReactNode;
@@ -44,11 +34,7 @@ const ZCreateAdminOrganisationFormSchema = ZCreateAdminOrganisationRequestSchema
type TCreateOrganisationFormSchema = z.infer<typeof ZCreateAdminOrganisationFormSchema>;
export const AdminOrganisationCreateDialog = ({
trigger,
ownerUserId,
...props
}: OrganisationCreateDialogProps) => {
export const AdminOrganisationCreateDialog = ({ trigger, ownerUserId, ...props }: OrganisationCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
@@ -101,11 +87,7 @@ export const AdminOrganisationCreateDialog = ({
}, [open, form]);
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -127,10 +109,7 @@ export const AdminOrganisationCreateDialog = ({
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
<FormField
control={form.control}
name="name"
@@ -149,10 +128,7 @@ export const AdminOrganisationCreateDialog = ({
<Alert variant="neutral">
<AlertDescription className="mt-0">
<Trans>
You will need to configure any claims or subscription after creating this
organisation
</Trans>
<Trans>You will need to configure any claims or subscription after creating this organisation</Trans>
</AlertDescription>
</Alert>
@@ -0,0 +1,188 @@
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export type AdminOrganisationDeleteDialogProps = {
organisationId: string;
organisationName: string;
trigger?: React.ReactNode;
};
export const AdminOrganisationDeleteDialog = ({
organisationId,
organisationName,
trigger,
}: AdminOrganisationDeleteDialogProps) => {
const [open, setOpen] = useState(false);
const { t } = useLingui();
const { toast } = useToast();
const deleteMessage = t`delete ${organisationName}`;
const ZAdminDeleteOrganisationFormSchema = z.object({
organisationName: z.literal(deleteMessage, {
errorMap: () => ({ message: t`You must enter '${deleteMessage}' to proceed` }),
}),
sendEmailToOwner: z.boolean(),
});
type TAdminDeleteOrganisationFormSchema = z.infer<typeof ZAdminDeleteOrganisationFormSchema>;
const form = useForm<TAdminDeleteOrganisationFormSchema>({
resolver: zodResolver(ZAdminDeleteOrganisationFormSchema),
defaultValues: {
organisationName: '',
sendEmailToOwner: true,
},
});
const { mutateAsync: deleteOrganisation } = trpc.admin.organisation.delete.useMutation();
const onFormSubmit = async (values: TAdminDeleteOrganisationFormSchema) => {
try {
await deleteOrganisation({
organisationId,
organisationName,
sendEmailToOwner: values.sendEmailToOwner,
});
toast({
title: t`Deletion scheduled`,
description: t`The organisation will be deleted in the background. Documents will be orphaned, not deleted.`,
duration: 7500,
});
setOpen(false);
} catch (err) {
const error = AppError.parseError(err);
console.error(error);
toast({
title: t`An error occurred`,
description: t`We encountered an error while attempting to delete this organisation. Please try again later.`,
variant: 'destructive',
duration: 10000,
});
}
};
useEffect(() => {
if (!open) {
form.reset();
}
}, [open, form]);
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive">
<Trans>Delete</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Delete organisation</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
You are about to delete <span className="font-semibold">{organisationName}</span>. This action is not
reversible. All teams will be removed and all documents will be orphaned to the deleted-account service
account.
</Trans>
</DialogDescription>
</DialogHeader>
<Alert variant="destructive">
<AlertDescription>
<Trans>
The deletion will run in the background, and can take up to a few minutes to complete. Do not re-run this
deletion.
</Trans>
</AlertDescription>
</Alert>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
<FormField
control={form.control}
name="organisationName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Confirm by typing <span className="text-destructive">{deleteMessage}</span>
</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sendEmailToOwner"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
id="admin-delete-organisation-send-email"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<label
htmlFor="admin-delete-organisation-send-email"
className="font-normal text-muted-foreground text-sm leading-snug"
>
<Trans>Email the organisation owner to notify them of the deletion.</Trans>
</label>
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" variant="destructive" loading={form.formState.isSubmitting}>
<Trans>Delete</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useNavigate } from 'react-router';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -20,6 +13,11 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useNavigate } from 'react-router';
export type AdminOrganisationMemberDeleteDialogProps = {
organisationId: string;
@@ -42,33 +40,32 @@ export const AdminOrganisationMemberDeleteDialog = ({
const [open, setOpen] = useState(false);
const { mutateAsync: deleteOrganisationMember, isPending } =
trpc.admin.organisationMember.delete.useMutation({
onSuccess: async () => {
toast({
title: _(msg`Success`),
description: _(msg`Member has been removed from the organisation.`),
duration: 5000,
});
const { mutateAsync: deleteOrganisationMember, isPending } = trpc.admin.organisationMember.delete.useMutation({
onSuccess: async () => {
toast({
title: _(msg`Success`),
description: _(msg`Member has been removed from the organisation.`),
duration: 5000,
});
setOpen(false);
setOpen(false);
// Refresh the page to show updated data
await navigate(0);
},
onError: (err) => {
const error = AppError.parseError(err);
// Refresh the page to show updated data
await navigate(0);
},
onError: (err) => {
const error = AppError.parseError(err);
console.error(error);
console.error(error);
toast({
title: _(msg`An error occurred`),
description: _(msg`We couldn't remove this member. Please try again later.`),
variant: 'destructive',
duration: 10000,
});
},
});
toast({
title: _(msg`An error occurred`),
description: _(msg`We couldn't remove this member. Please try again later.`),
variant: 'destructive',
duration: 10000,
});
},
});
return (
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
@@ -1,15 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import type { TGetAdminOrganisationResponse } from '@documenso/trpc/server/admin-router/get-admin-organisation.types';
@@ -23,22 +11,18 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { z } from 'zod';
export type AdminOrganisationMemberUpdateDialogProps = {
trigger?: React.ReactNode;
@@ -69,9 +53,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
// Determine the current role value for the form
const currentRoleValue = isOwner
? 'OWNER'
: getHighestOrganisationRoleInGroup(
organisationMember.organisationGroupMembers.map((ogm) => ogm.group),
);
: getHighestOrganisationRoleInGroup(organisationMember.organisationGroupMembers.map((ogm) => ogm.group));
const organisationMemberName = organisationMember.user.name ?? organisationMember.user.email;
const form = useForm<ZUpdateOrganisationMemberSchema>({
@@ -81,8 +63,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
},
});
const { mutateAsync: updateOrganisationMemberRole } =
trpc.admin.organisationMember.updateRole.useMutation();
const { mutateAsync: updateOrganisationMemberRole } = trpc.admin.organisationMember.updateRole.useMutation();
const onFormSubmit = async ({ role }: ZUpdateOrganisationMemberSchema) => {
try {
@@ -135,11 +116,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
}, [open, currentRoleValue, form]);
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">
@@ -156,8 +133,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
<DialogDescription className="mt-4">
<Trans>
You are currently updating <span className="font-bold">{organisationMemberName}</span>
.
You are currently updating <span className="font-bold">{organisationMemberName}</span>.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -1,8 +1,3 @@
import { useEffect, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -15,14 +10,10 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useMemo, useState } from 'react';
export type AdminSwapSubscriptionDialogProps = {
open: boolean;
@@ -68,8 +59,7 @@ export const AdminSwapSubscriptionDialog = ({
}
const hasActiveSubscription =
org.subscription &&
(org.subscription.status === 'ACTIVE' || org.subscription.status === 'PAST_DUE');
org.subscription && (org.subscription.status === 'ACTIVE' || org.subscription.status === 'PAST_DUE');
return !hasActiveSubscription;
});
@@ -133,15 +123,14 @@ export const AdminSwapSubscriptionDialog = ({
<DialogDescription>
<Trans>
Move the subscription from "{sourceOrganisationName}" to another organisation owned by
this user.
Move the subscription from "{sourceOrganisationName}" to another organisation owned by this user.
</Trans>
</DialogDescription>
</DialogHeader>
<fieldset className="flex flex-col space-y-4" disabled={isSubmitting}>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">
<label className="font-medium text-sm">
<Trans>Target Organisation</Trans>
</label>
@@ -159,7 +148,7 @@ export const AdminSwapSubscriptionDialog = ({
</Select>
{eligibleOrgs.length === 0 && orgsData && (
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>No eligible organisations found. The target must be on the free plan.</Trans>
</p>
)}
@@ -169,8 +158,8 @@ export const AdminSwapSubscriptionDialog = ({
<Alert variant="warning">
<AlertDescription className="mt-0">
<Trans>
This will move the subscription from "{sourceOrganisationName}" to "
{selectedOrg.name}". The source organisation will be reset to the free plan.
This will move the subscription from "{sourceOrganisationName}" to "{selectedOrg.name}". The source
organisation will be reset to the free plan.
</Trans>
</AlertDescription>
</Alert>
@@ -181,12 +170,7 @@ export const AdminSwapSubscriptionDialog = ({
<Trans>Cancel</Trans>
</Button>
<Button
type="button"
onClick={onSubmit}
disabled={!selectedOrgId}
loading={isSubmitting}
>
<Button type="button" onClick={onSubmit} disabled={!selectedOrgId} loading={isSubmitting}>
<Trans>Move Subscription</Trans>
</Button>
</DialogFooter>
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useNavigate } from 'react-router';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -20,6 +13,11 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useNavigate } from 'react-router';
export type AdminTeamMemberDeleteDialogProps = {
teamId: number;
@@ -93,8 +91,8 @@ export const AdminTeamMemberDeleteDialog = ({
<div>
<DialogDescription>
<Trans>
You are about to remove the following user from the team{' '}
<span className="font-semibold">{teamName}</span>:
You are about to remove the following user from the team <span className="font-semibold">{teamName}</span>
:
</Trans>
</DialogDescription>
@@ -1,11 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -22,6 +14,12 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
export type AdminUserDeleteDialogProps = {
className?: string;
@@ -34,8 +32,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
const navigate = useNavigate();
const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
trpc.admin.user.delete.useMutation();
const { mutateAsync: deleteUser, isPending: isDeletingUser } = trpc.admin.user.delete.useMutation();
const onDeleteAccount = async () => {
try {
@@ -69,16 +66,13 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>Delete Account</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Delete the users account and all its contents. This action is irreversible and will
cancel their subscription, so proceed with caution.
Delete the users account and all its contents. This action is irreversible and will cancel their
subscription, so proceed with caution.
</Trans>
</AlertDescription>
</div>
@@ -111,12 +105,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<DialogFooter>
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -21,23 +14,24 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { match } from 'ts-pattern';
export type AdminUserDisableDialogProps = {
className?: string;
userToDisable: TGetUserResponse;
};
export const AdminUserDisableDialog = ({
className,
userToDisable,
}: AdminUserDisableDialogProps) => {
export const AdminUserDisableDialog = ({ className, userToDisable }: AdminUserDisableDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isPending: isDisablingUser } =
trpc.admin.user.disable.useMutation();
const { mutateAsync: disableUser, isPending: isDisablingUser } = trpc.admin.user.disable.useMutation();
const onDisableAccount = async () => {
try {
@@ -69,16 +63,13 @@ export const AdminUserDisableDialog = ({
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>Disable Account</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Disabling the user results in the user not being able to use the account. It also
disables all the related contents such as subscription, webhooks, teams, and API keys.
Disabling the user results in the user not being able to use the account. It also disables all the related
contents such as subscription, webhooks, teams, and API keys.
</Trans>
</AlertDescription>
</div>
@@ -100,9 +91,8 @@ export const AdminUserDisableDialog = ({
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>
This action is reversible, but please be careful as the account may be
affected permanently (e.g. their settings and contents not being restored
properly).
This action is reversible, but please be careful as the account may be affected permanently (e.g.
their settings and contents not being restored properly).
</Trans>
</AlertDescription>
</Alert>
@@ -116,12 +106,7 @@ export const AdminUserDisableDialog = ({
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<DialogFooter>
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -21,6 +14,11 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { match } from 'ts-pattern';
export type AdminUserEnableDialogProps = {
className?: string;
@@ -33,8 +31,7 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isPending: isEnablingUser } =
trpc.admin.user.enable.useMutation();
const { mutateAsync: enableUser, isPending: isEnablingUser } = trpc.admin.user.enable.useMutation();
const onEnableAccount = async () => {
try {
@@ -66,16 +63,13 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>Enable Account</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Enabling the account results in the user being able to use the account again, and all
the related features such as webhooks, teams, and API keys for example.
Enabling the account results in the user being able to use the account again, and all the related features
such as webhooks, teams, and API keys for example.
</Trans>
</AlertDescription>
</div>
@@ -103,20 +97,11 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<DialogFooter>
<Button
onClick={onEnableAccount}
loading={isEnablingUser}
disabled={email !== userToEnable.email}
>
<Button onClick={onEnableAccount} loading={isEnablingUser} disabled={email !== userToEnable.email}>
<Trans>Enable account</Trans>
</Button>
</DialogFooter>
@@ -1,11 +1,3 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -22,24 +14,26 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
export type AdminUserResetTwoFactorDialogProps = {
className?: string;
user: TGetUserResponse;
};
export const AdminUserResetTwoFactorDialog = ({
className,
user,
}: AdminUserResetTwoFactorDialogProps) => {
export const AdminUserResetTwoFactorDialog = ({ className, user }: AdminUserResetTwoFactorDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const [email, setEmail] = useState('');
const [open, setOpen] = useState(false);
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
trpc.admin.user.resetTwoFactor.useMutation();
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } = trpc.admin.user.resetTwoFactor.useMutation();
const onResetTwoFactor = async () => {
try {
@@ -64,9 +58,7 @@ export const AdminUserResetTwoFactorDialog = ({
AppErrorCode.UNAUTHORIZED,
() => msg`You are not authorized to reset two factor authentcation for this user.`,
)
.otherwise(
() => msg`An error occurred while resetting two factor authentication for the user.`,
);
.otherwise(() => msg`An error occurred while resetting two factor authentication for the user.`);
toast({
title: _(msg`Error`),
@@ -87,16 +79,13 @@ export const AdminUserResetTwoFactorDialog = ({
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
<div>
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Reset the users two factor authentication. This action is irreversible and will
disable two factor authentication for the user.
Reset the users two factor authentication. This action is irreversible and will disable two factor
authentication for the user.
</Trans>
</AlertDescription>
</div>
@@ -119,8 +108,7 @@ export const AdminUserResetTwoFactorDialog = ({
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>
This action is irreversible. Please ensure you have informed the user before
proceeding.
This action is irreversible. Please ensure you have informed the user before proceeding.
</Trans>
</AlertDescription>
</Alert>
@@ -132,12 +120,7 @@ export const AdminUserResetTwoFactorDialog = ({
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<DialogFooter>
@@ -1,20 +1,11 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Trans, useLingui } from '@lingui/react/macro';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { useState } from 'react';
import { useCurrentTeam } from '~/providers/team';
@@ -24,11 +15,7 @@ type AiFeaturesEnableDialogProps = {
onEnabled: () => void;
};
export const AiFeaturesEnableDialog = ({
open,
onOpenChange,
onEnabled,
}: AiFeaturesEnableDialogProps) => {
export const AiFeaturesEnableDialog = ({ open, onOpenChange, onEnabled }: AiFeaturesEnableDialogProps) => {
const { t } = useLingui();
const team = useCurrentTeam();
@@ -71,11 +58,7 @@ export const AiFeaturesEnableDialog = ({
onOpenChange(false);
} catch (err) {
console.error('Failed to enable AI features', err);
setError(
err instanceof Error
? err.message
: t`We couldn't enable AI features right now. Please try again.`,
);
setError(err instanceof Error ? err.message : t`We couldn't enable AI features right now. Please try again.`);
}
};
@@ -89,39 +72,38 @@ export const AiFeaturesEnableDialog = ({
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
Turn on AI detection to automatically find recipients and fields in your documents. AI
providers do not retain your data for training.
Turn on AI detection to automatically find recipients and fields in your documents. AI providers do not
retain your data for training.
</Trans>
</p>
<Alert variant="neutral">
<AlertDescription>
<Trans>
Your document content will be sent securely to our AI provider solely for detection
and will not be stored or used for training.
Your document content will be sent securely to our AI provider solely for detection and will not be
stored or used for training.
</Trans>
</AlertDescription>
</Alert>
{canEnableAiFeatures ? (
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
You're an admin. You can enable AI features for this team right away. Everyone on
the team will see AI detection once enabled.
You're an admin. You can enable AI features for this team right away. Everyone on the team will see AI
detection once enabled.
</Trans>
</p>
) : (
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
AI features are disabled for your team. Please ask your team owner or organisation
owner to enable them.
AI features are disabled for your team. Please ask your team owner or organisation owner to enable them.
</Trans>
</p>
)}
{error ? <p className="text-sm text-destructive">{error}</p> : null}
{error ? <p className="text-destructive text-sm">{error}</p> : null}
</div>
<DialogFooter>
@@ -1,29 +1,17 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label';
import { Textarea } from '@documenso/ui/primitives/textarea';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label';
import { Textarea } from '@documenso/ui/primitives/textarea';
import {
AiApiError,
type DetectFieldsProgressEvent,
detectFields,
} from '../../../server/api/ai/detect-fields.client';
import { AiApiError, type DetectFieldsProgressEvent, detectFields } from '../../../server/api/ai/detect-fields.client';
import { AnimatedDocumentScanner } from '../general/animated-document-scanner';
type DialogState = 'PROMPT' | 'PROCESSING' | 'REVIEW' | 'ERROR' | 'RATE_LIMITED';
@@ -171,20 +159,17 @@ export const AiFieldDetectionDialog = ({
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
We'll scan your document to find form fields like signature lines, text inputs,
checkboxes, and more. Detected fields will be suggested for you to review.
We'll scan your document to find form fields like signature lines, text inputs, checkboxes, and more.
Detected fields will be suggested for you to review.
</Trans>
</p>
<Alert className="flex items-center gap-2 space-y-0" variant="neutral">
<ShieldCheckIcon className="h-5 w-5 stroke-green-600" />
<AlertDescription className="mt-0">
<Trans>
Your document is processed securely using AI services that don't retain your
data.
</Trans>
<Trans>Your document is processed securely using AI services that don't retain your data.</Trans>
</AlertDescription>
</Alert>
@@ -200,7 +185,7 @@ export const AiFieldDetectionDialog = ({
rows={2}
className="resize-none"
/>
<p className="text-xs text-muted-foreground">
<p className="text-muted-foreground text-xs">
<Trans>Help the AI assign fields to the right recipients.</Trans>
</p>
</div>
@@ -231,7 +216,7 @@ export const AiFieldDetectionDialog = ({
<p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p>
{progress && (
<p className="mt-2 text-xs text-muted-foreground/60">
<p className="mt-2 text-muted-foreground/60 text-xs">
<Plural
value={progress.fieldsDetected}
one={
@@ -248,7 +233,7 @@ export const AiFieldDetectionDialog = ({
</p>
)}
<p className="mt-2 max-w-[40ch] text-center text-xs text-muted-foreground/60">
<p className="mt-2 max-w-[40ch] text-center text-muted-foreground/60 text-xs">
<Trans>This can take a minute or two depending on the size of your document.</Trans>
</p>
@@ -278,16 +263,16 @@ export const AiFieldDetectionDialog = ({
{detectedFields.length === 0 ? (
<div className="flex flex-col items-center py-8">
<FormInputIcon className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-4 text-center text-sm text-muted-foreground">
<p className="mt-4 text-center text-muted-foreground text-sm">
<Trans>No fields were detected in your document.</Trans>
</p>
<p className="mt-1 text-center text-xs text-muted-foreground/70">
<p className="mt-1 text-center text-muted-foreground/70 text-xs">
<Trans>You can add fields manually in the editor.</Trans>
</p>
</div>
) : (
<>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Plural
value={detectedFields.length}
one="We found # field in your document."
@@ -299,7 +284,7 @@ export const AiFieldDetectionDialog = ({
{fieldCountsByType.map(([type, count]) => (
<li key={type} className="flex items-center justify-between px-4 py-3">
<span className="text-sm">{_(FIELD_TYPE_LABELS[type]) || type}</span>
<span className="text-sm font-medium text-muted-foreground">{count}</span>
<span className="font-medium text-muted-foreground text-sm">{count}</span>
</li>
))}
</ul>
@@ -314,7 +299,7 @@ export const AiFieldDetectionDialog = ({
{detectedFields.length > 0 && (
<Button type="button" onClick={onAddFields}>
<CheckIcon className="-ml-1 mr-2 h-4 w-4" />
<CheckIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>Add fields</Trans>
</Button>
)}
@@ -331,11 +316,11 @@ export const AiFieldDetectionDialog = ({
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>Something went wrong while detecting fields.</Trans>
</p>
{error && <p className="mt-2 text-sm text-destructive">{error}</p>}
{error && <p className="mt-2 text-destructive text-sm">{error}</p>}
</div>
<DialogFooter>
@@ -358,10 +343,8 @@ export const AiFieldDetectionDialog = ({
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
<Trans>
You've made too many detection requests. Please wait a minute before trying again.
</Trans>
<p className="text-muted-foreground text-sm">
<Trans>You've made too many detection requests. Please wait a minute before trying again.</Trans>
</p>
</div>
@@ -1,22 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import {
AiApiError,
@@ -146,20 +138,17 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader>
<div>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
We'll scan your document to find signature fields and identify who needs to sign.
Detected recipients will be suggested for you to review.
We'll scan your document to find signature fields and identify who needs to sign. Detected recipients
will be suggested for you to review.
</Trans>
</p>
<Alert className="mt-4 flex items-center gap-2 space-y-0" variant="neutral">
<ShieldCheckIcon className="h-5 w-5 stroke-green-600" />
<AlertDescription className="mt-0">
<Trans>
Your document is processed securely using AI services that don't retain your
data.
</Trans>
<Trans>Your document is processed securely using AI services that don't retain your data.</Trans>
</AlertDescription>
</Alert>
</div>
@@ -189,7 +178,7 @@ export const AiRecipientDetectionDialog = ({
<p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p>
{progress && (
<p className="mt-2 text-xs text-muted-foreground/60">
<p className="mt-2 text-muted-foreground/60 text-xs">
<Plural
value={progress.recipientsDetected}
one={
@@ -206,7 +195,7 @@ export const AiRecipientDetectionDialog = ({
</p>
)}
<p className="mt-2 max-w-[40ch] text-center text-xs text-muted-foreground/60">
<p className="mt-2 max-w-[40ch] text-center text-muted-foreground/60 text-xs">
<Trans>This can take a minute or two depending on the size of your document.</Trans>
</p>
@@ -236,16 +225,16 @@ export const AiRecipientDetectionDialog = ({
{detectedRecipients.length === 0 ? (
<div className="flex flex-col items-center py-8">
<UserIcon className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-4 text-center text-sm text-muted-foreground">
<p className="mt-4 text-center text-muted-foreground text-sm">
<Trans>No recipients were detected in your document.</Trans>
</p>
<p className="mt-1 text-center text-xs text-muted-foreground/70">
<p className="mt-1 text-center text-muted-foreground/70 text-xs">
<Trans>You can add recipients manually in the editor.</Trans>
</p>
</div>
) : (
<>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Plural
value={detectedRecipients.length}
one="We found # recipient in your document."
@@ -265,13 +254,13 @@ export const AiRecipientDetectionDialog = ({
: '?'
}
primaryText={
<p className="text-sm font-medium text-foreground">
<p className="font-medium text-foreground text-sm">
{recipient.name || _(msg`Unknown name`)}
</p>
}
secondaryText={
<div className="text-xs text-muted-foreground">
<p className="italic text-muted-foreground/70">
<div className="text-muted-foreground text-xs">
<p className="text-muted-foreground/70 italic">
{recipient.email || _(msg`No email detected`)}
</p>
<p>{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}</p>
@@ -304,7 +293,7 @@ export const AiRecipientDetectionDialog = ({
{detectedRecipients.length > 0 && (
<Button type="button" onClick={onAddRecipients}>
<CheckIcon className="-ml-1 mr-2 h-4 w-4" />
<CheckIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>Add recipients</Trans>
</Button>
)}
@@ -321,11 +310,11 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>Something went wrong while detecting recipients.</Trans>
</p>
{error && <p className="mt-2 text-sm text-destructive">{error}</p>}
{error && <p className="mt-2 text-destructive text-sm">{error}</p>}
</div>
<DialogFooter>
@@ -349,10 +338,8 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
<Trans>
You've made too many detection requests. Please wait a minute before trying again.
</Trans>
<p className="text-muted-foreground text-sm">
<Trans>You've made too many detection requests. Please wait a minute before trying again.</Trans>
</p>
</div>
@@ -1,10 +1,3 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zEmail } from '@documenso/lib/utils/zod';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -15,15 +8,13 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
@@ -104,9 +95,8 @@ export function AssistantConfirmationDialog({
</DialogTitle>
<DialogDescription>
<Trans>
Are you sure you want to complete the document? This action cannot be undone.
Please ensure that you have completed prefilling all relevant fields before
proceeding.
Are you sure you want to complete the document? This action cannot be undone. Please ensure that you
have completed prefilling all relevant fields before proceeding.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -116,7 +106,7 @@ export function AssistantConfirmationDialog({
<div className="mt-4 flex flex-col gap-4">
{!isEditingNextSigner && (
<div>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
<Trans>
The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> (
@@ -147,11 +137,7 @@ export function AssistantConfirmationDialog({
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input
{...field}
className="mt-2"
placeholder={t`Enter the next signer's name`}
/>
<Input {...field} className="mt-2" placeholder={t`Enter the next signer's name`} />
</FormControl>
<FormMessage />
@@ -1,8 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import type { z } from 'zod';
import type { TLicenseClaim } from '@documenso/lib/types/license';
import { generateDefaultSubscriptionClaim } from '@documenso/lib/utils/organisations-claims';
import { trpc } from '@documenso/trpc/react';
@@ -18,6 +13,9 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import type { z } from 'zod';
import { SubscriptionClaimForm } from '../forms/subscription-claim-form';
@@ -75,12 +73,7 @@ export const ClaimCreateDialog = ({ licenseFlags }: ClaimCreateDialogProps) => {
licenseFlags={licenseFlags}
formSubmitTrigger={
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isPending}
>
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
<Trans>Cancel</Trans>
</Button>
@@ -1,7 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@@ -15,6 +11,8 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
export type ClaimDeleteDialogProps = {
claimId: string;
@@ -23,12 +21,7 @@ export type ClaimDeleteDialogProps = {
trigger: React.ReactNode;
};
export const ClaimDeleteDialog = ({
claimId,
claimName,
claimLocked,
trigger,
}: ClaimDeleteDialogProps) => {
export const ClaimDeleteDialog = ({ claimId, claimName, claimLocked, trigger }: ClaimDeleteDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
@@ -1,7 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import type { TLicenseClaim } from '@documenso/lib/types/license';
import { trpc } from '@documenso/trpc/react';
import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types';
@@ -16,6 +12,8 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { SubscriptionClaimForm } from '../forms/subscription-claim-form';
@@ -74,12 +72,7 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD
licenseFlags={licenseFlags}
formSubmitTrigger={
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isPending}
>
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
<Trans>Cancel</Trans>
</Button>
@@ -1,15 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@@ -23,16 +11,19 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { useCurrentTeam } from '~/providers/team';
@@ -165,7 +156,7 @@ export const DocumentMoveToFolderDialog = ({
</DialogHeader>
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-3 h-4 w-4" />
<Search className="absolute top-3 left-2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={_(msg`Search folders...`)}
value={searchTerm}
@@ -219,7 +210,7 @@ export const DocumentMoveToFolderDialog = ({
))}
{searchTerm && filteredFolders?.length === 0 && (
<div className="text-muted-foreground px-2 py-2 text-center text-sm">
<div className="px-2 py-2 text-center text-muted-foreground text-sm">
<Trans>No folders found</Trans>
</div>
)}
@@ -239,9 +230,7 @@ export const DocumentMoveToFolderDialog = ({
<Button
type="submit"
disabled={
isFoldersLoading || form.formState.isSubmitting || currentFolderId === null
}
disabled={isFoldersLoading || form.formState.isSubmitting || currentFolderId === null}
>
<Trans>Move</Trans>
</Button>
@@ -1,14 +1,3 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { SigningStatus, type Team, type User } from '@prisma/client';
import { History } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
@@ -28,14 +17,17 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { SigningStatus, type Team, type User } from '@prisma/client';
import { History } from 'lucide-react';
import { useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { useCurrentTeam } from '~/providers/team';
@@ -142,10 +134,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
render={({ field: { value, onChange } }) => (
<>
{recipients.map((recipient) => (
<FormItem
key={recipient.id}
className="flex flex-row items-center justify-between gap-x-3"
>
<FormItem key={recipient.id} className="flex flex-row items-center justify-between gap-x-3">
<FormLabel
className={cn('my-2 flex items-center gap-2 font-normal', {
'opacity-50': !value.includes(recipient.id),
@@ -1,11 +1,3 @@
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { P, match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -22,6 +14,11 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { useEffect, useState } from 'react';
import { match, P } from 'ts-pattern';
type EnvelopeDeleteDialogProps = {
id: string;
@@ -130,13 +127,13 @@ export const EnvelopeDeleteDialog = ({
<AlertDescription>
{type === EnvelopeType.DOCUMENT ? (
<Trans>
Please note that this action is <strong>irreversible</strong>. Once confirmed,
this document will be permanently deleted.
Please note that this action is <strong>irreversible</strong>. Once confirmed, this document will
be permanently deleted.
</Trans>
) : (
<Trans>
Please note that this action is <strong>irreversible</strong>. Once confirmed,
this template will be permanently deleted.
Please note that this action is <strong>irreversible</strong>. Once confirmed, this template will
be permanently deleted.
</Trans>
)}
</AlertDescription>
@@ -1,16 +1,3 @@
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import * as z from 'zod';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -32,27 +19,24 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { InfoIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import * as z from 'zod';
export type EnvelopeDistributeDialogProps = {
onDistribute?: () => Promise<void>;
@@ -66,10 +50,7 @@ export const ZEnvelopeDistributeFormSchema = z.object({
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
subject: z.string(),
message: z.string(),
distributionMethod: z
.nativeEnum(DocumentDistributionMethod)
.optional()
.default(DocumentDistributionMethod.EMAIL),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional().default(DocumentDistributionMethod.EMAIL),
}),
});
@@ -100,8 +81,7 @@ export const EnvelopeDistributeDialog = ({
emailReplyTo: envelope.documentMeta?.emailReplyTo || undefined,
subject: envelope.documentMeta?.subject ?? '',
message: envelope.documentMeta?.message ?? '',
distributionMethod:
envelope.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
distributionMethod: envelope.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
},
},
resolver: zodResolver(ZEnvelopeDistributeFormSchema),
@@ -114,16 +94,15 @@ export const EnvelopeDistributeDialog = ({
formState: { isSubmitting },
} = form;
const { data: emailData, isLoading: isLoadingEmails } =
trpc.enterprise.organisation.email.find.useQuery(
{
organisationId: organisation.id,
perPage: 100,
},
{
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
},
);
const { data: emailData, isLoading: isLoadingEmails } = trpc.enterprise.organisation.email.find.useQuery(
{
organisationId: organisation.id,
perPage: 100,
},
{
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
},
);
const emails = emailData?.data || [];
@@ -153,9 +132,7 @@ export const EnvelopeDistributeDialog = ({
recipientAuth: recipient.authOptions,
});
return (
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email
);
return (auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email;
});
}, [recipientsWithIndex, envelope.authOptions]);
@@ -310,14 +287,9 @@ export const EnvelopeDistributeDialog = ({
<Select
{...field}
value={field.value === null ? '-1' : field.value}
onValueChange={(value) =>
field.onChange(value === '-1' ? null : value)
}
onValueChange={(value) => field.onChange(value === '-1' ? null : value)}
>
<SelectTrigger
loading={isLoadingEmails}
className="bg-background"
>
<SelectTrigger loading={isLoadingEmails} className="bg-background">
<SelectValue />
</SelectTrigger>
@@ -346,8 +318,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem>
<FormLabel>
<Trans>
Reply To Email{' '}
<span className="text-muted-foreground">(Optional)</span>
Reply To Email <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
@@ -367,8 +338,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem>
<FormLabel>
<Trans>
Subject{' '}
<span className="text-muted-foreground">(Optional)</span>
Subject <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
@@ -387,8 +357,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>
Message{' '}
<span className="text-muted-foreground">(Optional)</span>
Message <span className="text-muted-foreground">(Optional)</span>
</Trans>
<Tooltip>
<TooltipTrigger type="button">
@@ -422,15 +391,15 @@ export const EnvelopeDistributeDialog = ({
exit={{ opacity: 0, transition: { duration: 0.15 } }}
className="min-h-60 rounded-lg border"
>
<div className="py-24 text-center text-sm text-muted-foreground">
<div className="py-24 text-center text-muted-foreground text-sm">
<p>
<Trans>We won't send anything to notify recipients.</Trans>
</p>
<p className="mt-2">
<Trans>
We will generate signing links for you, which you can send to the
recipients through your method of choice.
We will generate signing links for you, which you can send to the recipients through your
method of choice.
</Trans>
</p>
</div>
@@ -470,7 +439,7 @@ export const EnvelopeDistributeDialog = ({
<AlertDescription>
<Trans>The following signers are missing signature fields:</Trans>
<ul className="ml-2 mt-1 list-inside list-disc">
<ul className="mt-1 ml-2 list-inside list-disc">
{recipientsMissingSignatureFields.map((recipient) => (
<li key={recipient.id}>
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
@@ -483,7 +452,7 @@ export const EnvelopeDistributeDialog = ({
<AlertDescription>
<Trans>The following recipients require an email address:</Trans>
<ul className="ml-2 mt-1 list-inside list-disc">
<ul className="mt-1 ml-2 list-inside list-disc">
{recipientsMissingRequiredEmail.map((recipient) => (
<li key={recipient.id}>
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
@@ -1,10 +1,3 @@
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, type EnvelopeItem } from '@prisma/client';
import { DownloadIcon, FileTextIcon } from 'lucide-react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
@@ -18,6 +11,10 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentStatus, type EnvelopeItem } from '@prisma/client';
import { DownloadIcon, FileTextIcon } from 'lucide-react';
import { useMemo, useState } from 'react';
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'envelopeId' | 'title' | 'order'>;
@@ -65,10 +62,8 @@ export const EnvelopeDownloadDialog = ({
[envelopeItemIdAndVersion: string]: boolean;
}>({});
const generateDownloadKey = (
envelopeItemId: string,
version: 'original' | 'signed' | 'pending',
) => `${envelopeItemId}-${version}`;
const generateDownloadKey = (envelopeItemId: string, version: 'original' | 'signed' | 'pending') =>
`${envelopeItemId}-${version}`;
// The dialog shows the original document alongside one of:
// - "Signed" (when the envelope is COMPLETED)
@@ -96,24 +91,20 @@ export const EnvelopeDownloadDialog = ({
return null;
}, [envelopeStatus, isLegacy, token, t]);
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
trpc.envelope.item.getManyByToken.useQuery(
{
envelopeId,
access: token ? { type: 'recipient', token } : { type: 'user' },
},
{
initialData: initialEnvelopeItems ? { data: initialEnvelopeItems } : undefined,
enabled: open,
},
);
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getManyByToken.useQuery(
{
envelopeId,
access: token ? { type: 'recipient', token } : { type: 'user' },
},
{
initialData: initialEnvelopeItems ? { data: initialEnvelopeItems } : undefined,
enabled: open,
},
);
const envelopeItems = envelopeItemsPayload?.data || [];
const onDownload = async (
envelopeItem: EnvelopeItemToDownload,
version: 'original' | 'signed' | 'pending',
) => {
const onDownload = async (envelopeItem: EnvelopeItemToDownload, version: 'original' | 'signed' | 'pending') => {
const { id: envelopeItemId } = envelopeItem;
if (isDownloadingState[generateDownloadKey(envelopeItemId, version)]) {
@@ -169,13 +160,9 @@ export const EnvelopeDownloadDialog = ({
</DialogHeader>
<div className="flex w-full flex-col gap-4 overflow-hidden">
{isLoadingEnvelopeItems ? (
<>
{Array.from({ length: 1 }).map((_, index) => (
<div
key={index}
className="flex items-center gap-2 rounded-lg border border-border bg-card p-4"
>
{isLoadingEnvelopeItems
? Array.from({ length: 1 }).map((_, index) => (
<div key={index} className="flex items-center gap-2 rounded-lg border border-border bg-card p-4">
<Skeleton className="h-10 w-10 flex-shrink-0 rounded-lg" />
<div className="flex w-full flex-col gap-2">
@@ -185,64 +172,59 @@ export const EnvelopeDownloadDialog = ({
<Skeleton className="h-10 w-20 flex-shrink-0 rounded-lg" />
</div>
))}
</>
) : (
envelopeItems.map((item) => (
<div
key={item.id}
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50"
>
<div className="flex-shrink-0">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<FileTextIcon className="h-5 w-5 text-primary" />
))
: envelopeItems.map((item) => (
<div
key={item.id}
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50"
>
<div className="flex-shrink-0">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<FileTextIcon className="h-5 w-5 text-primary" />
</div>
</div>
</div>
<div className="min-w-0 flex-1">
{/* Todo: Envelopes - Fix overflow */}
<h4 className="truncate text-sm font-medium text-foreground" title={item.title}>
{item.title}
</h4>
<p className="mt-0.5 text-xs text-muted-foreground">
<Trans>PDF Document</Trans>
</p>
</div>
<div className="min-w-0 flex-1">
{/* Todo: Envelopes - Fix overflow */}
<h4 className="truncate font-medium text-foreground text-sm" title={item.title}>
{item.title}
</h4>
<p className="mt-0.5 text-muted-foreground text-xs">
<Trans>PDF Document</Trans>
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={async () => onDownload(item, 'original')}
loading={isDownloadingState[generateDownloadKey(item.id, 'original')]}
>
{!isDownloadingState[generateDownloadKey(item.id, 'original')] && (
<DownloadIcon className="mr-2 h-4 w-4" />
)}
<Trans context="Original document (adjective)">Original</Trans>
</Button>
{secondaryDownload && (
<div className="flex flex-shrink-0 items-center gap-2">
<Button
variant="default"
variant="outline"
size="sm"
className="text-xs"
onClick={async () => onDownload(item, secondaryDownload.version)}
loading={
isDownloadingState[generateDownloadKey(item.id, secondaryDownload.version)]
}
onClick={async () => onDownload(item, 'original')}
loading={isDownloadingState[generateDownloadKey(item.id, 'original')]}
>
{!isDownloadingState[
generateDownloadKey(item.id, secondaryDownload.version)
] && <DownloadIcon className="mr-2 h-4 w-4" />}
{secondaryDownload.label}
{!isDownloadingState[generateDownloadKey(item.id, 'original')] && (
<DownloadIcon className="mr-2 h-4 w-4" />
)}
<Trans context="Original document (adjective)">Original</Trans>
</Button>
)}
{secondaryDownload && (
<Button
variant="default"
size="sm"
className="text-xs"
onClick={async () => onDownload(item, secondaryDownload.version)}
loading={isDownloadingState[generateDownloadKey(item.id, secondaryDownload.version)]}
>
{!isDownloadingState[generateDownloadKey(item.id, secondaryDownload.version)] && (
<DownloadIcon className="mr-2 h-4 w-4" />
)}
{secondaryDownload.label}
</Button>
)}
</div>
</div>
</div>
))
)}
))}
</div>
</DialogContent>
</Dialog>
@@ -1,9 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { useNavigate } from 'react-router';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
@@ -18,6 +12,10 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { useCurrentTeam } from '~/providers/team';
@@ -27,11 +25,7 @@ type EnvelopeDuplicateDialogProps = {
trigger?: React.ReactNode;
};
export const EnvelopeDuplicateDialog = ({
envelopeId,
envelopeType,
trigger,
}: EnvelopeDuplicateDialogProps) => {
export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: EnvelopeDuplicateDialogProps) => {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
@@ -43,23 +37,22 @@ export const EnvelopeDuplicateDialog = ({
const isDocument = envelopeType === EnvelopeType.DOCUMENT;
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
trpc.envelope.duplicate.useMutation({
onSuccess: async ({ id }) => {
toast({
title: isDocument ? t`Document Duplicated` : t`Template Duplicated`,
description: isDocument
? t`Your document has been successfully duplicated.`
: t`Your template has been successfully duplicated.`,
duration: 5000,
});
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({
onSuccess: async ({ id }) => {
toast({
title: isDocument ? t`Document Duplicated` : t`Template Duplicated`,
description: isDocument
? t`Your document has been successfully duplicated.`
: t`Your template has been successfully duplicated.`,
duration: 5000,
});
const path = isDocument ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
const path = isDocument ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
await navigate(`${path}/${id}/edit`);
setOpen(false);
},
});
await navigate(`${path}/${id}/edit`);
setOpen(false);
},
});
const onDuplicate = async () => {
try {
@@ -1,8 +1,3 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@@ -16,6 +11,8 @@ import {
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
export type EnvelopeItemDeleteDialogProps = {
canItemBeDeleted: boolean;
@@ -39,28 +36,27 @@ export const EnvelopeItemDeleteDialog = ({
const { t } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteEnvelopeItem, isPending: isDeleting } =
trpc.envelope.item.delete.useMutation({
onSuccess: () => {
toast({
title: t`Success`,
description: t`You have successfully removed this envelope item.`,
duration: 5000,
});
const { mutateAsync: deleteEnvelopeItem, isPending: isDeleting } = trpc.envelope.item.delete.useMutation({
onSuccess: () => {
toast({
title: t`Success`,
description: t`You have successfully removed this envelope item.`,
duration: 5000,
});
onDelete?.(envelopeItemId);
onDelete?.(envelopeItemId);
setOpen(false);
},
onError: () => {
toast({
title: t`An unknown error occurred`,
description: t`We encountered an unknown error while attempting to remove this envelope item. Please try again later.`,
variant: 'destructive',
duration: 10000,
});
},
});
setOpen(false);
},
onError: () => {
toast({
title: t`An unknown error occurred`,
description: t`We encountered an unknown error while attempting to remove this envelope item. Please try again later.`,
variant: 'destructive',
duration: 10000,
});
},
});
return (
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
@@ -74,16 +70,12 @@ export const EnvelopeItemDeleteDialog = ({
</DialogTitle>
<DialogDescription className="mt-4">
<Trans>
You are about to remove the following document and all associated fields
</Trans>
<Trans>You are about to remove the following document and all associated fields</Trans>
</DialogDescription>
</DialogHeader>
<Alert variant="neutral">
<AlertDescription className="text-center font-semibold">
{envelopeItemTitle}
</AlertDescription>
<AlertDescription className="text-center font-semibold">{envelopeItemTitle}</AlertDescription>
</Alert>
<fieldset disabled={isDeleting}>
@@ -116,9 +108,7 @@ export const EnvelopeItemDeleteDialog = ({
</DialogTitle>
<DialogDescription className="mt-4">
<Trans>
You cannot delete this item because the document has been sent to recipients.
</Trans>
<Trans>You cannot delete this item because the document has been sent to recipients.</Trans>
</DialogDescription>
</DialogHeader>
@@ -1,13 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { AlertTriangleIcon, FileIcon, UploadIcon, XIcon } from 'lucide-react';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
@@ -28,16 +18,17 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { AlertTriangleIcon, FileIcon, UploadIcon, XIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const ZEditEnvelopeItemFormSchema = z.object({
title: ZDocumentTitleSchema,
@@ -66,9 +57,7 @@ export const EnvelopeItemEditDialog = ({
const { envelope, editorFields, setLocalEnvelope, isEmbedded } = useCurrentEnvelopeEditor();
const [isOpen, setIsOpen] = useState(false);
const [replacementFile, setReplacementFile] = useState<{ file: File; pageCount: number } | null>(
null,
);
const [replacementFile, setReplacementFile] = useState<{ file: File; pageCount: number } | null>(null);
const [isDropping, setIsDropping] = useState(false);
const form = useForm<TEditEnvelopeItemFormSchema>({
@@ -82,9 +71,7 @@ export const EnvelopeItemEditDialog = ({
onSuccess: ({ data, fields }) => {
setLocalEnvelope({
envelopeItems: envelope.envelopeItems.map((item) =>
item.id === data.id
? { ...item, documentDataId: data.documentDataId, title: data.title }
: item,
item.id === data.id ? { ...item, documentDataId: data.documentDataId, title: data.title } : item,
),
});
@@ -98,8 +85,7 @@ export const EnvelopeItemEditDialog = ({
const fieldsOnExcessPages =
replacementFile !== null
? envelope.fields.filter(
(field) =>
field.envelopeItemId === envelopeItem.id && field.page > replacementFile.pageCount,
(field) => field.envelopeItemId === envelopeItem.id && field.page > replacementFile.pageCount,
)
: [];
@@ -222,11 +208,7 @@ export const EnvelopeItemEditDialog = ({
};
return (
<Dialog
{...props}
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<Dialog {...props} open={isOpen} onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger}
</DialogTrigger>
@@ -279,18 +261,12 @@ export const EnvelopeItemEditDialog = ({
<div className="flex min-w-0 items-center space-x-2">
<FileIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0">
<p className="truncate text-sm font-medium">
{replacementFile.file.name}
</p>
<p className="text-xs text-muted-foreground">
<p className="truncate font-medium text-sm">{replacementFile.file.name}</p>
<p className="text-muted-foreground text-xs">
{formatFileSize(replacementFile.file.size)}
{isDropping ? ' · …' : ' · '}
{!isDropping && replacementFile.pageCount !== null && (
<Plural
one="1 page"
other="# pages"
value={replacementFile.pageCount}
/>
<Plural one="1 page" other="# pages" value={replacementFile.pageCount} />
)}
</p>
</div>
@@ -326,14 +302,14 @@ export const EnvelopeItemEditDialog = ({
data-testid="envelope-item-edit-dropzone"
{...getRootProps()}
className={cn(
'mt-1.5 flex cursor-pointer items-center justify-center rounded-md border border-dashed border-border px-4 py-4 transition-colors',
'mt-1.5 flex cursor-pointer items-center justify-center rounded-md border border-border border-dashed px-4 py-4 transition-colors',
isDragActive
? 'border-primary/50 bg-primary/5'
: 'hover:border-muted-foreground/50 hover:bg-muted/50',
)}
>
<input {...getInputProps()} />
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<div className="flex items-center space-x-2 text-muted-foreground text-sm">
<UploadIcon className="h-4 w-4" />
<span>
<Trans>Drop PDF here or click to select</Trans>
@@ -1,13 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import type { TEnvelope } from '@documenso/lib/types/envelope';
import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient';
@@ -26,14 +16,15 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { StackAvatar } from '../general/stack-avatar';
@@ -52,10 +43,7 @@ export const ZEnvelopeRedistributeFormSchema = z.object({
export type TEnvelopeRedistributeFormSchema = z.infer<typeof ZEnvelopeRedistributeFormSchema>;
export const EnvelopeRedistributeDialog = ({
envelope,
trigger,
}: EnvelopeRedistributeDialogProps) => {
export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedistributeDialogProps) => {
const recipients = envelope.recipients;
const { toast } = useToast();
@@ -1,20 +1,12 @@
import { useEffect, useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { DOCUMENT_TITLE_MAX_LENGTH } from '@documenso/trpc/server/document-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
export type EnvelopeRenameDialogProps = {
id: string;
@@ -89,9 +81,7 @@ export const EnvelopeRenameDialog = ({
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isTemplate ? <Trans>Rename Template</Trans> : <Trans>Rename Document</Trans>}
</DialogTitle>
<DialogTitle>{isTemplate ? <Trans>Rename Template</Trans> : <Trans>Rename Document</Trans>}</DialogTitle>
</DialogHeader>
<div className="py-2">
@@ -1,9 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
@@ -20,6 +14,10 @@ import {
} from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { useCurrentTeam } from '~/providers/team';
@@ -153,10 +151,7 @@ export const EnvelopeSaveAsTemplateDialog = ({
disabled={!includeRecipients}
onCheckedChange={(checked) => field.onChange(checked === true)}
/>
<Label
htmlFor="envelopeIncludeFields"
className={!includeRecipients ? 'opacity-50' : ''}
>
<Label htmlFor="envelopeIncludeFields" className={!includeRecipients ? 'opacity-50' : ''}>
<Trans>Include Fields</Trans>
</Label>
</div>
@@ -1,9 +1,3 @@
import { plural } from '@lingui/core/macro';
import { Plural, useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@@ -16,6 +10,10 @@ import {
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { plural } from '@lingui/core/macro';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
export type EnvelopesBulkDeleteDialogProps = {
envelopeIds: string[];
@@ -88,9 +86,7 @@ export const EnvelopesBulkDeleteDialog = ({
<Dialog {...props} open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isDocument ? <Trans>Delete Documents</Trans> : <Trans>Delete Templates</Trans>}
</DialogTitle>
<DialogTitle>{isDocument ? <Trans>Delete Documents</Trans> : <Trans>Delete Templates</Trans>}</DialogTitle>
<DialogDescription>
{isDocument ? (
@@ -149,12 +145,7 @@ export const EnvelopesBulkDeleteDialog = ({
</Alert>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)} disabled={isPending}>
<Trans>Cancel</Trans>
</Button>
@@ -1,15 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
@@ -21,16 +9,18 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
export type EnvelopesBulkMoveDialogProps = {
envelopeIds: string[];
@@ -119,10 +109,7 @@ export const EnvelopesBulkMoveDialog = ({
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(
AppErrorCode.NOT_FOUND,
() => t`The folder you are trying to move the items to does not exist.`,
)
.with(AppErrorCode.NOT_FOUND, () => t`The folder you are trying to move the items to does not exist.`)
.with(AppErrorCode.UNAUTHORIZED, () => t`You are not allowed to move these items.`)
.with(AppErrorCode.INVALID_BODY, () => t`All items must be of the same type.`)
.otherwise(() => t`An error occurred while moving the items.`);
@@ -143,11 +130,7 @@ export const EnvelopesBulkMoveDialog = ({
<DialogContent>
<DialogHeader>
<DialogTitle>
{isDocument ? (
<Trans>Move Documents to Folder</Trans>
) : (
<Trans>Move Templates to Folder</Trans>
)}
{isDocument ? <Trans>Move Documents to Folder</Trans> : <Trans>Move Templates to Folder</Trans>}
</DialogTitle>
<DialogDescription>
@@ -168,7 +151,7 @@ export const EnvelopesBulkMoveDialog = ({
</DialogHeader>
<div className="relative">
<Search className="absolute left-2 top-3 h-4 w-4 text-muted-foreground" />
<Search className="absolute top-3 left-2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
@@ -222,7 +205,7 @@ export const EnvelopesBulkMoveDialog = ({
))}
{searchTerm && filteredFolders?.length === 0 && (
<div className="px-2 py-2 text-center text-sm text-muted-foreground">
<div className="px-2 py-2 text-center text-muted-foreground text-sm">
<Trans>No folders found</Trans>
</div>
)}
@@ -1,14 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type { FolderType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderPlusIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useParams } from 'react-router';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -20,16 +9,18 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type { FolderType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderPlusIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useParams } from 'react-router';
import { z } from 'zod';
const ZCreateFolderFormSchema = z.object({
name: z.string().min(1, { message: 'Folder name is required' }),
@@ -43,12 +34,7 @@ export type FolderCreateDialogProps = {
parentFolderId?: string | null;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const FolderCreateDialog = ({
type,
trigger,
parentFolderId,
...props
}: FolderCreateDialogProps) => {
export const FolderCreateDialog = ({ type, trigger, parentFolderId, ...props }: FolderCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { folderId } = useParams();
@@ -98,11 +84,7 @@ export const FolderCreateDialog = ({
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button
variant="outline"
className="flex items-center"
data-testid="folder-create-button"
>
<Button variant="outline" className="flex items-center" data-testid="folder-create-button">
<FolderPlusIcon className="mr-2 h-4 w-4" />
<Trans>Create Folder</Trans>
</Button>
@@ -139,11 +121,7 @@ export const FolderCreateDialog = ({
/>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => setIsCreateFolderOpen(false)}
>
<Button type="button" variant="secondary" onClick={() => setIsCreateFolderOpen(false)}>
<Trans>Cancel</Trans>
</Button>

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