Compare commits

...

31 Commits

Author SHA1 Message Date
ephraimduncan f3f5903760 chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/document-file-conversion. Conflicts were
format-only (Tailwind class ordering, single-line vs multi-line) plus
two semantic merges:

- files.helpers.ts: combine main's pending-PDF download path with the
  branch's original-source-file (DOCX/PNG/JPEG) download path
- download-pdf.ts: combine main's versionToFilenameSuffix helper with
  the branch's server-provided Content-Disposition filename support
2026-05-12 11:28:47 +00: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
ephraimduncan f153a7c437 fix: broken merge in team-account-folders E2E test
The merge of main into feat/document-file-conversion silently produced a
bad hybrid for 'can create a template inside a template folder': main
(#2303) replaced the click+setInputFiles flow with fileChooser.setFiles,
while this branch only touched .nth(0) within that same block. Git kept
both, leaving an obsolete click on 'Upload Template Document'. Apply
main's final form.
2026-04-20 10:48:49 +00:00
ephraimduncan f9cb8d84ed Merge branch 'main' into feat/document-file-conversion
Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller

Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
2026-04-20 10:11:35 +00:00
ephraimduncan cc67454513 fix: merge conflicts 2026-01-15 12:42:36 +00:00
Ephraim Duncan 665a0d0ea0 Merge branch 'main' into feat/document-file-conversion 2026-01-06 20:48:27 +00:00
ephraimduncan 02ff8df09b chore: remove tests - temp 2026-01-01 23:04:26 +00:00
ephraimduncan d4bee4aed1 fix: tests 2026-01-01 22:46:35 +00:00
ephraimduncan f4cfa71379 fix: tests 2026-01-01 10:44:55 +00:00
ephraimduncan b1763e422c fix: tests 2026-01-01 10:13:34 +00:00
ephraimduncan 64e0695811 fix: translations 2025-12-30 20:10:08 +00:00
ephraimduncan 8a4205d808 fix: build errors 2025-12-30 19:57:01 +00:00
ephraimduncan 74db3d7a1c feat: add document file conversion 2025-12-30 19:24:23 +00:00
ephraimduncan 3976531045 chore: fix merge conflicts 2025-12-29 20:02:51 +00:00
Ephraim Atta-Duncan 7a499270be feat: document file conversion 2025-12-15 11:58:42 +00:00
1492 changed files with 19694 additions and 29988 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.
+9 -1
View File
@@ -160,8 +160,16 @@ NEXT_PRIVATE_REDIS_PREFIX="documenso"
NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_KEY=""
# OPTIONAL: Leave blank to disable billing. # OPTIONAL: Leave blank to disable billing.
NEXT_PUBLIC_FEATURE_BILLING_ENABLED= 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= 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). # OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS= NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
# OPTIONAL: Set to true to use internal webapp url in browserless requests. # OPTIONAL: Set to true to use internal webapp url in browserless requests.
-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'],
};
-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", "typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "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.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false, "javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
@@ -15,8 +15,20 @@
"[prisma]": { "[prisma]": {
"editor.defaultFormatter": "Prisma.prisma" "editor.defaultFormatter": "Prisma.prisma"
}, },
"[typescriptreact]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "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:e2e` - Run E2E tests with Playwright
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode - `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 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 - `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. **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 | | Package | Description |
| ---------------------------- | ------------------------- | | ---------------------------- | ------------------------- |
| `@documenso/app-tests` | E2E tests (Playwright) | | `@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/tailwind-config` | Shared Tailwind config |
| `@documenso/tsconfig` | Shared TypeScript configs | | `@documenso/tsconfig` | Shared TypeScript configs |
+1 -1
View File
@@ -10,4 +10,4 @@
"baseDir": "src", "baseDir": "src",
"uiLibrary": "radix-ui", "uiLibrary": "radix-ui",
"commands": {} "commands": {}
} }
+1 -7
View File
@@ -1,10 +1,4 @@
{ {
"title": "Concepts", "title": "Concepts",
"pages": [ "pages": ["document-lifecycle", "recipient-roles", "field-types", "signing-workflow", "signing-certificates"]
"document-lifecycle",
"recipient-roles",
"field-types",
"signing-workflow",
"signing-certificates"
]
} }
@@ -1,13 +1,4 @@
{ {
"title": "API Reference", "title": "API Reference",
"pages": [ "pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
"documents",
"recipients",
"fields",
"templates",
"teams",
"rate-limits",
"versioning",
"developer-mode"
]
} }
@@ -224,28 +224,41 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
## Feature Flags ## Feature Flags
| Variable | Description | Default | | Variable | Description | Default |
| ------------------------------------- | ----------------------------------------------------------------------------------- | ------- | | -------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration entirely | `false` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Disable all signup methods application-wide | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | | | `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_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` | | `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
### Signup Restrictions ### 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. - **`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 ```bash
# Allow signups only from specific domains # Allow signups only from specific domains
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" 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 # Or disable signups entirely
NEXT_PUBLIC_DISABLE_SIGNUP="true" NEXT_PUBLIC_DISABLE_SIGNUP="true"
``` ```
@@ -371,6 +384,10 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
# Signup restrictions (optional) # Signup restrictions (optional)
# NEXT_PUBLIC_DISABLE_SIGNUP="true" # 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" # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
``` ```
@@ -155,7 +155,13 @@ PORT=3000
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
# Signup restrictions (optional) # Signup restrictions (optional)
# Master switch — disables every signup method
NEXT_PUBLIC_DISABLE_SIGNUP=false 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 # 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"> <Callout type="info">
All accounts created through signup are regular user accounts. Admin access must be granted 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 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`. `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
</Callout> </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_PASSPHRASE` | Passphrase for the signing certificate | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - | | `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_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 | | | `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). 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 | | Variable | Description | Default |
| --------------------------------- | ---------------------------------- | ------- | | --------------------------------- | ---------------------------------- | ------- |
| `PORT` | Application port | `3000` | | `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_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` | | `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
@@ -1,13 +1,4 @@
{ {
"title": "Organisations", "title": "Organisations",
"pages": [ "pages": ["overview", "create-team", "members", "groups", "email-domains", "preferences", "single-sign-on", "billing"]
"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. team.
</Callout> </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 ## User Provisioning
When a user signs in through your SSO portal for the first time: When a user signs in through your SSO portal for the first time:
+1 -1
View File
@@ -1,5 +1,5 @@
import { baseOptions } from '@/lib/layout.shared';
import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) { export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>; 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 type { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import {
BookOpenIcon,
CodeIcon,
FileTextIcon,
GithubIcon,
ServerIcon,
ShieldCheckIcon,
UserIcon,
} from 'lucide-react';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Documenso Docs', title: 'Documenso Docs',
description: description:
@@ -22,21 +13,21 @@ export default function HomePage() {
<main className="mx-auto max-w-4xl px-4 py-12"> <main className="mx-auto max-w-4xl px-4 py-12">
{/* Hero */} {/* Hero */}
<div className="mb-16 pt-6 text-center"> <div className="mb-16 pt-6 text-center">
<h1 className="mb-4 text-4xl font-bold tracking-tight">Documenso Documentation</h1> <h1 className="mb-4 font-bold text-4xl tracking-tight">Documenso Documentation</h1>
<p className="text-fd-muted-foreground mx-auto mb-8 max-w-2xl text-lg"> <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 The open-source document signing platform. Send documents for signatures, integrate with your apps, or
your apps, or self-host with full control. self-host with full control.
</p> </p>
<div className="flex flex-wrap justify-center gap-3"> <div className="flex flex-wrap justify-center gap-3">
<Link <Link
href="/docs/users" 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 Get Started
</Link> </Link>
<a <a
href="https://github.com/documenso/documenso" 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" /> <GithubIcon className="size-4" />
View on GitHub View on GitHub
@@ -48,64 +39,60 @@ export default function HomePage() {
<div className="mb-16 grid gap-4 md:grid-cols-3"> <div className="mb-16 grid gap-4 md:grid-cols-3">
<Link <Link
href="/docs/users" 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"> <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" /> <UserIcon className="size-6" />
</div> </div>
<h2 className="mb-2 text-lg font-semibold">User Guide</h2> <h2 className="mb-2 font-semibold text-lg">User Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm"> <p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Send documents, create templates, and manage your team using the web application. Send documents, create templates, and manage your team using the web application.
</p> </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>
<Link <Link
href="/docs/developers" 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"> <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" /> <CodeIcon className="size-6" />
</div> </div>
<h2 className="mb-2 text-lg font-semibold">Developer Guide</h2> <h2 className="mb-2 font-semibold text-lg">Developer Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm"> <p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Integrate document signing into your applications with the REST API, webhooks, and Integrate document signing into your applications with the REST API, webhooks, and embedding.
embedding.
</p> </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>
<Link <Link
href="/docs/self-hosting" 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"> <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" /> <ServerIcon className="size-6" />
</div> </div>
<h2 className="mb-2 text-lg font-semibold">Self-Hosting Guide</h2> <h2 className="mb-2 font-semibold text-lg">Self-Hosting Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm"> <p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
Deploy your own Documenso instance with Docker, Kubernetes, or Railway. Deploy your own Documenso instance with Docker, Kubernetes, or Railway.
</p> </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> </Link>
</div> </div>
{/* Quick Start & Core Concepts */} {/* Quick Start & Core Concepts */}
<div className="mb-16 grid gap-8 md:grid-cols-2"> <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"> <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 Quick Start
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h4 className="mb-2 text-sm font-medium">Send your first document</h4> <h4 className="mb-2 font-medium text-sm">Send your first document</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm"> <ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li> <li>
<Link <Link href="/docs/users/getting-started/create-account" className="text-fd-primary hover:underline">
href="/docs/users/getting-started/create-account"
className="text-fd-primary hover:underline"
>
Create an account Create an account
</Link> </Link>
</li> </li>
@@ -120,8 +107,8 @@ export default function HomePage() {
</ol> </ol>
</div> </div>
<div> <div>
<h4 className="mb-2 text-sm font-medium">Integrate with the API</h4> <h4 className="mb-2 font-medium text-sm">Integrate with the API</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm"> <ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li> <li>
<Link <Link
href="/docs/developers/getting-started/authentication" href="/docs/developers/getting-started/authentication"
@@ -141,8 +128,8 @@ export default function HomePage() {
</ol> </ol>
</div> </div>
<div> <div>
<h4 className="mb-2 text-sm font-medium">Deploy self-hosted</h4> <h4 className="mb-2 font-medium text-sm">Deploy self-hosted</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm"> <ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
<li> <li>
<Link <Link
href="/docs/self-hosting/getting-started/requirements" href="/docs/self-hosting/getting-started/requirements"
@@ -164,36 +151,36 @@ export default function HomePage() {
</div> </div>
</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"> <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 Core Concepts
</h3> </h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Link <Link
href="/docs/concepts/document-lifecycle" 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="mb-1 font-medium">Document Lifecycle</div>
<div className="text-fd-muted-foreground text-xs">Draft to completed</div> <div className="text-fd-muted-foreground text-xs">Draft to completed</div>
</Link> </Link>
<Link <Link
href="/docs/concepts/recipient-roles" 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="mb-1 font-medium">Recipient Roles</div>
<div className="text-fd-muted-foreground text-xs">Signers and approvers</div> <div className="text-fd-muted-foreground text-xs">Signers and approvers</div>
</Link> </Link>
<Link <Link
href="/docs/concepts/field-types" 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="mb-1 font-medium">Field Types</div>
<div className="text-fd-muted-foreground text-xs">Signatures and inputs</div> <div className="text-fd-muted-foreground text-xs">Signatures and inputs</div>
</Link> </Link>
<Link <Link
href="/docs/concepts/signing-certificates" 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="mb-1 font-medium">Signing Certificates</div>
<div className="text-fd-muted-foreground text-xs">Digital verification</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"> <div className="mb-16 grid gap-4 md:grid-cols-2">
<Link <Link
href="/docs/compliance" 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"> <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" /> <ShieldCheckIcon className="size-5" />
@@ -221,7 +208,7 @@ export default function HomePage() {
<Link <Link
href="/docs/policies" 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"> <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" /> <FileTextIcon className="size-5" />
@@ -236,22 +223,22 @@ export default function HomePage() {
</div> </div>
{/* Community CTA */} {/* Community CTA */}
<div className="from-fd-primary/5 to-fd-primary/10 rounded-xl border bg-gradient-to-r p-8 text-center"> <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 text-lg font-semibold">Join the Community</h3> <h3 className="mb-2 font-semibold text-lg">Join the Community</h3>
<p className="text-fd-muted-foreground mb-6 text-sm"> <p className="mb-6 text-fd-muted-foreground text-sm">
Documenso is open source. Contribute, ask questions, or share feedback. Documenso is open source. Contribute, ask questions, or share feedback.
</p> </p>
<div className="flex flex-wrap justify-center gap-3"> <div className="flex flex-wrap justify-center gap-3">
<a <a
href="https://github.com/documenso/documenso" 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" /> <GithubIcon className="size-4" />
GitHub GitHub
</a> </a>
<a <a
href="https://documen.so/discord" 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"> <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" /> <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>
<a <a
href="https://app.documenso.com/signup" 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 Try Documenso
</a> </a>
+1 -1
View File
@@ -1,5 +1,5 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server'; import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';
export const { GET } = createFromSource(source, { export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages // 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 type { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions'; import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
import { getPageImage, source } from '@/lib/source'; import { getPageImage, source } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components'; import { getMDXComponents } from '@/mdx-components';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
const gitConfig = { const gitConfig = {
user: 'documenso', user: 'documenso',
+11 -16
View File
@@ -1,16 +1,14 @@
'use client'; '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 type * as PageTree from 'fumadocs-core/page-tree';
import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { CodeIcon, ServerIcon, UserIcon } from 'lucide-react'; 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 = [ const ROOT_SECTIONS = [
{ {
@@ -44,7 +42,9 @@ function getFirstPageUrl(children: PageTree.Node[]): string | undefined {
} }
if (child.type === 'folder' && child.children.length > 0) { if (child.type === 'folder' && child.children.length > 0) {
const url = getFirstPageUrl(child.children); const url = getFirstPageUrl(child.children);
if (url) return url; if (url) {
return url;
}
} }
} }
return undefined; 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' : '')} /> <Icon className={cn('mt-0.5 size-4 shrink-0', isActive ? 'text-fd-primary' : '')} />
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{section.label}</span> <span className="font-medium text-sm">{section.label}</span>
<span <span className={cn('text-xs', isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70')}>
className={cn(
'text-xs',
isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70',
)}
>
{section.subtitle} {section.subtitle}
</span> </span>
</div> </div>
+13 -17
View File
@@ -1,6 +1,6 @@
@import 'tailwindcss'; @import "tailwindcss";
@import 'fumadocs-ui/css/shadcn.css'; @import "fumadocs-ui/css/shadcn.css";
@import 'fumadocs-ui/css/preset.css'; @import "fumadocs-ui/css/preset.css";
@theme { @theme {
/* Brand utility colors */ /* Brand utility colors */
@@ -43,13 +43,11 @@
--sidebar-border: hsl(223.8136 0.0001% 89.8161%); --sidebar-border: hsl(223.8136 0.0001% 89.8161%);
--sidebar-ring: hsl(223.8136 0% 63.0163%); --sidebar-ring: hsl(223.8136 0% 63.0163%);
--font-sans: --font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
'Segoe UI Symbol', 'Noto Color Emoji'; "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--radius: 0.5rem; --radius: 0.5rem;
--shadow-x: 0; --shadow-x: 0;
--shadow-y: 1px; --shadow-y: 1px;
@@ -103,13 +101,11 @@
--sidebar-border: hsl(223.8136 0% 15.5096%); --sidebar-border: hsl(223.8136 0% 15.5096%);
--sidebar-ring: hsl(223.8136 0% 32.1993%); --sidebar-ring: hsl(223.8136 0% 32.1993%);
--font-sans: --font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
'Segoe UI Symbol', 'Noto Color Emoji'; "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--radius: 0.5rem; --radius: 0.5rem;
--shadow-x: 0; --shadow-x: 0;
--shadow-y: 1px; --shadow-y: 1px;
+2 -4
View File
@@ -1,7 +1,6 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { RootProvider } from 'fumadocs-ui/provider/next';
import PlausibleProvider from 'next-plausible'; import PlausibleProvider from 'next-plausible';
import './global.css'; import './global.css';
@@ -16,8 +15,7 @@ export const metadata: Metadata = {
template: '%s | Documenso Docs', template: '%s | Documenso Docs',
default: 'Documenso Docs', default: 'Documenso Docs',
}, },
description: description: 'The official documentation for Documenso, the open-source document signing platform.',
'The official documentation for Documenso, the open-source document signing platform.',
openGraph: { openGraph: {
siteName: 'Documenso Docs', siteName: 'Documenso Docs',
type: 'website', type: 'website',
+4 -4
View File
@@ -3,20 +3,20 @@ import Link from 'next/link';
export default function NotFound() { export default function NotFound() {
return ( return (
<main className="mx-auto flex max-w-xl flex-col items-center justify-center px-4 py-32 text-center"> <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> <h1 className="font-bold text-4xl tracking-tight">Page not found</h1>
<p className="text-fd-muted-foreground mt-4 text-lg"> <p className="mt-4 text-fd-muted-foreground text-lg">
The page you are looking for may have moved. Our documentation was recently restructured. The page you are looking for may have moved. Our documentation was recently restructured.
</p> </p>
<div className="mt-8 flex flex-wrap justify-center gap-3"> <div className="mt-8 flex flex-wrap justify-center gap-3">
<Link <Link
href="/docs/users" 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 Browse documentation
</Link> </Link>
<Link <Link
href="/" 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 Go to homepage
</Link> </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 { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url'; 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; export const revalidate = false;
const loadAssets = async () => { const loadAssets = async () => {
const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([ const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([
readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))), readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))),
readFile( readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url))),
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-semibold.ttf', import.meta.url)),
),
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-bold.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(); const { logoSrc, fonts } = await loadAssets();
return new ImageResponse( 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 <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', flex: 1,
height: '100%', justifyContent: 'center',
backgroundColor: 'white', gap: '16px',
padding: '60px 80px',
fontFamily: 'Inter',
position: 'relative',
}} }}
> >
{/* Green accent bar */} <h1
<div
style={{ style={{
position: 'absolute', color: '#18181B',
top: 0, fontSize: page.data.title.length > 40 ? '48px' : '56px',
left: 0, fontWeight: 700,
right: 0, lineHeight: 1.15,
height: '6px', letterSpacing: '-0.025em',
backgroundColor: '#6DC947', margin: 0,
}}
/>
{/* Top: Logo */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {page.data.title}
<img src={logoSrc} alt="Documenso" height="28" /> </h1>
<span {page.data.description && (
<p
style={{ style={{
color: '#D4D4D8', color: '#71717A',
fontSize: '28px', fontSize: '22px',
fontWeight: 400, fontWeight: 400,
}} lineHeight: 1.4,
>
|
</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',
margin: 0, margin: 0,
maxWidth: '900px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}} }}
> >
{page.data.title} {page.data.description}
</h1> </p>
{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>
</div> </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, width: 1200,
height: 630, height: 630,
+12 -22
View File
@@ -1,12 +1,11 @@
'use client'; 'use client';
import { useMemo, useState } from 'react';
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button'; import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react'; 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>(); const cache = new Map<string, string>();
@@ -21,7 +20,9 @@ export function LLMCopyButton({
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => { const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl); const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached); if (cached) {
return navigator.clipboard.writeText(cached);
}
setLoading(true); setLoading(true);
@@ -48,7 +49,7 @@ export function LLMCopyButton({
buttonVariants({ buttonVariants({
color: 'secondary', color: 'secondary',
size: 'sm', 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} onClick={onClick}
@@ -74,8 +75,7 @@ export function ViewOptions({
githubUrl: string; githubUrl: string;
}) { }) {
const items = useMemo(() => { const items = useMemo(() => {
const fullMarkdownUrl = const fullMarkdownUrl = typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`; const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [ return [
@@ -96,12 +96,7 @@ export function ViewOptions({
q, q,
})}`, })}`,
icon: ( icon: (
<svg <svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title> <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" /> <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> </svg>
@@ -113,12 +108,7 @@ export function ViewOptions({
q, q,
})}`, })}`,
icon: ( icon: (
<svg <svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic</title> <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" /> <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> </svg>
@@ -146,7 +136,7 @@ export function ViewOptions({
)} )}
> >
Open Open
<ChevronDown className="text-fd-muted-foreground size-3.5" /> <ChevronDown className="size-3.5 text-fd-muted-foreground" />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="flex flex-col"> <PopoverContent className="flex flex-col">
{items.map((item) => ( {items.map((item) => (
@@ -155,11 +145,11 @@ export function ViewOptions({
href={item.href} href={item.href}
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" 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.icon}
{item.title} {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> </a>
))} ))}
</PopoverContent> </PopoverContent>
+1 -2
View File
@@ -1,8 +1,7 @@
'use client'; 'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect, useId, useRef, useState } from 'react';
export const Mermaid = ({ chart }: { chart: string }) => { export const Mermaid = ({ chart }: { chart: string }) => {
const [mounted, setMounted] = useState(false); 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 * as PageTree from 'fumadocs-core/page-tree';
import { type InferPageType, loader } from 'fumadocs-core/source'; import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; 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 // See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({ export const source = loader({
@@ -30,9 +30,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
// Find the main section folder // Find the main section folder
const rootFolder = fullTree.children.find( const rootFolder = fullTree.children.find(
(child): child is PageTree.Folder => (child): child is PageTree.Folder =>
child.type === 'folder' && child.type === 'folder' && typeof child.name === 'string' && child.name.toLowerCase() === rootName.toLowerCase(),
typeof child.name === 'string' &&
child.name.toLowerCase() === rootName.toLowerCase(),
); );
if (!rootFolder) { if (!rootFolder) {
@@ -42,9 +40,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
// Find shared section folders // Find shared section folders
const sharedFolders = fullTree.children.filter( const sharedFolders = fullTree.children.filter(
(child): child is PageTree.Folder => (child): child is PageTree.Folder =>
child.type === 'folder' && child.type === 'folder' && typeof child.name === 'string' && SHARED_SECTIONS.includes(child.name.toLowerCase()),
typeof child.name === 'string' &&
SHARED_SECTIONS.includes(child.name.toLowerCase()),
); );
// Create separator for main section // 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 * as TabsComponents from 'fumadocs-ui/components/tabs';
import defaultMdxComponents from 'fumadocs-ui/mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types'; import type { MDXComponents } from 'mdx/types';
import { Mermaid } from '@/components/mdx/mermaid';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMDXComponents(components?: MDXComponents): any { export function getMDXComponents(components?: MDXComponents): any {
+6 -22
View File
@@ -2,11 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"target": "ESNext", "target": "ESNext",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -20,12 +16,8 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"],
"./src/*" "fumadocs-mdx:collections/*": [".source/*"]
],
"fumadocs-mdx:collections/*": [
".source/*"
]
}, },
"plugins": [ "plugins": [
{ {
@@ -33,14 +25,6 @@
} }
] ]
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"next-env.d.ts", "exclude": ["node_modules"]
"**/*.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'; const FORMAT = 'MMM yyyy';
export const addZeroMonth = ( export const addZeroMonth = (transformedData: TransformedData, isCumulative = false): TransformedData => {
transformedData: TransformedData,
isCumulative = false,
): TransformedData => {
const result: TransformedData = { const result: TransformedData = {
labels: [...transformedData.labels], labels: [...transformedData.labels],
datasets: transformedData.datasets.map((dataset) => ({ 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 reqOrigin = req.headers.get('Origin') || undefined;
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin; const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
if (!value) return; if (!value) {
return;
}
return getOriginHeaders(reqOrigin, value); return getOriginHeaders(reqOrigin, value);
} }
@@ -85,12 +87,17 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
const { headers } = res; const { headers } = res;
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false); const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
const mergeHeaders = (v: string, k: string) => { const mergeHeaders = (v: string, k: string) => {
if (k === 'Vary') headers.append(k, v); if (k === 'Vary') {
else headers.set(k, v); headers.append(k, v);
} else {
headers.set(k, v);
}
}; };
// If there's no origin we won't touch the response // If there's no origin we won't touch the response
if (!originHeaders) return res; if (!originHeaders) {
return res;
}
originHeaders.forEach(mergeHeaders); 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'); headers.set('Access-Control-Allow-Credentials', 'true');
} }
const exposed = Array.isArray(opts.exposedHeaders) const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(',') : opts.exposedHeaders;
? opts.exposedHeaders.join(',')
: opts.exposedHeaders;
if (exposed) { if (exposed) {
headers.set('Access-Control-Expose-Headers', 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)); headers.set('Access-Control-Max-Age', String(opts.maxAge));
} }
if (opts.preflightContinue) return res; if (opts.preflightContinue) {
return res;
}
headers.set('Content-Length', '0'); headers.set('Content-Length', '0');
return new Response(null, { status: opts.optionsSuccessStatus, headers }); 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 { DocumentStatus, EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { addZeroMonth } from '../add-zero-month'; import { addZeroMonth } from '../add-zero-month';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => { export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
@@ -30,9 +29,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative'
datasets: [ datasets: [
{ {
label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents', label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents',
data: result data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
.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'); return addZeroMonth(transformedData, type === 'cumulative');
}; };
export type GetCompletedDocumentsMonthlyResult = Awaited< export type GetCompletedDocumentsMonthlyResult = Awaited<ReturnType<typeof getCompletedDocumentsMonthly>>;
ReturnType<typeof getCompletedDocumentsMonthly>
>;
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma'; import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
import { addZeroMonth } from '../add-zero-month'; import { addZeroMonth } from '../add-zero-month';
@@ -29,9 +28,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' =
datasets: [ datasets: [
{ {
label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up', label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up',
data: result data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
.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'); return addZeroMonth(transformedData, type === 'cumulative');
}; };
export type GetSignerConversionMonthlyResult = Awaited< export type GetSignerConversionMonthlyResult = Awaited<ReturnType<typeof getSignerConversionMonthly>>;
ReturnType<typeof getSignerConversionMonthly>
>;
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma'; import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
import { addZeroMonth } from '../add-zero-month'; import { addZeroMonth } from '../add-zero-month';
@@ -26,9 +25,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count
datasets: [ datasets: [
{ {
label: type === 'count' ? 'New Users' : 'Total Users', label: type === 'count' ? 'New Users' : 'Total Users',
data: result data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
.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 { DateTime } from 'luxon';
import { type TransformedData, addZeroMonth } from './add-zero-month'; import { addZeroMonth, type TransformedData } from './add-zero-month';
type MetricKeys = { type MetricKeys = {
stars: number; stars: number;
@@ -24,13 +24,7 @@ const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
earlyAdopters: 'Customers', earlyAdopters: 'Customers',
}; };
export function transformData({ export function transformData({ data, metric }: { data: DataEntry; metric: MetricKey }): TransformedData {
data,
metric,
}: {
data: DataEntry;
metric: MetricKey;
}): TransformedData {
try { try {
if (!data || Object.keys(data).length === 0) { if (!data || Object.keys(data).length === 0) {
return { return {
+1 -7
View File
@@ -22,12 +22,6 @@
"@/*": ["./*"] "@/*": ["./*"]
} }
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"] "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 */ /* Inter Variable Fonts */
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
src: url('/fonts/inter-variablefont_opsz,wght.ttf') format('truetype-variations'); src: url("/fonts/inter-variablefont_opsz,wght.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -11,8 +11,8 @@
/* Inter Italic Variable Fonts */ /* Inter Italic Variable Fonts */
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
src: url('/fonts/inter-italic-variablefont_opsz,wght.ttf') format('truetype-variations'); src: url("/fonts/inter-italic-variablefont_opsz,wght.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
@@ -20,16 +20,16 @@
/* Caveat Variable Font */ /* Caveat Variable Font */
@font-face { @font-face {
font-family: 'Caveat'; font-family: "Caveat";
src: url('/fonts/caveat-variablefont_wght.ttf') format('truetype-variations'); src: url("/fonts/caveat-variablefont_wght.ttf") format("truetype-variations");
font-weight: 400 600; font-weight: 400 600;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Noto Sans'; font-family: "Noto Sans";
src: url('/fonts/noto-sans.ttf') format('truetype-variations'); src: url("/fonts/noto-sans.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -37,8 +37,8 @@
/* Korean noto sans */ /* Korean noto sans */
@font-face { @font-face {
font-family: 'Noto Sans Korean'; font-family: "Noto Sans Korean";
src: url('/fonts/noto-sans-korean.ttf') format('truetype-variations'); src: url("/fonts/noto-sans-korean.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -46,8 +46,8 @@
/* Japanese noto sans */ /* Japanese noto sans */
@font-face { @font-face {
font-family: 'Noto Sans Japanese'; font-family: "Noto Sans Japanese";
src: url('/fonts/noto-sans-japanese.ttf') format('truetype-variations'); src: url("/fonts/noto-sans-japanese.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -55,8 +55,8 @@
/* Chinese noto sans */ /* Chinese noto sans */
@font-face { @font-face {
font-family: 'Noto Sans Chinese'; font-family: "Noto Sans Chinese";
src: url('/fonts/noto-sans-chinese.ttf') format('truetype-variations'); src: url("/fonts/noto-sans-chinese.ttf") format("truetype-variations");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -64,8 +64,8 @@
@layer base { @layer base {
:root { :root {
--font-sans: 'Inter'; --font-sans: "Inter";
--font-signature: 'Caveat'; --font-signature: "Caveat";
--font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese'; --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 { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -21,6 +15,10 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AccountDeleteDialogProps = {
className?: string; className?: string;
@@ -36,8 +34,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
const [enteredEmail, setEnteredEmail] = useState<string>(''); const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } = const { mutateAsync: deleteAccount, isPending: isDeletingAccount } = trpc.profile.deleteAccount.useMutation();
trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {
try { try {
@@ -63,18 +60,15 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle> <AlertTitle>
<Trans>Delete Account</Trans> <Trans>Delete Account</Trans>
</AlertTitle> </AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>
Delete your account and all its contents, including completed documents. This action Delete your account and all its contents, including completed documents. This action is irreversible and
is irreversible and will cancel your subscription, so proceed with caution. will cancel your subscription, so proceed with caution.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -109,10 +103,8 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
<DialogDescription> <DialogDescription>
<Trans> <Trans>
Documenso will delete{' '} Documenso will delete <span className="font-semibold">all of your documents</span>, along with all
<span className="font-semibold">all of your documents</span>, along with all of of your completed documents, signatures, and all other resources belonging to your Account.
your completed documents, signatures, and all other resources belonging to your
Account.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -121,9 +113,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
<div> <div>
<Label> <Label>
<Trans> <Trans>
Please type{' '} Please type <span className="font-semibold text-muted-foreground">{user.email}</span> to confirm.
<span className="text-muted-foreground font-semibold">{user.email}</span> to
confirm.
</Trans> </Trans>
</Label> </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 { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -19,6 +12,11 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminDocumentDeleteDialogProps = {
envelopeId: string; envelopeId: string;
@@ -32,8 +30,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } = const { mutateAsync: deleteDocument, isPending: isDeletingDocument } = trpc.admin.document.delete.useMutation();
trpc.admin.document.delete.useMutation();
const handleDeleteDocument = async () => { const handleDeleteDocument = async () => {
try { try {
@@ -64,18 +61,13 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
return ( return (
<div> <div>
<div> <div>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle> <AlertTitle>
<Trans>Delete Document</Trans> <Trans>Delete Document</Trans>
</AlertTitle> </AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>Delete the document. This action is irreversible so proceed with caution.</Trans>
Delete the document. This action is irreversible so proceed with caution.
</Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -105,12 +97,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
<Trans>To confirm, please enter the reason</Trans> <Trans>To confirm, please enter the reason</Trans>
</DialogDescription> </DialogDescription>
<Input <Input className="mt-2" type="text" value={reason} onChange={(e) => setReason(e.target.value)} />
className="mt-2"
type="text"
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</div> </div>
<DialogFooter> <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 { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCreateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/create-admin-organisation.types'; import { ZCreateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/create-admin-organisation.types';
@@ -22,16 +12,16 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type OrganisationCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -44,11 +34,7 @@ const ZCreateAdminOrganisationFormSchema = ZCreateAdminOrganisationRequestSchema
type TCreateOrganisationFormSchema = z.infer<typeof ZCreateAdminOrganisationFormSchema>; type TCreateOrganisationFormSchema = z.infer<typeof ZCreateAdminOrganisationFormSchema>;
export const AdminOrganisationCreateDialog = ({ export const AdminOrganisationCreateDialog = ({ trigger, ownerUserId, ...props }: OrganisationCreateDialogProps) => {
trigger,
ownerUserId,
...props
}: OrganisationCreateDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@@ -101,11 +87,7 @@ export const AdminOrganisationCreateDialog = ({
}, [open, form]); }, [open, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button className="flex-shrink-0" variant="secondary"> <Button className="flex-shrink-0" variant="secondary">
@@ -127,10 +109,7 @@ export const AdminOrganisationCreateDialog = ({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -149,10 +128,7 @@ export const AdminOrganisationCreateDialog = ({
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription className="mt-0"> <AlertDescription className="mt-0">
<Trans> <Trans>You will need to configure any claims or subscription after creating this organisation</Trans>
You will need to configure any claims or subscription after creating this
organisation
</Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -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 { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -20,6 +13,11 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminOrganisationMemberDeleteDialogProps = {
organisationId: string; organisationId: string;
@@ -42,33 +40,32 @@ export const AdminOrganisationMemberDeleteDialog = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { mutateAsync: deleteOrganisationMember, isPending } = const { mutateAsync: deleteOrganisationMember, isPending } = trpc.admin.organisationMember.delete.useMutation({
trpc.admin.organisationMember.delete.useMutation({ onSuccess: async () => {
onSuccess: async () => { toast({
toast({ title: _(msg`Success`),
title: _(msg`Success`), description: _(msg`Member has been removed from the organisation.`),
description: _(msg`Member has been removed from the organisation.`), duration: 5000,
duration: 5000, });
});
setOpen(false); setOpen(false);
// Refresh the page to show updated data // Refresh the page to show updated data
await navigate(0); await navigate(0);
}, },
onError: (err) => { onError: (err) => {
const error = AppError.parseError(err); const error = AppError.parseError(err);
console.error(error); console.error(error);
toast({ toast({
title: _(msg`An error occurred`), title: _(msg`An error occurred`),
description: _(msg`We couldn't remove this member. Please try again later.`), description: _(msg`We couldn't remove this member. Please try again later.`),
variant: 'destructive', variant: 'destructive',
duration: 10000, duration: 10000,
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}> <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 { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetAdminOrganisationResponse } from '@documenso/trpc/server/admin-router/get-admin-organisation.types'; import type { TGetAdminOrganisationResponse } from '@documenso/trpc/server/admin-router/get-admin-organisation.types';
@@ -23,22 +11,18 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
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 { 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 = { export type AdminOrganisationMemberUpdateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -69,9 +53,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
// Determine the current role value for the form // Determine the current role value for the form
const currentRoleValue = isOwner const currentRoleValue = isOwner
? 'OWNER' ? 'OWNER'
: getHighestOrganisationRoleInGroup( : getHighestOrganisationRoleInGroup(organisationMember.organisationGroupMembers.map((ogm) => ogm.group));
organisationMember.organisationGroupMembers.map((ogm) => ogm.group),
);
const organisationMemberName = organisationMember.user.name ?? organisationMember.user.email; const organisationMemberName = organisationMember.user.name ?? organisationMember.user.email;
const form = useForm<ZUpdateOrganisationMemberSchema>({ const form = useForm<ZUpdateOrganisationMemberSchema>({
@@ -81,8 +63,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
}, },
}); });
const { mutateAsync: updateOrganisationMemberRole } = const { mutateAsync: updateOrganisationMemberRole } = trpc.admin.organisationMember.updateRole.useMutation();
trpc.admin.organisationMember.updateRole.useMutation();
const onFormSubmit = async ({ role }: ZUpdateOrganisationMemberSchema) => { const onFormSubmit = async ({ role }: ZUpdateOrganisationMemberSchema) => {
try { try {
@@ -135,11 +116,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
}, [open, currentRoleValue, form]); }, [open, currentRoleValue, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? ( {trigger ?? (
<Button variant="secondary"> <Button variant="secondary">
@@ -156,8 +133,7 @@ export const AdminOrganisationMemberUpdateDialog = ({
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>
You are currently updating <span className="font-bold">{organisationMemberName}</span> You are currently updating <span className="font-bold">{organisationMemberName}</span>.
.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </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 { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -15,14 +10,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useMemo, useState } from 'react';
export type AdminSwapSubscriptionDialogProps = { export type AdminSwapSubscriptionDialogProps = {
open: boolean; open: boolean;
@@ -68,8 +59,7 @@ export const AdminSwapSubscriptionDialog = ({
} }
const hasActiveSubscription = const hasActiveSubscription =
org.subscription && org.subscription && (org.subscription.status === 'ACTIVE' || org.subscription.status === 'PAST_DUE');
(org.subscription.status === 'ACTIVE' || org.subscription.status === 'PAST_DUE');
return !hasActiveSubscription; return !hasActiveSubscription;
}); });
@@ -133,15 +123,14 @@ export const AdminSwapSubscriptionDialog = ({
<DialogDescription> <DialogDescription>
<Trans> <Trans>
Move the subscription from "{sourceOrganisationName}" to another organisation owned by Move the subscription from "{sourceOrganisationName}" to another organisation owned by this user.
this user.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<fieldset className="flex flex-col space-y-4" disabled={isSubmitting}> <fieldset className="flex flex-col space-y-4" disabled={isSubmitting}>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm font-medium"> <label className="font-medium text-sm">
<Trans>Target Organisation</Trans> <Trans>Target Organisation</Trans>
</label> </label>
@@ -159,7 +148,7 @@ export const AdminSwapSubscriptionDialog = ({
</Select> </Select>
{eligibleOrgs.length === 0 && orgsData && ( {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> <Trans>No eligible organisations found. The target must be on the free plan.</Trans>
</p> </p>
)} )}
@@ -169,8 +158,8 @@ export const AdminSwapSubscriptionDialog = ({
<Alert variant="warning"> <Alert variant="warning">
<AlertDescription className="mt-0"> <AlertDescription className="mt-0">
<Trans> <Trans>
This will move the subscription from "{sourceOrganisationName}" to " This will move the subscription from "{sourceOrganisationName}" to "{selectedOrg.name}". The source
{selectedOrg.name}". The source organisation will be reset to the free plan. organisation will be reset to the free plan.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -181,12 +170,7 @@ export const AdminSwapSubscriptionDialog = ({
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button <Button type="button" onClick={onSubmit} disabled={!selectedOrgId} loading={isSubmitting}>
type="button"
onClick={onSubmit}
disabled={!selectedOrgId}
loading={isSubmitting}
>
<Trans>Move Subscription</Trans> <Trans>Move Subscription</Trans>
</Button> </Button>
</DialogFooter> </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 { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -20,6 +13,11 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminTeamMemberDeleteDialogProps = {
teamId: number; teamId: number;
@@ -93,8 +91,8 @@ export const AdminTeamMemberDeleteDialog = ({
<div> <div>
<DialogDescription> <DialogDescription>
<Trans> <Trans>
You are about to remove the following user from the team{' '} You are about to remove the following user from the team <span className="font-semibold">{teamName}</span>
<span className="font-semibold">{teamName}</span>: :
</Trans> </Trans>
</DialogDescription> </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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types'; import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -22,6 +14,12 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminUserDeleteDialogProps = {
className?: string; className?: string;
@@ -34,8 +32,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isPending: isDeletingUser } = const { mutateAsync: deleteUser, isPending: isDeletingUser } = trpc.admin.user.delete.useMutation();
trpc.admin.user.delete.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {
try { try {
@@ -69,16 +66,13 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle>Delete Account</AlertTitle> <AlertTitle>Delete Account</AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>
Delete the users account and all its contents. This action is irreversible and will Delete the users account and all its contents. This action is irreversible and will cancel their
cancel their subscription, so proceed with caution. subscription, so proceed with caution.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -111,12 +105,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
</Trans> </Trans>
</DialogDescription> </DialogDescription>
<Input <Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div> </div>
<DialogFooter> <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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types'; import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -21,23 +14,24 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminUserDisableDialogProps = {
className?: string; className?: string;
userToDisable: TGetUserResponse; userToDisable: TGetUserResponse;
}; };
export const AdminUserDisableDialog = ({ export const AdminUserDisableDialog = ({ className, userToDisable }: AdminUserDisableDialogProps) => {
className,
userToDisable,
}: AdminUserDisableDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isPending: isDisablingUser } = const { mutateAsync: disableUser, isPending: isDisablingUser } = trpc.admin.user.disable.useMutation();
trpc.admin.user.disable.useMutation();
const onDisableAccount = async () => { const onDisableAccount = async () => {
try { try {
@@ -69,16 +63,13 @@ export const AdminUserDisableDialog = ({
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle>Disable Account</AlertTitle> <AlertTitle>Disable Account</AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>
Disabling the user results in the user not being able to use the account. It also Disabling the user results in the user not being able to use the account. It also disables all the related
disables all the related contents such as subscription, webhooks, teams, and API keys. contents such as subscription, webhooks, teams, and API keys.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -100,9 +91,8 @@ export const AdminUserDisableDialog = ({
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription className="selection:bg-red-100"> <AlertDescription className="selection:bg-red-100">
<Trans> <Trans>
This action is reversible, but please be careful as the account may be This action is reversible, but please be careful as the account may be affected permanently (e.g.
affected permanently (e.g. their settings and contents not being restored their settings and contents not being restored properly).
properly).
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -116,12 +106,7 @@ export const AdminUserDisableDialog = ({
</Trans> </Trans>
</DialogDescription> </DialogDescription>
<Input <Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div> </div>
<DialogFooter> <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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types'; import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -21,6 +14,11 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminUserEnableDialogProps = {
className?: string; className?: string;
@@ -33,8 +31,7 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isPending: isEnablingUser } = const { mutateAsync: enableUser, isPending: isEnablingUser } = trpc.admin.user.enable.useMutation();
trpc.admin.user.enable.useMutation();
const onEnableAccount = async () => { const onEnableAccount = async () => {
try { try {
@@ -66,16 +63,13 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle>Enable Account</AlertTitle> <AlertTitle>Enable Account</AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>
Enabling the account results in the user being able to use the account again, and all Enabling the account results in the user being able to use the account again, and all the related features
the related features such as webhooks, teams, and API keys for example. such as webhooks, teams, and API keys for example.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -103,20 +97,11 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
</Trans> </Trans>
</DialogDescription> </DialogDescription>
<Input <Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button onClick={onEnableAccount} loading={isEnablingUser} disabled={email !== userToEnable.email}>
onClick={onEnableAccount}
loading={isEnablingUser}
disabled={email !== userToEnable.email}
>
<Trans>Enable account</Trans> <Trans>Enable account</Trans>
</Button> </Button>
</DialogFooter> </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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types'; import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
@@ -22,24 +14,26 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type AdminUserResetTwoFactorDialogProps = {
className?: string; className?: string;
user: TGetUserResponse; user: TGetUserResponse;
}; };
export const AdminUserResetTwoFactorDialog = ({ export const AdminUserResetTwoFactorDialog = ({ className, user }: AdminUserResetTwoFactorDialogProps) => {
className,
user,
}: AdminUserResetTwoFactorDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { revalidate } = useRevalidator(); const { revalidate } = useRevalidator();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } = const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } = trpc.admin.user.resetTwoFactor.useMutation();
trpc.admin.user.resetTwoFactor.useMutation();
const onResetTwoFactor = async () => { const onResetTwoFactor = async () => {
try { try {
@@ -64,9 +58,7 @@ export const AdminUserResetTwoFactorDialog = ({
AppErrorCode.UNAUTHORIZED, AppErrorCode.UNAUTHORIZED,
() => msg`You are not authorized to reset two factor authentcation for this user.`, () => msg`You are not authorized to reset two factor authentcation for this user.`,
) )
.otherwise( .otherwise(() => msg`An error occurred while resetting two factor authentication for the user.`);
() => msg`An error occurred while resetting two factor authentication for the user.`,
);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
@@ -87,16 +79,13 @@ export const AdminUserResetTwoFactorDialog = ({
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div> <div>
<AlertTitle>Reset Two Factor Authentication</AlertTitle> <AlertTitle>Reset Two Factor Authentication</AlertTitle>
<AlertDescription className="mr-2"> <AlertDescription className="mr-2">
<Trans> <Trans>
Reset the users two factor authentication. This action is irreversible and will Reset the users two factor authentication. This action is irreversible and will disable two factor
disable two factor authentication for the user. authentication for the user.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</div> </div>
@@ -119,8 +108,7 @@ export const AdminUserResetTwoFactorDialog = ({
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription className="selection:bg-red-100"> <AlertDescription className="selection:bg-red-100">
<Trans> <Trans>
This action is irreversible. Please ensure you have informed the user before This action is irreversible. Please ensure you have informed the user before proceeding.
proceeding.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -132,12 +120,7 @@ export const AdminUserResetTwoFactorDialog = ({
</Trans> </Trans>
</DialogDescription> </DialogDescription>
<Input <Input className="mt-2" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div> </div>
<DialogFooter> <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 { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
Dialog, import { Trans, useLingui } from '@lingui/react/macro';
DialogContent, import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
DialogFooter, import { useState } from 'react';
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
@@ -24,11 +15,7 @@ type AiFeaturesEnableDialogProps = {
onEnabled: () => void; onEnabled: () => void;
}; };
export const AiFeaturesEnableDialog = ({ export const AiFeaturesEnableDialog = ({ open, onOpenChange, onEnabled }: AiFeaturesEnableDialogProps) => {
open,
onOpenChange,
onEnabled,
}: AiFeaturesEnableDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const team = useCurrentTeam(); const team = useCurrentTeam();
@@ -71,11 +58,7 @@ export const AiFeaturesEnableDialog = ({
onOpenChange(false); onOpenChange(false);
} catch (err) { } catch (err) {
console.error('Failed to enable AI features', err); console.error('Failed to enable AI features', err);
setError( setError(err instanceof Error ? err.message : t`We couldn't enable AI features right now. Please try again.`);
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> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
Turn on AI detection to automatically find recipients and fields in your documents. AI Turn on AI detection to automatically find recipients and fields in your documents. AI providers do not
providers do not retain your data for training. retain your data for training.
</Trans> </Trans>
</p> </p>
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription> <AlertDescription>
<Trans> <Trans>
Your document content will be sent securely to our AI provider solely for detection Your document content will be sent securely to our AI provider solely for detection and will not be
and will not be stored or used for training. stored or used for training.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
{canEnableAiFeatures ? ( {canEnableAiFeatures ? (
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
You're an admin. You can enable AI features for this team right away. Everyone on You're an admin. You can enable AI features for this team right away. Everyone on the team will see AI
the team will see AI detection once enabled. detection once enabled.
</Trans> </Trans>
</p> </p>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
AI features are disabled for your team. Please ask your team owner or organisation AI features are disabled for your team. Please ask your team owner or organisation owner to enable them.
owner to enable them.
</Trans> </Trans>
</p> </p>
)} )}
{error ? <p className="text-sm text-destructive">{error}</p> : null} {error ? <p className="text-destructive text-sm">{error}</p> : null}
</div> </div>
<DialogFooter> <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 type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro'; import { Plural, Trans } from '@lingui/react/macro';
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react'; 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 { AiApiError, type DetectFieldsProgressEvent, detectFields } from '../../../server/api/ai/detect-fields.client';
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 { AnimatedDocumentScanner } from '../general/animated-document-scanner'; import { AnimatedDocumentScanner } from '../general/animated-document-scanner';
type DialogState = 'PROMPT' | 'PROCESSING' | 'REVIEW' | 'ERROR' | 'RATE_LIMITED'; type DialogState = 'PROMPT' | 'PROCESSING' | 'REVIEW' | 'ERROR' | 'RATE_LIMITED';
@@ -171,20 +159,17 @@ export const AiFieldDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
We'll scan your document to find form fields like signature lines, text inputs, We'll scan your document to find form fields like signature lines, text inputs, checkboxes, and more.
checkboxes, and more. Detected fields will be suggested for you to review. Detected fields will be suggested for you to review.
</Trans> </Trans>
</p> </p>
<Alert className="flex items-center gap-2 space-y-0" variant="neutral"> <Alert className="flex items-center gap-2 space-y-0" variant="neutral">
<ShieldCheckIcon className="h-5 w-5 stroke-green-600" /> <ShieldCheckIcon className="h-5 w-5 stroke-green-600" />
<AlertDescription className="mt-0"> <AlertDescription className="mt-0">
<Trans> <Trans>Your document is processed securely using AI services that don't retain your data.</Trans>
Your document is processed securely using AI services that don't retain your
data.
</Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -200,7 +185,7 @@ export const AiFieldDetectionDialog = ({
rows={2} rows={2}
className="resize-none" 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> <Trans>Help the AI assign fields to the right recipients.</Trans>
</p> </p>
</div> </div>
@@ -231,7 +216,7 @@ export const AiFieldDetectionDialog = ({
<p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p> <p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p>
{progress && ( {progress && (
<p className="mt-2 text-xs text-muted-foreground/60"> <p className="mt-2 text-muted-foreground/60 text-xs">
<Plural <Plural
value={progress.fieldsDetected} value={progress.fieldsDetected}
one={ one={
@@ -248,7 +233,7 @@ export const AiFieldDetectionDialog = ({
</p> </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> <Trans>This can take a minute or two depending on the size of your document.</Trans>
</p> </p>
@@ -278,16 +263,16 @@ export const AiFieldDetectionDialog = ({
{detectedFields.length === 0 ? ( {detectedFields.length === 0 ? (
<div className="flex flex-col items-center py-8"> <div className="flex flex-col items-center py-8">
<FormInputIcon className="h-12 w-12 text-muted-foreground/50" /> <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> <Trans>No fields were detected in your document.</Trans>
</p> </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> <Trans>You can add fields manually in the editor.</Trans>
</p> </p>
</div> </div>
) : ( ) : (
<> <>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Plural <Plural
value={detectedFields.length} value={detectedFields.length}
one="We found # field in your document." one="We found # field in your document."
@@ -299,7 +284,7 @@ export const AiFieldDetectionDialog = ({
{fieldCountsByType.map(([type, count]) => ( {fieldCountsByType.map(([type, count]) => (
<li key={type} className="flex items-center justify-between px-4 py-3"> <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">{_(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> </li>
))} ))}
</ul> </ul>
@@ -314,7 +299,7 @@ export const AiFieldDetectionDialog = ({
{detectedFields.length > 0 && ( {detectedFields.length > 0 && (
<Button type="button" onClick={onAddFields}> <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> <Trans>Add fields</Trans>
</Button> </Button>
)} )}
@@ -331,11 +316,11 @@ export const AiFieldDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div className="py-4"> <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> <Trans>Something went wrong while detecting fields.</Trans>
</p> </p>
{error && <p className="mt-2 text-sm text-destructive">{error}</p>} {error && <p className="mt-2 text-destructive text-sm">{error}</p>}
</div> </div>
<DialogFooter> <DialogFooter>
@@ -358,10 +343,8 @@ export const AiFieldDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>You've made too many detection requests. Please wait a minute before trying again.</Trans>
You've made too many detection requests. Please wait a minute before trying again.
</Trans>
</p> </p>
</div> </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 { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema'; import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
Dialog, import { msg } from '@lingui/core/macro';
DialogContent, import { useLingui } from '@lingui/react';
DialogFooter, import { Plural, Trans } from '@lingui/react/macro';
DialogHeader, import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
DialogTitle, import { useCallback, useEffect, useState } from 'react';
} from '@documenso/ui/primitives/dialog';
import { import {
AiApiError, AiApiError,
@@ -146,20 +138,17 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div> <div>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
We'll scan your document to find signature fields and identify who needs to sign. We'll scan your document to find signature fields and identify who needs to sign. Detected recipients
Detected recipients will be suggested for you to review. will be suggested for you to review.
</Trans> </Trans>
</p> </p>
<Alert className="mt-4 flex items-center gap-2 space-y-0" variant="neutral"> <Alert className="mt-4 flex items-center gap-2 space-y-0" variant="neutral">
<ShieldCheckIcon className="h-5 w-5 stroke-green-600" /> <ShieldCheckIcon className="h-5 w-5 stroke-green-600" />
<AlertDescription className="mt-0"> <AlertDescription className="mt-0">
<Trans> <Trans>Your document is processed securely using AI services that don't retain your data.</Trans>
Your document is processed securely using AI services that don't retain your
data.
</Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</div> </div>
@@ -189,7 +178,7 @@ export const AiRecipientDetectionDialog = ({
<p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p> <p className="mt-8 text-muted-foreground">{_(PROCESSING_MESSAGES[messageIndex])}</p>
{progress && ( {progress && (
<p className="mt-2 text-xs text-muted-foreground/60"> <p className="mt-2 text-muted-foreground/60 text-xs">
<Plural <Plural
value={progress.recipientsDetected} value={progress.recipientsDetected}
one={ one={
@@ -206,7 +195,7 @@ export const AiRecipientDetectionDialog = ({
</p> </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> <Trans>This can take a minute or two depending on the size of your document.</Trans>
</p> </p>
@@ -236,16 +225,16 @@ export const AiRecipientDetectionDialog = ({
{detectedRecipients.length === 0 ? ( {detectedRecipients.length === 0 ? (
<div className="flex flex-col items-center py-8"> <div className="flex flex-col items-center py-8">
<UserIcon className="h-12 w-12 text-muted-foreground/50" /> <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> <Trans>No recipients were detected in your document.</Trans>
</p> </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> <Trans>You can add recipients manually in the editor.</Trans>
</p> </p>
</div> </div>
) : ( ) : (
<> <>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Plural <Plural
value={detectedRecipients.length} value={detectedRecipients.length}
one="We found # recipient in your document." one="We found # recipient in your document."
@@ -265,13 +254,13 @@ export const AiRecipientDetectionDialog = ({
: '?' : '?'
} }
primaryText={ primaryText={
<p className="text-sm font-medium text-foreground"> <p className="font-medium text-foreground text-sm">
{recipient.name || _(msg`Unknown name`)} {recipient.name || _(msg`Unknown name`)}
</p> </p>
} }
secondaryText={ secondaryText={
<div className="text-xs text-muted-foreground"> <div className="text-muted-foreground text-xs">
<p className="italic text-muted-foreground/70"> <p className="text-muted-foreground/70 italic">
{recipient.email || _(msg`No email detected`)} {recipient.email || _(msg`No email detected`)}
</p> </p>
<p>{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}</p> <p>{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}</p>
@@ -304,7 +293,7 @@ export const AiRecipientDetectionDialog = ({
{detectedRecipients.length > 0 && ( {detectedRecipients.length > 0 && (
<Button type="button" onClick={onAddRecipients}> <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> <Trans>Add recipients</Trans>
</Button> </Button>
)} )}
@@ -321,11 +310,11 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div className="py-4"> <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> <Trans>Something went wrong while detecting recipients.</Trans>
</p> </p>
{error && <p className="mt-2 text-sm text-destructive">{error}</p>} {error && <p className="mt-2 text-destructive text-sm">{error}</p>}
</div> </div>
<DialogFooter> <DialogFooter>
@@ -349,10 +338,8 @@ export const AiRecipientDetectionDialog = ({
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>You've made too many detection requests. Please wait a minute before trying again.</Trans>
You've made too many detection requests. Please wait a minute before trying again.
</Trans>
</p> </p>
</div> </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 { zEmail } from '@documenso/lib/utils/zod';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@@ -15,15 +8,13 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; 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'; import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
@@ -104,9 +95,8 @@ export function AssistantConfirmationDialog({
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
<Trans> <Trans>
Are you sure you want to complete the document? This action cannot be undone. Are you sure you want to complete the document? This action cannot be undone. Please ensure that you
Please ensure that you have completed prefilling all relevant fields before have completed prefilling all relevant fields before proceeding.
proceeding.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -116,7 +106,7 @@ export function AssistantConfirmationDialog({
<div className="mt-4 flex flex-col gap-4"> <div className="mt-4 flex flex-col gap-4">
{!isEditingNextSigner && ( {!isEditingNextSigner && (
<div> <div>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
<Trans> <Trans>
The next recipient to sign this document will be{' '} The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> ( <span className="font-semibold">{form.watch('name')}</span> (
@@ -147,11 +137,7 @@ export function AssistantConfirmationDialog({
<Trans>Name</Trans> <Trans>Name</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} className="mt-2" placeholder={t`Enter the next signer's name`} />
{...field}
className="mt-2"
placeholder={t`Enter the next signer's name`}
/>
</FormControl> </FormControl>
<FormMessage /> <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 type { TLicenseClaim } from '@documenso/lib/types/license';
import { generateDefaultSubscriptionClaim } from '@documenso/lib/utils/organisations-claims'; import { generateDefaultSubscriptionClaim } from '@documenso/lib/utils/organisations-claims';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -18,6 +13,9 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { SubscriptionClaimForm } from '../forms/subscription-claim-form';
@@ -75,12 +73,7 @@ export const ClaimCreateDialog = ({ licenseFlags }: ClaimCreateDialogProps) => {
licenseFlags={licenseFlags} licenseFlags={licenseFlags}
formSubmitTrigger={ formSubmitTrigger={
<DialogFooter> <DialogFooter>
<Button <Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isPending}
>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
@@ -1,7 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -15,6 +11,8 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
export type ClaimDeleteDialogProps = { export type ClaimDeleteDialogProps = {
claimId: string; claimId: string;
@@ -23,12 +21,7 @@ export type ClaimDeleteDialogProps = {
trigger: React.ReactNode; trigger: React.ReactNode;
}; };
export const ClaimDeleteDialog = ({ export const ClaimDeleteDialog = ({ claimId, claimName, claimLocked, trigger }: ClaimDeleteDialogProps) => {
claimId,
claimName,
claimLocked,
trigger,
}: ClaimDeleteDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); 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 type { TLicenseClaim } from '@documenso/lib/types/license';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types'; import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types';
@@ -16,6 +12,8 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { SubscriptionClaimForm } from '../forms/subscription-claim-form';
@@ -74,12 +72,7 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD
licenseFlags={licenseFlags} licenseFlags={licenseFlags}
formSubmitTrigger={ formSubmitTrigger={
<DialogFooter> <DialogFooter>
<Button <Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isPending}
>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FolderType } from '@documenso/lib/types/folder-type'; import { FolderType } from '@documenso/lib/types/folder-type';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@@ -23,16 +11,19 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { useCurrentTeam } from '~/providers/team';
@@ -165,7 +156,7 @@ export const DocumentMoveToFolderDialog = ({
</DialogHeader> </DialogHeader>
<div className="relative"> <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 <Input
placeholder={_(msg`Search folders...`)} placeholder={_(msg`Search folders...`)}
value={searchTerm} value={searchTerm}
@@ -219,7 +210,7 @@ export const DocumentMoveToFolderDialog = ({
))} ))}
{searchTerm && filteredFolders?.length === 0 && ( {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> <Trans>No folders found</Trans>
</div> </div>
)} )}
@@ -239,9 +230,7 @@ export const DocumentMoveToFolderDialog = ({
<Button <Button
type="submit" type="submit"
disabled={ disabled={isFoldersLoading || form.formState.isSubmitting || currentFolderId === null}
isFoldersLoading || form.formState.isSubmitting || currentFolderId === null
}
> >
<Trans>Move</Trans> <Trans>Move</Trans>
</Button> </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 { useSession } from '@documenso/lib/client-only/providers/session';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import type { TRecipientLite } from '@documenso/lib/types/recipient'; import type { TRecipientLite } from '@documenso/lib/types/recipient';
@@ -28,14 +17,17 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu'; import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu';
import { import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { useCurrentTeam } from '~/providers/team';
@@ -142,10 +134,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<> <>
{recipients.map((recipient) => ( {recipients.map((recipient) => (
<FormItem <FormItem key={recipient.id} className="flex flex-row items-center justify-between gap-x-3">
key={recipient.id}
className="flex flex-row items-center justify-between gap-x-3"
>
<FormLabel <FormLabel
className={cn('my-2 flex items-center gap-2 font-normal', { className={cn('my-2 flex items-center gap-2 font-normal', {
'opacity-50': !value.includes(recipient.id), '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 { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { trpc as trpcReact } from '@documenso/trpc/react'; import { trpc as trpcReact } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -22,6 +14,11 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { type EnvelopeDeleteDialogProps = {
id: string; id: string;
@@ -130,13 +127,13 @@ export const EnvelopeDeleteDialog = ({
<AlertDescription> <AlertDescription>
{type === EnvelopeType.DOCUMENT ? ( {type === EnvelopeType.DOCUMENT ? (
<Trans> <Trans>
Please note that this action is <strong>irreversible</strong>. Once confirmed, Please note that this action is <strong>irreversible</strong>. Once confirmed, this document will
this document will be permanently deleted. be permanently deleted.
</Trans> </Trans>
) : ( ) : (
<Trans> <Trans>
Please note that this action is <strong>irreversible</strong>. Once confirmed, Please note that this action is <strong>irreversible</strong>. Once confirmed, this template will
this template will be permanently deleted. be permanently deleted.
</Trans> </Trans>
)} )}
</AlertDescription> </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 { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -32,27 +19,24 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { Textarea } from '@documenso/ui/primitives/textarea'; import { Textarea } from '@documenso/ui/primitives/textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type EnvelopeDistributeDialogProps = {
onDistribute?: () => Promise<void>; onDistribute?: () => Promise<void>;
@@ -66,10 +50,7 @@ export const ZEnvelopeDistributeFormSchema = z.object({
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()), emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
subject: z.string(), subject: z.string(),
message: z.string(), message: z.string(),
distributionMethod: z distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional().default(DocumentDistributionMethod.EMAIL),
.nativeEnum(DocumentDistributionMethod)
.optional()
.default(DocumentDistributionMethod.EMAIL),
}), }),
}); });
@@ -100,8 +81,7 @@ export const EnvelopeDistributeDialog = ({
emailReplyTo: envelope.documentMeta?.emailReplyTo || undefined, emailReplyTo: envelope.documentMeta?.emailReplyTo || undefined,
subject: envelope.documentMeta?.subject ?? '', subject: envelope.documentMeta?.subject ?? '',
message: envelope.documentMeta?.message ?? '', message: envelope.documentMeta?.message ?? '',
distributionMethod: distributionMethod: envelope.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
envelope.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
}, },
}, },
resolver: zodResolver(ZEnvelopeDistributeFormSchema), resolver: zodResolver(ZEnvelopeDistributeFormSchema),
@@ -114,16 +94,15 @@ export const EnvelopeDistributeDialog = ({
formState: { isSubmitting }, formState: { isSubmitting },
} = form; } = form;
const { data: emailData, isLoading: isLoadingEmails } = const { data: emailData, isLoading: isLoadingEmails } = trpc.enterprise.organisation.email.find.useQuery(
trpc.enterprise.organisation.email.find.useQuery( {
{ organisationId: organisation.id,
organisationId: organisation.id, perPage: 100,
perPage: 100, },
}, {
{ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, },
}, );
);
const emails = emailData?.data || []; const emails = emailData?.data || [];
@@ -153,9 +132,7 @@ export const EnvelopeDistributeDialog = ({
recipientAuth: recipient.authOptions, recipientAuth: recipient.authOptions,
}); });
return ( return (auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email;
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email
);
}); });
}, [recipientsWithIndex, envelope.authOptions]); }, [recipientsWithIndex, envelope.authOptions]);
@@ -310,14 +287,9 @@ export const EnvelopeDistributeDialog = ({
<Select <Select
{...field} {...field}
value={field.value === null ? '-1' : field.value} value={field.value === null ? '-1' : field.value}
onValueChange={(value) => onValueChange={(value) => field.onChange(value === '-1' ? null : value)}
field.onChange(value === '-1' ? null : value)
}
> >
<SelectTrigger <SelectTrigger loading={isLoadingEmails} className="bg-background">
loading={isLoadingEmails}
className="bg-background"
>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@@ -346,8 +318,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans> <Trans>
Reply To Email{' '} Reply To Email <span className="text-muted-foreground">(Optional)</span>
<span className="text-muted-foreground">(Optional)</span>
</Trans> </Trans>
</FormLabel> </FormLabel>
@@ -367,8 +338,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans> <Trans>
Subject{' '} Subject <span className="text-muted-foreground">(Optional)</span>
<span className="text-muted-foreground">(Optional)</span>
</Trans> </Trans>
</FormLabel> </FormLabel>
@@ -387,8 +357,7 @@ export const EnvelopeDistributeDialog = ({
<FormItem> <FormItem>
<FormLabel className="flex flex-row items-center"> <FormLabel className="flex flex-row items-center">
<Trans> <Trans>
Message{' '} Message <span className="text-muted-foreground">(Optional)</span>
<span className="text-muted-foreground">(Optional)</span>
</Trans> </Trans>
<Tooltip> <Tooltip>
<TooltipTrigger type="button"> <TooltipTrigger type="button">
@@ -422,15 +391,15 @@ export const EnvelopeDistributeDialog = ({
exit={{ opacity: 0, transition: { duration: 0.15 } }} exit={{ opacity: 0, transition: { duration: 0.15 } }}
className="min-h-60 rounded-lg border" 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> <p>
<Trans>We won't send anything to notify recipients.</Trans> <Trans>We won't send anything to notify recipients.</Trans>
</p> </p>
<p className="mt-2"> <p className="mt-2">
<Trans> <Trans>
We will generate signing links for you, which you can send to the We will generate signing links for you, which you can send to the recipients through your
recipients through your method of choice. method of choice.
</Trans> </Trans>
</p> </p>
</div> </div>
@@ -470,7 +439,7 @@ export const EnvelopeDistributeDialog = ({
<AlertDescription> <AlertDescription>
<Trans>The following signers are missing signature fields:</Trans> <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) => ( {recipientsMissingSignatureFields.map((recipient) => (
<li key={recipient.id}> <li key={recipient.id}>
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`} {recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
@@ -483,7 +452,7 @@ export const EnvelopeDistributeDialog = ({
<AlertDescription> <AlertDescription>
<Trans>The following recipients require an email address:</Trans> <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) => ( {recipientsMissingRequiredEmail.map((recipient) => (
<li key={recipient.id}> <li key={recipient.id}>
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`} {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 { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -18,6 +11,10 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'>; type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'envelopeId' | 'title' | 'order'>;
@@ -65,10 +62,8 @@ export const EnvelopeDownloadDialog = ({
[envelopeItemIdAndVersion: string]: boolean; [envelopeItemIdAndVersion: string]: boolean;
}>({}); }>({});
const generateDownloadKey = ( const generateDownloadKey = (envelopeItemId: string, version: 'original' | 'signed' | 'pending') =>
envelopeItemId: string, `${envelopeItemId}-${version}`;
version: 'original' | 'signed' | 'pending',
) => `${envelopeItemId}-${version}`;
// The dialog shows the original document alongside one of: // The dialog shows the original document alongside one of:
// - "Signed" (when the envelope is COMPLETED) // - "Signed" (when the envelope is COMPLETED)
@@ -96,24 +91,20 @@ export const EnvelopeDownloadDialog = ({
return null; return null;
}, [envelopeStatus, isLegacy, token, t]); }, [envelopeStatus, isLegacy, token, t]);
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getManyByToken.useQuery(
trpc.envelope.item.getManyByToken.useQuery( {
{ envelopeId,
envelopeId, access: token ? { type: 'recipient', token } : { type: 'user' },
access: token ? { type: 'recipient', token } : { type: 'user' }, },
}, {
{ initialData: initialEnvelopeItems ? { data: initialEnvelopeItems } : undefined,
initialData: initialEnvelopeItems ? { data: initialEnvelopeItems } : undefined, enabled: open,
enabled: open, },
}, );
);
const envelopeItems = envelopeItemsPayload?.data || []; const envelopeItems = envelopeItemsPayload?.data || [];
const onDownload = async ( const onDownload = async (envelopeItem: EnvelopeItemToDownload, version: 'original' | 'signed' | 'pending') => {
envelopeItem: EnvelopeItemToDownload,
version: 'original' | 'signed' | 'pending',
) => {
const { id: envelopeItemId } = envelopeItem; const { id: envelopeItemId } = envelopeItem;
if (isDownloadingState[generateDownloadKey(envelopeItemId, version)]) { if (isDownloadingState[generateDownloadKey(envelopeItemId, version)]) {
@@ -169,13 +160,9 @@ export const EnvelopeDownloadDialog = ({
</DialogHeader> </DialogHeader>
<div className="flex w-full flex-col gap-4 overflow-hidden"> <div className="flex w-full flex-col gap-4 overflow-hidden">
{isLoadingEnvelopeItems ? ( {isLoadingEnvelopeItems
<> ? Array.from({ length: 1 }).map((_, index) => (
{Array.from({ length: 1 }).map((_, index) => ( <div key={index} className="flex items-center gap-2 rounded-lg border border-border bg-card p-4">
<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" /> <Skeleton className="h-10 w-10 flex-shrink-0 rounded-lg" />
<div className="flex w-full flex-col gap-2"> <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" /> <Skeleton className="h-10 w-20 flex-shrink-0 rounded-lg" />
</div> </div>
))} ))
</> : envelopeItems.map((item) => (
) : ( <div
envelopeItems.map((item) => ( key={item.id}
<div className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50"
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">
<div className="flex-shrink-0"> <FileTextIcon className="h-5 w-5 text-primary" />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> </div>
<FileTextIcon className="h-5 w-5 text-primary" />
</div> </div>
</div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{/* Todo: Envelopes - Fix overflow */} {/* Todo: Envelopes - Fix overflow */}
<h4 className="truncate text-sm font-medium text-foreground" title={item.title}> <h4 className="truncate font-medium text-foreground text-sm" title={item.title}>
{item.title} {item.title}
</h4> </h4>
<p className="mt-0.5 text-xs text-muted-foreground"> <p className="mt-0.5 text-muted-foreground text-xs">
<Trans>PDF Document</Trans> <Trans>PDF Document</Trans>
</p> </p>
</div> </div>
<div className="flex flex-shrink-0 items-center gap-2"> <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 && (
<Button <Button
variant="default" variant="outline"
size="sm" size="sm"
className="text-xs" className="text-xs"
onClick={async () => onDownload(item, secondaryDownload.version)} onClick={async () => onDownload(item, 'original')}
loading={ loading={isDownloadingState[generateDownloadKey(item.id, 'original')]}
isDownloadingState[generateDownloadKey(item.id, secondaryDownload.version)]
}
> >
{!isDownloadingState[ {!isDownloadingState[generateDownloadKey(item.id, 'original')] && (
generateDownloadKey(item.id, secondaryDownload.version) <DownloadIcon className="mr-2 h-4 w-4" />
] && <DownloadIcon className="mr-2 h-4 w-4" />} )}
{secondaryDownload.label} <Trans context="Original document (adjective)">Original</Trans>
</Button> </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> ))}
))
)}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </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 { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -18,6 +12,10 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { useCurrentTeam } from '~/providers/team';
@@ -27,11 +25,7 @@ type EnvelopeDuplicateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const EnvelopeDuplicateDialog = ({ export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: EnvelopeDuplicateDialogProps) => {
envelopeId,
envelopeType,
trigger,
}: EnvelopeDuplicateDialogProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -43,23 +37,22 @@ export const EnvelopeDuplicateDialog = ({
const isDocument = envelopeType === EnvelopeType.DOCUMENT; const isDocument = envelopeType === EnvelopeType.DOCUMENT;
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({
trpc.envelope.duplicate.useMutation({ onSuccess: async ({ id }) => {
onSuccess: async ({ id }) => { toast({
toast({ title: isDocument ? t`Document Duplicated` : t`Template Duplicated`,
title: isDocument ? t`Document Duplicated` : t`Template Duplicated`, description: isDocument
description: isDocument ? t`Your document has been successfully duplicated.`
? t`Your document has been successfully duplicated.` : t`Your template has been successfully duplicated.`,
: t`Your template has been successfully duplicated.`, duration: 5000,
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`); await navigate(`${path}/${id}/edit`);
setOpen(false); setOpen(false);
}, },
}); });
const onDuplicate = async () => { const onDuplicate = async () => {
try { 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 { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -16,6 +11,8 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
export type EnvelopeItemDeleteDialogProps = { export type EnvelopeItemDeleteDialogProps = {
canItemBeDeleted: boolean; canItemBeDeleted: boolean;
@@ -39,28 +36,27 @@ export const EnvelopeItemDeleteDialog = ({
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: deleteEnvelopeItem, isPending: isDeleting } = const { mutateAsync: deleteEnvelopeItem, isPending: isDeleting } = trpc.envelope.item.delete.useMutation({
trpc.envelope.item.delete.useMutation({ onSuccess: () => {
onSuccess: () => { toast({
toast({ title: t`Success`,
title: t`Success`, description: t`You have successfully removed this envelope item.`,
description: t`You have successfully removed this envelope item.`, duration: 5000,
duration: 5000, });
});
onDelete?.(envelopeItemId); onDelete?.(envelopeItemId);
setOpen(false); setOpen(false);
}, },
onError: () => { onError: () => {
toast({ toast({
title: t`An unknown error occurred`, title: t`An unknown error occurred`,
description: t`We encountered an unknown error while attempting to remove this envelope item. Please try again later.`, description: t`We encountered an unknown error while attempting to remove this envelope item. Please try again later.`,
variant: 'destructive', variant: 'destructive',
duration: 10000, duration: 10000,
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
@@ -74,16 +70,12 @@ export const EnvelopeItemDeleteDialog = ({
</DialogTitle> </DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>You are about to remove the following document and all associated fields</Trans>
You are about to remove the following document and all associated fields
</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription className="text-center font-semibold"> <AlertDescription className="text-center font-semibold">{envelopeItemTitle}</AlertDescription>
{envelopeItemTitle}
</AlertDescription>
</Alert> </Alert>
<fieldset disabled={isDeleting}> <fieldset disabled={isDeleting}>
@@ -116,9 +108,7 @@ export const EnvelopeItemDeleteDialog = ({
</DialogTitle> </DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>You cannot delete this item because the document has been sent to recipients.</Trans>
You cannot delete this item because the document has been sent to recipients.
</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </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 { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
@@ -28,16 +18,17 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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({ const ZEditEnvelopeItemFormSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
@@ -66,9 +57,7 @@ export const EnvelopeItemEditDialog = ({
const { envelope, editorFields, setLocalEnvelope, isEmbedded } = useCurrentEnvelopeEditor(); const { envelope, editorFields, setLocalEnvelope, isEmbedded } = useCurrentEnvelopeEditor();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [replacementFile, setReplacementFile] = useState<{ file: File; pageCount: number } | null>( const [replacementFile, setReplacementFile] = useState<{ file: File; pageCount: number } | null>(null);
null,
);
const [isDropping, setIsDropping] = useState(false); const [isDropping, setIsDropping] = useState(false);
const form = useForm<TEditEnvelopeItemFormSchema>({ const form = useForm<TEditEnvelopeItemFormSchema>({
@@ -82,9 +71,7 @@ export const EnvelopeItemEditDialog = ({
onSuccess: ({ data, fields }) => { onSuccess: ({ data, fields }) => {
setLocalEnvelope({ setLocalEnvelope({
envelopeItems: envelope.envelopeItems.map((item) => envelopeItems: envelope.envelopeItems.map((item) =>
item.id === data.id item.id === data.id ? { ...item, documentDataId: data.documentDataId, title: data.title } : item,
? { ...item, documentDataId: data.documentDataId, title: data.title }
: item,
), ),
}); });
@@ -98,8 +85,7 @@ export const EnvelopeItemEditDialog = ({
const fieldsOnExcessPages = const fieldsOnExcessPages =
replacementFile !== null replacementFile !== null
? envelope.fields.filter( ? envelope.fields.filter(
(field) => (field) => field.envelopeItemId === envelopeItem.id && field.page > replacementFile.pageCount,
field.envelopeItemId === envelopeItem.id && field.page > replacementFile.pageCount,
) )
: []; : [];
@@ -222,11 +208,7 @@ export const EnvelopeItemEditDialog = ({
}; };
return ( return (
<Dialog <Dialog {...props} open={isOpen} onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}>
{...props}
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger} {trigger}
</DialogTrigger> </DialogTrigger>
@@ -279,18 +261,12 @@ export const EnvelopeItemEditDialog = ({
<div className="flex min-w-0 items-center space-x-2"> <div className="flex min-w-0 items-center space-x-2">
<FileIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> <FileIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0"> <div className="min-w-0">
<p className="truncate text-sm font-medium"> <p className="truncate font-medium text-sm">{replacementFile.file.name}</p>
{replacementFile.file.name} <p className="text-muted-foreground text-xs">
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(replacementFile.file.size)} {formatFileSize(replacementFile.file.size)}
{isDropping ? ' · …' : ' · '} {isDropping ? ' · …' : ' · '}
{!isDropping && replacementFile.pageCount !== null && ( {!isDropping && replacementFile.pageCount !== null && (
<Plural <Plural one="1 page" other="# pages" value={replacementFile.pageCount} />
one="1 page"
other="# pages"
value={replacementFile.pageCount}
/>
)} )}
</p> </p>
</div> </div>
@@ -326,14 +302,14 @@ export const EnvelopeItemEditDialog = ({
data-testid="envelope-item-edit-dropzone" data-testid="envelope-item-edit-dropzone"
{...getRootProps()} {...getRootProps()}
className={cn( 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 isDragActive
? 'border-primary/50 bg-primary/5' ? 'border-primary/50 bg-primary/5'
: 'hover:border-muted-foreground/50 hover:bg-muted/50', : 'hover:border-muted-foreground/50 hover:bg-muted/50',
)} )}
> >
<input {...getInputProps()} /> <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" /> <UploadIcon className="h-4 w-4" />
<span> <span>
<Trans>Drop PDF here or click to select</Trans> <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 { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import type { TEnvelope } from '@documenso/lib/types/envelope'; import type { TEnvelope } from '@documenso/lib/types/envelope';
import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient'; import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient';
@@ -26,14 +16,15 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { StackAvatar } from '../general/stack-avatar';
@@ -52,10 +43,7 @@ export const ZEnvelopeRedistributeFormSchema = z.object({
export type TEnvelopeRedistributeFormSchema = z.infer<typeof ZEnvelopeRedistributeFormSchema>; export type TEnvelopeRedistributeFormSchema = z.infer<typeof ZEnvelopeRedistributeFormSchema>;
export const EnvelopeRedistributeDialog = ({ export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedistributeDialogProps) => {
envelope,
trigger,
}: EnvelopeRedistributeDialogProps) => {
const recipients = envelope.recipients; const recipients = envelope.recipients;
const { toast } = useToast(); 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 { trpc as trpcReact } from '@documenso/trpc/react';
import { DOCUMENT_TITLE_MAX_LENGTH } from '@documenso/trpc/server/document-router/schema'; import { DOCUMENT_TITLE_MAX_LENGTH } from '@documenso/trpc/server/document-router/schema';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
export type EnvelopeRenameDialogProps = { export type EnvelopeRenameDialogProps = {
id: string; id: string;
@@ -89,9 +81,7 @@ export const EnvelopeRenameDialog = ({
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>{isTemplate ? <Trans>Rename Template</Trans> : <Trans>Rename Document</Trans>}</DialogTitle>
{isTemplate ? <Trans>Rename Template</Trans> : <Trans>Rename Document</Trans>}
</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-2"> <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 { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -20,6 +14,10 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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'; import { useCurrentTeam } from '~/providers/team';
@@ -153,10 +151,7 @@ export const EnvelopeSaveAsTemplateDialog = ({
disabled={!includeRecipients} disabled={!includeRecipients}
onCheckedChange={(checked) => field.onChange(checked === true)} onCheckedChange={(checked) => field.onChange(checked === true)}
/> />
<Label <Label htmlFor="envelopeIncludeFields" className={!includeRecipients ? 'opacity-50' : ''}>
htmlFor="envelopeIncludeFields"
className={!includeRecipients ? 'opacity-50' : ''}
>
<Trans>Include Fields</Trans> <Trans>Include Fields</Trans>
</Label> </Label>
</div> </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 { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -16,6 +10,10 @@ import {
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type EnvelopesBulkDeleteDialogProps = {
envelopeIds: string[]; envelopeIds: string[];
@@ -88,9 +86,7 @@ export const EnvelopesBulkDeleteDialog = ({
<Dialog {...props} open={open} onOpenChange={onOpenChange}> <Dialog {...props} open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>{isDocument ? <Trans>Delete Documents</Trans> : <Trans>Delete Templates</Trans>}</DialogTitle>
{isDocument ? <Trans>Delete Documents</Trans> : <Trans>Delete Templates</Trans>}
</DialogTitle>
<DialogDescription> <DialogDescription>
{isDocument ? ( {isDocument ? (
@@ -149,12 +145,7 @@ export const EnvelopesBulkDeleteDialog = ({
</Alert> </Alert>
<DialogFooter> <DialogFooter>
<Button <Button type="button" variant="secondary" onClick={() => onOpenChange(false)} disabled={isPending}>
type="button"
variant="secondary"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -21,16 +9,18 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 = { export type EnvelopesBulkMoveDialogProps = {
envelopeIds: string[]; envelopeIds: string[];
@@ -119,10 +109,7 @@ export const EnvelopesBulkMoveDialog = ({
const error = AppError.parseError(err); const error = AppError.parseError(err);
const errorMessage = match(error.code) const errorMessage = match(error.code)
.with( .with(AppErrorCode.NOT_FOUND, () => t`The folder you are trying to move the items to does not exist.`)
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.UNAUTHORIZED, () => t`You are not allowed to move these items.`)
.with(AppErrorCode.INVALID_BODY, () => t`All items must be of the same type.`) .with(AppErrorCode.INVALID_BODY, () => t`All items must be of the same type.`)
.otherwise(() => t`An error occurred while moving the items.`); .otherwise(() => t`An error occurred while moving the items.`);
@@ -143,11 +130,7 @@ export const EnvelopesBulkMoveDialog = ({
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{isDocument ? ( {isDocument ? <Trans>Move Documents to Folder</Trans> : <Trans>Move Templates to Folder</Trans>}
<Trans>Move Documents to Folder</Trans>
) : (
<Trans>Move Templates to Folder</Trans>
)}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
@@ -168,7 +151,7 @@ export const EnvelopesBulkMoveDialog = ({
</DialogHeader> </DialogHeader>
<div className="relative"> <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 <Input
placeholder={t`Search folders...`} placeholder={t`Search folders...`}
value={searchTerm} value={searchTerm}
@@ -222,7 +205,7 @@ export const EnvelopesBulkMoveDialog = ({
))} ))}
{searchTerm && filteredFolders?.length === 0 && ( {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> <Trans>No folders found</Trans>
</div> </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 { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@@ -20,16 +9,18 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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({ const ZCreateFolderFormSchema = z.object({
name: z.string().min(1, { message: 'Folder name is required' }), name: z.string().min(1, { message: 'Folder name is required' }),
@@ -43,12 +34,7 @@ export type FolderCreateDialogProps = {
parentFolderId?: string | null; parentFolderId?: string | null;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
export const FolderCreateDialog = ({ export const FolderCreateDialog = ({ type, trigger, parentFolderId, ...props }: FolderCreateDialogProps) => {
type,
trigger,
parentFolderId,
...props
}: FolderCreateDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { folderId } = useParams(); const { folderId } = useParams();
@@ -98,11 +84,7 @@ export const FolderCreateDialog = ({
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}> <Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{trigger ?? ( {trigger ?? (
<Button <Button variant="outline" className="flex items-center" data-testid="folder-create-button">
variant="outline"
className="flex items-center"
data-testid="folder-create-button"
>
<FolderPlusIcon className="mr-2 h-4 w-4" /> <FolderPlusIcon className="mr-2 h-4 w-4" />
<Trans>Create Folder</Trans> <Trans>Create Folder</Trans>
</Button> </Button>
@@ -139,11 +121,7 @@ export const FolderCreateDialog = ({
/> />
<DialogFooter> <DialogFooter>
<Button <Button type="button" variant="secondary" onClick={() => setIsCreateFolderOpen(false)}>
type="button"
variant="secondary"
onClick={() => setIsCreateFolderOpen(false)}
>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
@@ -1,12 +1,3 @@
import { useEffect } 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 { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
@@ -20,16 +11,15 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export type FolderDeleteDialogProps = { export type FolderDeleteDialogProps = {
folder: TFolderWithSubfolders; folder: TFolderWithSubfolders;
@@ -110,14 +100,12 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{(folder._count.documents > 0 || {(folder._count.documents > 0 || folder._count.templates > 0 || folder._count.subfolders > 0) && (
folder._count.templates > 0 ||
folder._count.subfolders > 0) && (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
<Trans> <Trans>
This folder contains multiple items. Deleting it will remove all subfolders and move This folder contains multiple items. Deleting it will remove all subfolders and move all nested
all nested documents and templates to the root folder. documents and templates to the root folder.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -134,9 +122,7 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
<FormLabel> <FormLabel>
<Trans> <Trans>
Confirm by typing:{' '} Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold"> <span className="font-semibold font-sm text-destructive">{deleteMessage}</span>
{deleteMessage}
</span>
</Trans> </Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -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 { FolderIcon, HomeIcon, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
@@ -20,15 +10,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { FolderIcon, HomeIcon, Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export type FolderMoveDialogProps = { export type FolderMoveDialogProps = {
foldersData: TFolderWithSubfolders[] | undefined; foldersData: TFolderWithSubfolders[] | undefined;
@@ -43,12 +34,7 @@ const ZMoveFolderFormSchema = z.object({
type TMoveFolderFormSchema = z.infer<typeof ZMoveFolderFormSchema>; type TMoveFolderFormSchema = z.infer<typeof ZMoveFolderFormSchema>;
export const FolderMoveDialog = ({ export const FolderMoveDialog = ({ foldersData, folder, isOpen, onOpenChange }: FolderMoveDialogProps) => {
foldersData,
folder,
isOpen,
onOpenChange,
}: FolderMoveDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -129,7 +115,7 @@ export const FolderMoveDialog = ({
</DialogHeader> </DialogHeader>
<div className="relative"> <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 <Input
placeholder={t`Search folders...`} placeholder={t`Search folders...`}
value={searchTerm} value={searchTerm}
@@ -1,12 +1,3 @@
import { useEffect } 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 { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -21,23 +12,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
@@ -1,17 +1,3 @@
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { ExternalLinkIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import type { z } from 'zod';
import type { InternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans'; import type { InternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
@@ -34,18 +20,22 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { ExternalLinkIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import type { z } from 'zod';
import { IndividualPersonalLayoutCheckoutButton } from '../general/billing-plans'; import { IndividualPersonalLayoutCheckoutButton } from '../general/billing-plans';
@@ -70,9 +60,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
const actionSearchParam = searchParams?.get('action'); const actionSearchParam = searchParams?.get('action');
const [step, setStep] = useState<'billing' | 'create'>( const [step, setStep] = useState<'billing' | 'create'>(IS_BILLING_ENABLED() ? 'billing' : 'create');
IS_BILLING_ENABLED() ? 'billing' : 'create',
);
const [selectedPriceId, setSelectedPriceId] = useState<string>(''); const [selectedPriceId, setSelectedPriceId] = useState<string>('');
@@ -145,11 +133,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
}; };
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button className="flex-shrink-0" variant="secondary"> <Button className="flex-shrink-0" variant="secondary">
@@ -215,10 +199,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -237,11 +218,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
<DialogFooter> <DialogFooter>
{IS_BILLING_ENABLED() ? ( {IS_BILLING_ENABLED() ? (
<Button <Button type="button" variant="secondary" onClick={() => setStep('billing')}>
type="button"
variant="secondary"
onClick={() => setStep('billing')}
>
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
) : ( ) : (
@@ -290,30 +267,23 @@ type BillingPlanFormProps = {
canCreateFreeOrganisation: boolean; canCreateFreeOrganisation: boolean;
}; };
const BillingPlanForm = ({ const BillingPlanForm = ({ value, onChange, plans, canCreateFreeOrganisation }: BillingPlanFormProps) => {
value,
onChange,
plans,
canCreateFreeOrganisation,
}: BillingPlanFormProps) => {
const { t } = useLingui(); const { t } = useLingui();
const [billingPeriod, setBillingPeriod] = useState<'monthlyPrice' | 'yearlyPrice'>('yearlyPrice'); const [billingPeriod, setBillingPeriod] = useState<'monthlyPrice' | 'yearlyPrice'>('yearlyPrice');
const dynamicPlans = useMemo(() => { const dynamicPlans = useMemo(() => {
return [INTERNAL_CLAIM_ID.INDIVIDUAL, INTERNAL_CLAIM_ID.TEAM, INTERNAL_CLAIM_ID.PLATFORM].map( return [INTERNAL_CLAIM_ID.INDIVIDUAL, INTERNAL_CLAIM_ID.TEAM, INTERNAL_CLAIM_ID.PLATFORM].map((planId) => {
(planId) => { const plan = plans[planId];
const plan = plans[planId];
return { return {
id: planId, id: planId,
name: plan.name, name: plan.name,
description: parseMessageDescriptorMacro(t, internalClaimsDescription[planId]), description: parseMessageDescriptorMacro(t, internalClaimsDescription[planId]),
monthlyPrice: plan.monthlyPrice, monthlyPrice: plan.monthlyPrice,
yearlyPrice: plan.yearlyPrice, yearlyPrice: plan.yearlyPrice,
}; };
}, });
);
}, [plans]); }, [plans]);
useEffect(() => { useEffect(() => {
@@ -357,9 +327,9 @@ const BillingPlanForm = ({
<button <button
onClick={() => onChange('')} onClick={() => onChange('')}
className={cn( className={cn(
'hover:border-primary flex cursor-pointer items-center space-x-2 rounded-md border p-4 transition-all hover:shadow-sm', 'flex cursor-pointer items-center space-x-2 rounded-md border p-4 transition-all hover:border-primary hover:shadow-sm',
{ {
'ring-primary/10 border-primary ring-2 ring-offset-1': '' === value, 'border-primary ring-2 ring-primary/10 ring-offset-1': '' === value,
}, },
)} )}
disabled={!canCreateFreeOrganisation} disabled={!canCreateFreeOrganisation}
@@ -390,10 +360,9 @@ const BillingPlanForm = ({
key={plan[billingPeriod]?.id} key={plan[billingPeriod]?.id}
onClick={() => onChange(plan[billingPeriod]?.id ?? '')} onClick={() => onChange(plan[billingPeriod]?.id ?? '')}
className={cn( className={cn(
'hover:border-primary flex cursor-pointer items-center space-x-2 rounded-md border p-4 transition-all hover:shadow-sm', 'flex cursor-pointer items-center space-x-2 rounded-md border p-4 transition-all hover:border-primary hover:shadow-sm',
{ {
'ring-primary/10 border-primary ring-2 ring-offset-1': 'border-primary ring-2 ring-primary/10 ring-offset-1': plan[billingPeriod]?.id === value,
plan[billingPeriod]?.id === value,
}, },
)} )}
> >
@@ -401,14 +370,10 @@ const BillingPlanForm = ({
<p className="font-medium">{plan.name}</p> <p className="font-medium">{plan.name}</p>
<p className="text-muted-foreground">{plan.description}</p> <p className="text-muted-foreground">{plan.description}</p>
</div> </div>
<div className="whitespace-nowrap text-right text-sm font-medium"> <div className="whitespace-nowrap text-right font-medium text-sm">
<p>{plan[billingPeriod]?.friendlyPrice}</p> <p>{plan[billingPeriod]?.friendlyPrice}</p>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{billingPeriod === 'monthlyPrice' ? ( {billingPeriod === 'monthlyPrice' ? <Trans>per month</Trans> : <Trans>per year</Trans>}
<Trans>per month</Trans>
) : (
<Trans>per year</Trans>
)}
</span> </span>
</div> </div>
</button> </button>
@@ -417,13 +382,13 @@ const BillingPlanForm = ({
<Link <Link
to="https://documen.so/enterprise-cta" to="https://documen.so/enterprise-cta"
target="_blank" target="_blank"
className="bg-muted/30 flex items-center space-x-2 rounded-md border p-4" className="flex items-center space-x-2 rounded-md border bg-muted/30 p-4"
> >
<div className="flex-1 font-normal"> <div className="flex-1 font-normal">
<p className="text-muted-foreground font-medium"> <p className="font-medium text-muted-foreground">
<Trans>Enterprise</Trans> <Trans>Enterprise</Trans>
</p> </p>
<p className="text-muted-foreground flex flex-row items-center gap-1"> <p className="flex flex-row items-center gap-1 text-muted-foreground">
<Trans>Contact sales here</Trans> <Trans>Contact sales here</Trans>
<ExternalLinkIcon className="h-4 w-4" /> <ExternalLinkIcon className="h-4 w-4" />
</p> </p>
@@ -434,7 +399,7 @@ const BillingPlanForm = ({
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<Link <Link
to="https://documenso.com/pricing" to="https://documenso.com/pricing"
className="text-primary hover:text-primary/80 flex items-center justify-center gap-1 text-sm hover:underline" className="flex items-center justify-center gap-1 text-primary text-sm hover:text-primary/80 hover:underline"
target="_blank" target="_blank"
> >
<Trans>Compare all plans and features in detail</Trans> <Trans>Compare all plans and features in detail</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';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
@@ -22,16 +12,17 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
export type OrganisationDeleteDialogProps = { export type OrganisationDeleteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -117,19 +108,16 @@ export const OrganisationDeleteDialog = ({ trigger }: OrganisationDeleteDialogPr
<DialogDescription> <DialogDescription>
<Trans> <Trans>
You are about to delete <span className="font-semibold">{organisation.name}</span>. You are about to delete <span className="font-semibold">{organisation.name}</span>. All data related to
All data related to this organisation such as teams, documents, and all other this organisation such as teams, documents, and all other resources will be deleted. This action is
resources will be deleted. This action is irreversible. irreversible.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="organisationName" name="organisationName"
@@ -1,11 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCreateOrganisationEmailRequestSchema } from '@documenso/trpc/server/enterprise-router/create-organisation-email.types'; import { ZCreateOrganisationEmailRequestSchema } from '@documenso/trpc/server/enterprise-router/create-organisation-email.types';
@@ -30,6 +22,12 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 type { z } from 'zod';
type EmailDomain = { type EmailDomain = {
id: string; id: string;
@@ -69,8 +67,7 @@ export const OrganisationEmailCreateDialog = ({
}, },
}); });
const { mutateAsync: createOrganisationEmail, isPending } = const { mutateAsync: createOrganisationEmail, isPending } = trpc.enterprise.organisation.email.create.useMutation();
trpc.enterprise.organisation.email.create.useMutation();
// Reset state when dialog closes // Reset state when dialog closes
useEffect(() => { useEffect(() => {
@@ -176,14 +173,14 @@ export const OrganisationEmailCreateDialog = ({
}} }}
placeholder={t`support`} placeholder={t`support`}
/> />
<div className="bg-muted text-muted-foreground absolute bottom-0 right-0 top-0 flex items-center rounded-r-md border px-3 py-2 text-sm"> <div className="absolute top-0 right-0 bottom-0 flex items-center rounded-r-md border bg-muted px-3 py-2 text-muted-foreground text-sm">
@{emailDomain.domain} @{emailDomain.domain}
</div> </div>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
{!form.formState.errors.email && ( {!form.formState.errors.email && (
<span className="text-foreground/50 text-xs font-normal"> <span className="font-normal text-foreground/50 text-xs">
{field.value ? ( {field.value ? (
field.value field.value
) : ( ) : (
@@ -225,11 +222,7 @@ export const OrganisationEmailCreateDialog = ({
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button <Button type="submit" data-testid="dialog-create-organisation-email-button" loading={isPending}>
type="submit"
data-testid="dialog-create-organisation-email-button"
loading={isPending}
>
<Trans>Create Email</Trans> <Trans>Create Email</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1,8 +1,3 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -17,6 +12,8 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
export type OrganisationEmailDeleteDialogProps = { export type OrganisationEmailDeleteDialogProps = {
emailId: string; emailId: string;
@@ -24,11 +21,7 @@ export type OrganisationEmailDeleteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const OrganisationEmailDeleteDialog = ({ export const OrganisationEmailDeleteDialog = ({ trigger, emailId, email }: OrganisationEmailDeleteDialogProps) => {
trigger,
emailId,
email,
}: OrganisationEmailDeleteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { t } = useLingui(); const { t } = useLingui();
@@ -36,26 +29,25 @@ export const OrganisationEmailDeleteDialog = ({
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
const { mutateAsync: deleteEmail, isPending: isDeleting } = const { mutateAsync: deleteEmail, isPending: isDeleting } = trpc.enterprise.organisation.email.delete.useMutation({
trpc.enterprise.organisation.email.delete.useMutation({ onSuccess: () => {
onSuccess: () => { toast({
toast({ title: t`Success`,
title: t`Success`, description: t`You have successfully removed this email from the organisation.`,
description: t`You have successfully removed this email from the organisation.`, duration: 5000,
duration: 5000, });
});
setOpen(false); setOpen(false);
}, },
onError: () => { onError: () => {
toast({ toast({
title: t`An unknown error occurred`, title: t`An unknown error occurred`,
description: t`We encountered an unknown error while attempting to remove this email. Please try again later.`, description: t`We encountered an unknown error while attempting to remove this email. Please try again later.`,
variant: 'destructive', variant: 'destructive',
duration: 10000, duration: 10000,
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
@@ -1,12 +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 type { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -32,6 +23,12 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 type { z } from 'zod';
import { OrganisationEmailDomainRecordContent } from './organisation-email-domain-records-dialog'; import { OrganisationEmailDomainRecordContent } from './organisation-email-domain-records-dialog';
@@ -43,9 +40,7 @@ const ZCreateOrganisationEmailDomainFormSchema = ZCreateOrganisationEmailDomainR
domain: true, domain: true,
}); });
type TCreateOrganisationEmailDomainFormSchema = z.infer< type TCreateOrganisationEmailDomainFormSchema = z.infer<typeof ZCreateOrganisationEmailDomainFormSchema>;
typeof ZCreateOrganisationEmailDomainFormSchema
>;
type DomainRecord = { type DomainRecord = {
name: string; name: string;
@@ -53,10 +48,7 @@ type DomainRecord = {
type: string; type: string;
}; };
export const OrganisationEmailDomainCreateDialog = ({ export const OrganisationEmailDomainCreateDialog = ({ trigger, ...props }: OrganisationEmailCreateDialogProps) => {
trigger,
...props
}: OrganisationEmailCreateDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
@@ -72,8 +64,7 @@ export const OrganisationEmailDomainCreateDialog = ({
}, },
}); });
const { mutateAsync: createOrganisationEmail } = const { mutateAsync: createOrganisationEmail } = trpc.enterprise.organisation.emailDomain.create.useMutation();
trpc.enterprise.organisation.emailDomain.create.useMutation();
// Reset state when dialog closes // Reset state when dialog closes
useEffect(() => { useEffect(() => {
@@ -119,11 +110,7 @@ export const OrganisationEmailDomainCreateDialog = ({
}; };
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button className="flex-shrink-0" variant="secondary"> <Button className="flex-shrink-0" variant="secondary">
@@ -140,18 +127,15 @@ export const OrganisationEmailDomainCreateDialog = ({
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
<Trans> <Trans>
Add a custom domain to send emails on behalf of your organisation. We'll generate Add a custom domain to send emails on behalf of your organisation. We'll generate DKIM records that you
DKIM records that you need to add to your DNS provider. need to add to your DNS provider.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="domain" name="domain"
@@ -165,10 +149,7 @@ export const OrganisationEmailDomainCreateDialog = ({
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
<Trans> <Trans>Enter the domain you want to use for sending emails (without http:// or www)</Trans>
Enter the domain you want to use for sending emails (without http:// or
www)
</Trans>
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
@@ -1,11 +1,3 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -18,16 +10,14 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
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';
export type OrganisationEmailDomainDeleteDialogProps = { export type OrganisationEmailDomainDeleteDialogProps = {
emailDomainId: string; emailDomainId: string;
@@ -107,10 +97,9 @@ export const OrganisationEmailDomainDeleteDialog = ({
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>
You are about to remove the email domain{' '} You are about to remove the email domain <span className="font-semibold">{emailDomain}</span> from{' '}
<span className="font-semibold">{emailDomain}</span> from{' '} <span className="font-semibold">{organisation.name}</span>. All emails associated with this domain will be
<span className="font-semibold">{organisation.name}</span>. All emails associated with deleted.
this domain will be deleted.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -126,9 +115,7 @@ export const OrganisationEmailDomainDeleteDialog = ({
<FormLabel> <FormLabel>
<Trans> <Trans>
Confirm by typing{' '} Confirm by typing{' '}
<span className="font-sm text-destructive font-semibold"> <span className="font-semibold font-sm text-destructive">{deleteMessage}</span>
{deleteMessage}
</span>
</Trans> </Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -1,7 +1,3 @@
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button'; import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -18,6 +14,8 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
export type OrganisationEmailDomainRecordsDialogProps = { export type OrganisationEmailDomainRecordsDialogProps = {
trigger: React.ReactNode; trigger: React.ReactNode;
@@ -72,7 +70,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
<div className="relative"> <div className="relative">
<Input className="pr-12" disabled value={record.type} /> <Input className="pr-12" disabled value={record.type} />
<div className="absolute bottom-0 right-2 top-0 flex items-center justify-center"> <div className="absolute top-0 right-2 bottom-0 flex items-center justify-center">
<CopyTextButton <CopyTextButton
value={record.type} value={record.type}
onCopySuccess={() => toast({ title: t`Copied to clipboard` })} onCopySuccess={() => toast({ title: t`Copied to clipboard` })}
@@ -88,7 +86,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
<div className="relative"> <div className="relative">
<Input className="pr-12" disabled value={record.name} /> <Input className="pr-12" disabled value={record.name} />
<div className="absolute bottom-0 right-2 top-0 flex items-center justify-center"> <div className="absolute top-0 right-2 bottom-0 flex items-center justify-center">
<CopyTextButton <CopyTextButton
value={record.name} value={record.name}
onCopySuccess={() => toast({ title: t`Copied to clipboard` })} onCopySuccess={() => toast({ title: t`Copied to clipboard` })}
@@ -104,7 +102,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
<div className="relative"> <div className="relative">
<Input className="pr-12" disabled value={record.value} /> <Input className="pr-12" disabled value={record.value} />
<div className="absolute bottom-0 right-2 top-0 flex items-center justify-center"> <div className="absolute top-0 right-2 bottom-0 flex items-center justify-center">
<CopyTextButton <CopyTextButton
value={record.value} value={record.value}
onCopySuccess={() => toast({ title: t`Copied to clipboard` })} onCopySuccess={() => toast({ title: t`Copied to clipboard` })}
@@ -119,9 +117,8 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription> <AlertDescription>
<Trans> <Trans>
Once you update your DNS records, it may take up to 48 hours for it to be propogated. Once you update your DNS records, it may take up to 48 hours for it to be propogated. Once the DNS
Once the DNS propagation is complete you will need to come back and press the "Sync" propagation is complete you will need to come back and press the "Sync" domains button.
domains button.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -1,11 +1,3 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TGetOrganisationEmailDomainResponse } from '@documenso/trpc/server/enterprise-router/get-organisation-email-domain.types'; import type { TGetOrganisationEmailDomainResponse } from '@documenso/trpc/server/enterprise-router/get-organisation-email-domain.types';
import { ZUpdateOrganisationEmailRequestSchema } from '@documenso/trpc/server/enterprise-router/update-organisation-email.types'; import { ZUpdateOrganisationEmailRequestSchema } from '@documenso/trpc/server/enterprise-router/update-organisation-email.types';
@@ -30,6 +22,12 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 type { z } from 'zod';
export type OrganisationEmailUpdateDialogProps = { export type OrganisationEmailUpdateDialogProps = {
trigger: React.ReactNode; trigger: React.ReactNode;
@@ -61,8 +59,7 @@ export const OrganisationEmailUpdateDialog = ({
}, },
}); });
const { mutateAsync: updateOrganisationEmail, isPending } = const { mutateAsync: updateOrganisationEmail, isPending } = trpc.enterprise.organisation.email.update.useMutation();
trpc.enterprise.organisation.email.update.useMutation();
const onFormSubmit = async ({ emailName }: ZUpdateOrganisationEmailSchema) => { const onFormSubmit = async ({ emailName }: ZUpdateOrganisationEmailSchema) => {
try { try {
@@ -98,11 +95,7 @@ export const OrganisationEmailUpdateDialog = ({
}, [open, form]); }, [open, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger} {trigger}
</DialogTrigger> </DialogTrigger>
@@ -115,8 +108,7 @@ export const OrganisationEmailUpdateDialog = ({
<DialogDescription> <DialogDescription>
<Trans> <Trans>
You are currently updating{' '} You are currently updating <span className="font-bold">{organisationEmail.email}</span>
<span className="font-bold">{organisationEmail.email}</span>
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -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 { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations'; import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations';
import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
@@ -34,15 +24,20 @@ import {
FormMessage, FormMessage,
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox'; 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 { 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 type { z } from 'zod';
import {
type OrganisationMemberOption,
OrganisationMembersMultiSelectCombobox,
} from '~/components/general/organisation-members-multiselect-combobox';
export type OrganisationGroupCreateDialogProps = { export type OrganisationGroupCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -56,14 +51,12 @@ const ZCreateOrganisationGroupFormSchema = ZCreateOrganisationGroupRequestSchema
type TCreateOrganisationGroupFormSchema = z.infer<typeof ZCreateOrganisationGroupFormSchema>; type TCreateOrganisationGroupFormSchema = z.infer<typeof ZCreateOrganisationGroupFormSchema>;
export const OrganisationGroupCreateDialog = ({ export const OrganisationGroupCreateDialog = ({ trigger, ...props }: OrganisationGroupCreateDialogProps) => {
trigger,
...props
}: OrganisationGroupCreateDialogProps) => {
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [selectedMembers, setSelectedMembers] = useState<OrganisationMemberOption[]>([]);
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
const form = useForm({ const form = useForm({
@@ -77,18 +70,7 @@ export const OrganisationGroupCreateDialog = ({
const { mutateAsync: createOrganisationGroup } = trpc.organisation.group.create.useMutation(); const { mutateAsync: createOrganisationGroup } = trpc.organisation.group.create.useMutation();
const { data: membersFindResult, isLoading: isLoadingMembers } = const onFormSubmit = async ({ name, organisationRole, memberIds }: TCreateOrganisationGroupFormSchema) => {
trpc.organisation.member.find.useQuery({
organisationId: organisation.id,
});
const members = membersFindResult?.data ?? [];
const onFormSubmit = async ({
name,
organisationRole,
memberIds,
}: TCreateOrganisationGroupFormSchema) => {
try { try {
await createOrganisationGroup({ await createOrganisationGroup({
organisationId: organisation.id, organisationId: organisation.id,
@@ -119,14 +101,11 @@ export const OrganisationGroupCreateDialog = ({
useEffect(() => { useEffect(() => {
form.reset(); form.reset();
setSelectedMembers([]);
}, [open, form]); }, [open, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button className="flex-shrink-0" variant="secondary"> <Button className="flex-shrink-0" variant="secondary">
@@ -148,10 +127,7 @@ export const OrganisationGroupCreateDialog = ({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -178,14 +154,12 @@ export const OrganisationGroupCreateDialog = ({
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Select {...field} onValueChange={field.onChange}> <Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="text-muted-foreground w-full"> <SelectTrigger className="w-full text-muted-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent position="popper"> <SelectContent position="popper">
{ORGANISATION_MEMBER_ROLE_HIERARCHY[ {ORGANISATION_MEMBER_ROLE_HIERARCHY[organisation.currentOrganisationRole].map((role) => (
organisation.currentOrganisationRole
].map((role) => (
<SelectItem key={role} value={role}> <SelectItem key={role} value={role}>
{t(EXTENDED_ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role} {t(EXTENDED_ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role}
</SelectItem> </SelectItem>
@@ -209,16 +183,15 @@ export const OrganisationGroupCreateDialog = ({
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<MultiSelectCombobox <OrganisationMembersMultiSelectCombobox
options={members.map((member) => ({ organisationId={organisation.id}
label: member.name, selectedMembers={selectedMembers}
value: member.id, onChange={(members) => {
}))} setSelectedMembers(members);
loading={isLoadingMembers} field.onChange(members.map((member) => member.id));
selectedValues={field.value} }}
onChange={field.onChange} className="w-full bg-background"
className="bg-background w-full" dataTestId="group-members-picker"
emptySelectionPlaceholder={t`Select members`}
/> />
</FormControl> </FormControl>
@@ -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 { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -18,6 +12,10 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 OrganisationGroupDeleteDialogProps = { export type OrganisationGroupDeleteDialogProps = {
organisationGroupId: string; organisationGroupId: string;
@@ -37,28 +35,27 @@ export const OrganisationGroupDeleteDialog = ({
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
const { mutateAsync: deleteGroup, isPending: isDeleting } = const { mutateAsync: deleteGroup, isPending: isDeleting } = trpc.organisation.group.delete.useMutation({
trpc.organisation.group.delete.useMutation({ onSuccess: () => {
onSuccess: () => { toast({
toast({ title: _(msg`Success`),
title: _(msg`Success`), description: _(msg`You have successfully removed this group from the organisation.`),
description: _(msg`You have successfully removed this group from the organisation.`), duration: 5000,
duration: 5000, });
});
setOpen(false); setOpen(false);
}, },
onError: () => { onError: () => {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
description: _( description: _(
msg`We encountered an unknown error while attempting to remove this group. Please try again later.`, msg`We encountered an unknown error while attempting to remove this group. Please try again later.`,
), ),
variant: 'destructive', variant: 'destructive',
duration: 10000, duration: 10000,
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
@@ -85,9 +82,7 @@ export const OrganisationGroupDeleteDialog = ({
</DialogHeader> </DialogHeader>
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription className="text-center font-semibold"> <AlertDescription className="text-center font-semibold">{organisationGroupName}</AlertDescription>
{organisationGroupName}
</AlertDescription>
</Alert> </Alert>
<fieldset disabled={isDeleting}> <fieldset disabled={isDeleting}>
@@ -1,9 +1,3 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { OrganisationMemberRole } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -20,6 +14,9 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import type { OrganisationMemberRole } from '@prisma/client';
import { useState } from 'react';
export type OrganisationLeaveDialogProps = { export type OrganisationLeaveDialogProps = {
organisationId: string; organisationId: string;
@@ -41,26 +38,25 @@ export const OrganisationLeaveDialog = ({
const { t } = useLingui(); const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: leaveOrganisation, isPending: isLeavingOrganisation } = const { mutateAsync: leaveOrganisation, isPending: isLeavingOrganisation } = trpc.organisation.leave.useMutation({
trpc.organisation.leave.useMutation({ onSuccess: () => {
onSuccess: () => { toast({
toast({ title: t`Success`,
title: t`Success`, description: t`You have successfully left this organisation.`,
description: t`You have successfully left this organisation.`, duration: 5000,
duration: 5000, });
});
setOpen(false); setOpen(false);
}, },
onError: () => { onError: () => {
toast({ toast({
title: t`An unknown error occurred`, title: t`An unknown error occurred`,
description: t`We encountered an unknown error while attempting to leave this organisation. Please try again later.`, description: t`We encountered an unknown error while attempting to leave this organisation. Please try again later.`,
variant: 'destructive', variant: 'destructive',
duration: 10000, duration: 10000,
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLeavingOrganisation && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isLeavingOrganisation && setOpen(value)}>
@@ -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 { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert } from '@documenso/ui/primitives/alert'; import { Alert } from '@documenso/ui/primitives/alert';
@@ -19,6 +13,10 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 OrganisationMemberDeleteDialogProps = { export type OrganisationMemberDeleteDialogProps = {
organisationMemberId: string; organisationMemberId: string;
@@ -81,8 +79,8 @@ export const OrganisationMemberDeleteDialog = ({
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>
You are about to remove the following user from{' '} You are about to remove the following user from <span className="font-semibold">{organisation.name}</span>
<span className="font-semibold">{organisation.name}</span>. .
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -1,16 +1,3 @@
import { useEffect, useMemo, useRef, 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 { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
import Papa, { type ParseResult } from 'papaparse';
import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { IS_BILLING_ENABLED, SUPPORT_EMAIL } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED, SUPPORT_EMAIL } from '@documenso/lib/constants/app';
@@ -33,25 +20,23 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
import Papa, { type ParseResult } from 'papaparse';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod';
export type OrganisationMemberInviteDialogProps = { export type OrganisationMemberInviteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -100,10 +85,7 @@ const ZImportOrganisationMemberSchema = z.array(
}), }),
); );
export const OrganisationMemberInviteDialog = ({ export const OrganisationMemberInviteDialog = ({ trigger, ...props }: OrganisationMemberInviteDialogProps) => {
trigger,
...props
}: OrganisationMemberInviteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL'); const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
@@ -134,8 +116,7 @@ export const OrganisationMemberInviteDialog = ({
name: 'invitations', name: 'invitations',
}); });
const { mutateAsync: createOrganisationMemberInvites } = const { mutateAsync: createOrganisationMemberInvites } = trpc.organisation.member.invite.createMany.useMutation();
trpc.organisation.member.invite.createMany.useMutation();
const { data: fullOrganisation } = trpc.organisation.get.useQuery({ const { data: fullOrganisation } = trpc.organisation.get.useQuery({
organisationReference: organisation.id, organisationReference: organisation.id,
@@ -242,9 +223,7 @@ export const OrganisationMemberInviteDialog = ({
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
description: _( description: _(msg`Please check the CSV file and make sure it is according to our format`),
msg`Please check the CSV file and make sure it is according to our format`,
),
variant: 'destructive', variant: 'destructive',
}); });
} }
@@ -259,8 +238,7 @@ export const OrganisationMemberInviteDialog = ({
{ email: 'member@documenso.com', role: 'Member' }, { email: 'member@documenso.com', role: 'Member' },
]; ];
const csvContent = const csvContent = 'Email address,Role\n' + data.map((row) => `${row.email},${row.role}`).join('\n');
'Email address,Role\n' + data.map((row) => `${row.email},${row.role}`).join('\n');
const blob = new Blob([csvContent], { const blob = new Blob([csvContent], {
type: 'text/csv', type: 'text/csv',
@@ -273,11 +251,7 @@ export const OrganisationMemberInviteDialog = ({
}; };
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? ( {trigger ?? (
<Button variant="secondary"> <Button variant="secondary">
@@ -301,15 +275,11 @@ export const OrganisationMemberInviteDialog = ({
{dialogState === 'alert' && ( {dialogState === 'alert' && (
<> <>
<Alert <Alert className="flex flex-col justify-between p-6 sm:flex-row sm:items-center" variant="neutral">
className="flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<AlertDescription> <AlertDescription>
<Trans> <Trans>
Your plan does not support inviting members. Please upgrade or your plan or Your plan does not support inviting members. Please upgrade or your plan or contact sales at{' '}
contact sales at <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you would like to discuss your options.
would like to discuss your options.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -343,16 +313,10 @@ export const OrganisationMemberInviteDialog = ({
<TabsContent value="INDIVIDUAL"> <TabsContent value="INDIVIDUAL">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1"> <div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1">
{organisationMemberInvites.map((organisationMemberInvite, index) => ( {organisationMemberInvites.map((organisationMemberInvite, index) => (
<div <div className="flex w-full flex-row space-x-4" key={organisationMemberInvite.id}>
className="flex w-full flex-row space-x-4"
key={organisationMemberInvite.id}
>
<FormField <FormField
control={form.control} control={form.control}
name={`invitations.${index}.email`} name={`invitations.${index}.email`}
@@ -388,13 +352,13 @@ export const OrganisationMemberInviteDialog = ({
</SelectTrigger> </SelectTrigger>
<SelectContent position="popper"> <SelectContent position="popper">
{ORGANISATION_MEMBER_ROLE_HIERARCHY[ {ORGANISATION_MEMBER_ROLE_HIERARCHY[organisation.currentOrganisationRole].map(
organisation.currentOrganisationRole (role) => (
].map((role) => ( <SelectItem key={role} value={role}>
<SelectItem key={role} value={role}> {_(ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role}
{_(ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role} </SelectItem>
</SelectItem> ),
))} )}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@@ -457,13 +421,7 @@ export const OrganisationMemberInviteDialog = ({
<Trans>Click here to upload</Trans> <Trans>Click here to upload</Trans>
</p> </p>
<input <input onChange={onFileInputChange} type="file" ref={fileInputRef} accept=".csv" hidden />
onChange={onFileInputChange}
type="file"
ref={fileInputRef}
accept=".csv"
hidden
/>
</CardContent> </CardContent>
</Card> </Card>
@@ -1,14 +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 { OrganisationMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations'; import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations'; import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations';
@@ -23,22 +12,18 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
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 { 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 { 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 { z } from 'zod';
export type OrganisationMemberUpdateDialogProps = { export type OrganisationMemberUpdateDialogProps = {
currentUserOrganisationRole: OrganisationMemberRole; currentUserOrganisationRole: OrganisationMemberRole;
@@ -113,9 +98,7 @@ export const OrganisationMemberUpdateDialog = ({
form.reset(); form.reset();
if ( if (!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, organisationMemberRole)) {
!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, organisationMemberRole)
) {
setOpen(false); setOpen(false);
toast({ toast({
@@ -127,11 +110,7 @@ export const OrganisationMemberUpdateDialog = ({
}, [open, currentUserOrganisationRole, organisationMemberRole, form, toast]); }, [open, currentUserOrganisationRole, organisationMemberRole, form, toast]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? ( {trigger ?? (
<Button variant="secondary"> <Button variant="secondary">
@@ -148,8 +127,7 @@ export const OrganisationMemberUpdateDialog = ({
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>
You are currently updating <span className="font-bold">{organisationMemberName}</span> You are currently updating <span className="font-bold">{organisationMemberName}</span>.
.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -172,13 +150,11 @@ export const OrganisationMemberUpdateDialog = ({
</SelectTrigger> </SelectTrigger>
<SelectContent className="w-full" position="popper"> <SelectContent className="w-full" position="popper">
{ORGANISATION_MEMBER_ROLE_HIERARCHY[currentUserOrganisationRole].map( {ORGANISATION_MEMBER_ROLE_HIERARCHY[currentUserOrganisationRole].map((role) => (
(role) => ( <SelectItem key={role} value={role}>
<SelectItem key={role} value={role}> {_(ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role}
{_(ORGANISATION_MEMBER_ROLE_MAP[role]) ?? role} </SelectItem>
</SelectItem> ))}
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@@ -1,15 +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 { startRegistration } from '@simplewebauthn/browser';
import { KeyRoundIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { z } from 'zod';
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -24,16 +12,19 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { startRegistration } from '@simplewebauthn/browser';
import { KeyRoundIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { z } from 'zod';
export type PasskeyCreateDialogProps = { export type PasskeyCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -133,15 +124,11 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
}, [open, form]); }, [open, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button variant="secondary" loading={isPending}> <Button variant="secondary" loading={isPending}>
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" /> <KeyRoundIcon className="mr-1 -ml-1 h-5 w-5" />
<Trans>Add passkey</Trans> <Trans>Add passkey</Trans>
</Button> </Button>
)} )}
@@ -154,19 +141,13 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
</DialogTitle> </DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans> <Trans>Passkeys allow you to sign in and authenticate using biometrics, password managers, etc.</Trans>
Passkeys allow you to sign in and authenticate using biometrics, password managers,
etc.
</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="passkeyName" name="passkeyName"
@@ -186,15 +167,15 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription> <AlertDescription>
<Trans> <Trans>
When you click continue, you will be prompted to add the first available When you click continue, you will be prompted to add the first available authenticator on your
authenticator on your system. system.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
<AlertDescription className="mt-2"> <AlertDescription className="mt-2">
<Trans> <Trans>
If you do not want to use the authenticator prompted, you can close it, which If you do not want to use the authenticator prompted, you can close it, which will then display the
will then display the next available authenticator. next available authenticator.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -219,9 +200,7 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
.with('InvalidStateError', () => ( .with('InvalidStateError', () => (
<> <>
<AlertTitle className="text-sm"> <AlertTitle className="text-sm">
<Trans> <Trans>Passkey creation cancelled due to one of the following reasons:</Trans>
Passkey creation cancelled due to one of the following reasons:
</Trans>
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
<ul className="mt-1 list-inside list-disc"> <ul className="mt-1 list-inside list-disc">
@@ -1,17 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import type { Template } from '@documenso/prisma/types/template-legacy-schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { P, match } from 'ts-pattern';
import { z } from 'zod';
import { type Template } from '@documenso/prisma/types/template-legacy-schema';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { import {
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH, MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
@@ -29,25 +16,22 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@documenso/ui/primitives/table';
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
import { Textarea } from '@documenso/ui/primitives/textarea'; import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { Plural, Trans } from '@lingui/react/macro';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { match, P } from 'ts-pattern';
import { z } from 'zod';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
@@ -145,10 +129,7 @@ export const ManagePublicTemplateDialog = ({
} }
}; };
const onFormSubmit = async ({ const onFormSubmit = async ({ publicTitle, publicDescription }: TUpdatePublicTemplateFormSchema) => {
publicTitle,
publicDescription,
}: TUpdatePublicTemplateFormSchema) => {
if (!selectedTemplateId) { if (!selectedTemplateId) {
return; return;
} }
@@ -251,9 +232,7 @@ export const ManagePublicTemplateDialog = ({
<DialogDescription> <DialogDescription>
{team ? ( {team ? (
<Trans> <Trans>Select a template you'd like to display on your team's public profile</Trans>
Select a template you'd like to display on your team's public profile
</Trans>
) : ( ) : (
<Trans>Select a template you'd like to display on your public profile</Trans> <Trans>Select a template you'd like to display on your public profile</Trans>
)} )}
@@ -290,13 +269,9 @@ export const ManagePublicTemplateDialog = ({
key={row.id} key={row.id}
onClick={() => setSelectedTemplateId(row.id)} onClick={() => setSelectedTemplateId(row.id)}
> >
<TableCell className="text-muted-foreground max-w-[30ch] text-sm"> <TableCell className="max-w-[30ch] text-muted-foreground text-sm">{row.title}</TableCell>
{row.title}
</TableCell>
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">{i18n.date(row.createdAt)}</TableCell>
{i18n.date(row.createdAt)}
</TableCell>
<TableCell> <TableCell>
{selectedTemplateId === row.id ? ( {selectedTemplateId === row.id ? (
@@ -317,11 +292,7 @@ export const ManagePublicTemplateDialog = ({
</Button> </Button>
</DialogClose> </DialogClose>
<Button <Button type="button" disabled={selectedTemplateId === null} onClick={() => onManageStep()}>
type="button"
disabled={selectedTemplateId === null}
onClick={() => onManageStep()}
>
<Trans>Continue</Trans> <Trans>Continue</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -340,10 +311,7 @@ export const ManagePublicTemplateDialog = ({
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form <form className="flex h-full flex-col space-y-4" onSubmit={form.handleSubmit(onFormSubmit)}>
className="flex h-full flex-col space-y-4"
onSubmit={form.handleSubmit(onFormSubmit)}
>
<FormField <FormField
control={form.control} control={form.control}
name="publicTitle" name="publicTitle"
@@ -351,10 +319,7 @@ export const ManagePublicTemplateDialog = ({
<FormItem> <FormItem>
<FormLabel required>Title</FormLabel> <FormLabel required>Title</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder={_(msg`The public name for your template`)} {...field} />
placeholder={_(msg`The public name for your template`)}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -365,17 +330,14 @@ export const ManagePublicTemplateDialog = ({
control={form.control} control={form.control}
name="publicDescription" name="publicDescription"
render={({ field }) => { render={({ field }) => {
const remaningLength = const remaningLength = MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH - (field.value || '').length;
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH - (field.value || '').length;
return ( return (
<FormItem> <FormItem>
<FormLabel required>Description</FormLabel> <FormLabel required>Description</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
placeholder={_( placeholder={_(msg`The public description that will be displayed with this template`)}
msg`The public description that will be displayed with this template`,
)}
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -1,7 +1,3 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { authClient } from '@documenso/auth/client'; import { authClient } from '@documenso/auth/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@@ -15,6 +11,8 @@ import {
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
type SessionLogoutAllDialogProps = { type SessionLogoutAllDialogProps = {
onSuccess?: () => Promise<unknown>; onSuccess?: () => Promise<unknown>;
@@ -71,8 +69,8 @@ export const SessionLogoutAllDialog = ({ onSuccess, disabled }: SessionLogoutAll
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
<Trans> <Trans>
This will sign you out of all other devices. You will need to sign in again on those This will sign you out of all other devices. You will need to sign in again on those devices to continue
devices to continue using your account. using your account.
</Trans> </Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -1,13 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Plural, Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm, useWatch } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { type TCheckboxFieldMeta } from '@documenso/lib/types/field-meta'; import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Checkbox } from '@documenso/ui/primitives/checkbox';
@@ -20,6 +12,13 @@ import {
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form'; import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Plural, Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm, useWatch } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
export type SignFieldCheckboxDialogProps = { export type SignFieldCheckboxDialogProps = {
fieldMeta: TCheckboxFieldMeta; fieldMeta: TCheckboxFieldMeta;
@@ -28,157 +27,133 @@ export type SignFieldCheckboxDialogProps = {
preselectedIndices: number[]; preselectedIndices: number[];
}; };
export const SignFieldCheckboxDialog = createCallable< export const SignFieldCheckboxDialog = createCallable<SignFieldCheckboxDialogProps, number[] | null>(
SignFieldCheckboxDialogProps, ({ call, fieldMeta, validationRule, validationLength, preselectedIndices }) => {
number[] | null const ZSignFieldCheckboxFormSchema = z
>(({ call, fieldMeta, validationRule, validationLength, preselectedIndices }) => { .object({
const ZSignFieldCheckboxFormSchema = z values: z.array(
.object({ z.object({
values: z.array( checked: z.boolean(),
z.object({ value: z.string(),
checked: z.boolean(), }),
value: z.string(), ),
}), })
), .superRefine((data, ctx) => {
}) // Allow unselecting all options if the field is not required even if
.superRefine((data, ctx) => { // validation is not met.
// Allow unselecting all options if the field is not required even if if (!fieldMeta.required && data.values.every((value) => !value.checked)) {
// validation is not met. return;
if (!fieldMeta.required && data.values.every((value) => !value.checked)) { }
return;
}
const numberOfSelectedValues = data.values.filter((value) => value.checked).length; const numberOfSelectedValues = data.values.filter((value) => value.checked).length;
const isValid = validateCheckboxLength( const isValid = validateCheckboxLength(numberOfSelectedValues, validationRule, validationLength);
numberOfSelectedValues,
validationRule,
validationLength,
);
if (!isValid) { if (!isValid) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: msg`Validation failed`.id, message: msg`Validation failed`.id,
}); });
} }
});
const form = useForm<z.infer<typeof ZSignFieldCheckboxFormSchema>>({
resolver: zodResolver(ZSignFieldCheckboxFormSchema),
defaultValues: {
values: (fieldMeta.values || []).map((value, index) => ({
checked: preselectedIndices.includes(index) || false,
value: value.value,
})),
},
}); });
const form = useForm<z.infer<typeof ZSignFieldCheckboxFormSchema>>({ const formValues = useWatch({
resolver: zodResolver(ZSignFieldCheckboxFormSchema), control: form.control,
defaultValues: { });
values: (fieldMeta.values || []).map((value, index) => ({
checked: preselectedIndices.includes(index) || false,
value: value.value,
})),
},
});
const formValues = useWatch({ return (
control: form.control, <Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
}); <DialogContent position="center">
<DialogHeader>
<DialogTitle>{fieldMeta.label || <Trans>Select Options</Trans>}</DialogTitle>
return ( <DialogDescription
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}> className={cn('mt-4', {
<DialogContent position="center"> 'text-destructive': Object.keys(form.formState.errors).length > 0,
<DialogHeader> })}
<DialogTitle>{fieldMeta.label || <Trans>Select Options</Trans>}</DialogTitle>
<DialogDescription
className={cn('mt-4', {
'text-destructive': Object.keys(form.formState.errors).length > 0,
})}
>
{match(validationRule)
.with('>=', () => (
<Plural
value={validationLength}
one="Select at least # option"
other="Select at least # options"
/>
))
.with('=', () => (
<Plural
value={validationLength}
one="Select exactly # option"
other="Select exactly # options"
/>
))
.with('<=', () => (
<Plural
value={validationLength}
one="Select at most # option"
other="Select at most # options"
/>
))
.exhaustive()}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) =>
call.end(
data.values
.map((value, i) => (value.checked ? i : null))
.filter((value) => value !== null),
),
)}
>
<fieldset
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
> >
<ul className="space-y-3"> {match(validationRule)
{(formValues.values || []).map((value, index) => ( .with('>=', () => (
<li key={`checkbox-${index}`}> <Plural value={validationLength} one="Select at least # option" other="Select at least # options" />
<FormField ))
control={form.control} .with('=', () => (
name={`values.${index}`} <Plural value={validationLength} one="Select exactly # option" other="Select exactly # options" />
render={({ field }) => ( ))
<FormItem> .with('<=', () => (
<FormControl> <Plural value={validationLength} one="Select at most # option" other="Select at most # options" />
<div className="flex items-center"> ))
<Checkbox .exhaustive()}
id={`checkbox-value-${index}`} </DialogDescription>
className="h-5 w-5 border-foreground/30 data-[state=checked]:bg-primary" </DialogHeader>
checked={field.value.checked}
onCheckedChange={(checked) => {
field.onChange({
...field.value,
checked,
});
}}
/>
<label <Form {...form}>
className="ml-2 w-full text-sm text-muted-foreground" <form
htmlFor={`checkbox-value-${index}`} onSubmit={form.handleSubmit((data) =>
> call.end(data.values.map((value, i) => (value.checked ? i : null)).filter((value) => value !== null)),
{value.value} )}
</label> >
</div> <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
</FormControl> <ul className="space-y-3">
</FormItem> {(formValues.values || []).map((value, index) => (
)} <li key={`checkbox-${index}`}>
/> <FormField
</li> control={form.control}
))} name={`values.${index}`}
</ul> render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center">
<Checkbox
id={`checkbox-value-${index}`}
className="h-5 w-5 border-foreground/30 data-[state=checked]:bg-primary"
checked={field.value.checked}
onCheckedChange={(checked) => {
field.onChange({
...field.value,
checked,
});
}}
/>
<DialogFooter> <label
<Button type="button" variant="secondary" onClick={() => call.end(null)}> className="ml-2 w-full text-muted-foreground text-sm"
<Trans>Cancel</Trans> htmlFor={`checkbox-value-${index}`}
</Button> >
{value.value}
</label>
</div>
</FormControl>
</FormItem>
)}
/>
</li>
))}
</ul>
<Button type="submit"> <DialogFooter>
<Trans>Confirm</Trans> <Button type="button" variant="secondary" onClick={() => call.end(null)}>
</Button> <Trans>Cancel</Trans>
</DialogFooter> </Button>
</fieldset>
</form> <Button type="submit">
</Form> <Trans>Confirm</Trans>
</DialogContent> </Button>
</Dialog> </DialogFooter>
); </fieldset>
}); </form>
</Form>
</DialogContent>
</Dialog>
);
},
);
@@ -1,6 +1,3 @@
import { useLingui } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import type { TDropdownFieldMeta } from '@documenso/lib/types/field-meta'; import type { TDropdownFieldMeta } from '@documenso/lib/types/field-meta';
import { import {
CommandDialog, CommandDialog,
@@ -10,6 +7,8 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from '@documenso/ui/primitives/command'; } from '@documenso/ui/primitives/command';
import { useLingui } from '@lingui/react/macro';
import { createCallable } from 'react-call';
export type SignFieldDropdownDialogProps = { export type SignFieldDropdownDialogProps = {
fieldMeta: TDropdownFieldMeta; fieldMeta: TDropdownFieldMeta;
@@ -1,10 +1,3 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zEmail } from '@documenso/lib/utils/zod'; import { zEmail } from '@documenso/lib/utils/zod';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@@ -15,14 +8,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const ZSignFieldEmailFormSchema = z.object({ const ZSignFieldEmailFormSchema = z.object({
email: zEmail().min(1, { message: msg`Email is required`.id }), email: zEmail().min(1, { message: msg`Email is required`.id }),
@@ -58,10 +51,7 @@ export const SignFieldEmailDialog = createCallable<SignFieldEmailDialogProps, st
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((data) => call.end(data.email))}> <form onSubmit={form.handleSubmit((data) => call.end(data.email))}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
@@ -1,10 +1,3 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@@ -14,15 +7,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const ZSignFieldInitialsFormSchema = z.object({ const ZSignFieldInitialsFormSchema = z.object({
initials: z.string().min(1, { message: msg`Initials are required`.id }), initials: z.string().min(1, { message: msg`Initials are required`.id }),
@@ -34,64 +26,59 @@ export type SignFieldInitialsDialogProps = {
// //
}; };
export const SignFieldInitialsDialog = createCallable<SignFieldInitialsDialogProps, string | null>( export const SignFieldInitialsDialog = createCallable<SignFieldInitialsDialogProps, string | null>(({ call }) => {
({ call }) => { const form = useForm<TSignFieldInitialsFormSchema>({
const form = useForm<TSignFieldInitialsFormSchema>({ resolver: zodResolver(ZSignFieldInitialsFormSchema),
resolver: zodResolver(ZSignFieldInitialsFormSchema), defaultValues: {
defaultValues: { initials: '',
initials: '', },
}, });
});
return ( return (
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}> <Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
<Trans>Enter Initials</Trans> <Trans>Enter Initials</Trans>
</DialogTitle> </DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans>Please enter your initials</Trans> <Trans>Please enter your initials</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((data) => call.end(data.initials))}> <form onSubmit={form.handleSubmit((data) => call.end(data.initials))}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4" <FormField
disabled={form.formState.isSubmitting} control={form.control}
> name="initials"
<FormField render={({ field }) => (
control={form.control} <FormItem>
name="initials" <FormLabel required>
render={({ field }) => ( <Trans>Initials</Trans>
<FormItem> </FormLabel>
<FormLabel required> <FormControl>
<Trans>Initials</Trans> <Input {...field} />
</FormLabel> </FormControl>
<FormControl> <FormMessage />
<Input {...field} /> </FormItem>
</FormControl> )}
<FormMessage /> />
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button type="button" variant="secondary" onClick={() => call.end(null)}> <Button type="button" variant="secondary" onClick={() => call.end(null)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit"> <Button type="submit">
<Trans>Enter</Trans> <Trans>Enter</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
</fieldset> </fieldset>
</form> </form>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}, });
);
@@ -1,10 +1,3 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@@ -14,14 +7,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const ZSignFieldNameFormSchema = z.object({ const ZSignFieldNameFormSchema = z.object({
name: z.string().min(1, { message: msg`Name is required`.id }), name: z.string().min(1, { message: msg`Name is required`.id }),
@@ -33,61 +26,56 @@ export type SignFieldNameDialogProps = {
// //
}; };
export const SignFieldNameDialog = createCallable<SignFieldNameDialogProps, string | null>( export const SignFieldNameDialog = createCallable<SignFieldNameDialogProps, string | null>(({ call }) => {
({ call }) => { const form = useForm<TSignFieldNameFormSchema>({
const form = useForm<TSignFieldNameFormSchema>({ resolver: zodResolver(ZSignFieldNameFormSchema),
resolver: zodResolver(ZSignFieldNameFormSchema), defaultValues: {
defaultValues: { name: '',
name: '', },
}, });
});
return ( return (
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}> <Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
<Trans>Enter Name</Trans> <Trans>Enter Name</Trans>
</DialogTitle> </DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans>Please enter your full name</Trans> <Trans>Please enter your full name</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((data) => call.end(data.name))}> <form onSubmit={form.handleSubmit((data) => call.end(data.name))}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4" <FormField
disabled={form.formState.isSubmitting} control={form.control}
> name="name"
<FormField render={({ field }) => (
control={form.control} <FormItem>
name="name" <FormControl>
render={({ field }) => ( <Input {...field} />
<FormItem> </FormControl>
<FormControl> <FormMessage />
<Input {...field} /> </FormItem>
</FormControl> )}
<FormMessage /> />
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button type="button" variant="secondary" onClick={() => call.end(null)}> <Button type="button" variant="secondary" onClick={() => call.end(null)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit"> <Button type="submit">
<Trans>Enter</Trans> <Trans>Enter</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
</fieldset> </fieldset>
</form> </form>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}, });
);
@@ -1,10 +1,3 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import type { TNumberFieldMeta } from '@documenso/lib/types/field-meta'; import type { TNumberFieldMeta } from '@documenso/lib/types/field-meta';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -17,14 +10,13 @@ import {
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants'; import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export type SignFieldNumberDialogProps = { export type SignFieldNumberDialogProps = {
fieldMeta: TNumberFieldMeta; fieldMeta: TNumberFieldMeta;
@@ -115,10 +107,7 @@ export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps,
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((data) => call.end(data.number))}> <form onSubmit={form.handleSubmit((data) => call.end(data.number))}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="number" name="number"
@@ -1,17 +1,9 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { createCallable } from 'react-call';
import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure'; import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
@@ -23,18 +15,8 @@ export type SignFieldSignatureDialogProps = {
drawSignatureEnabled?: boolean; drawSignatureEnabled?: boolean;
}; };
export const SignFieldSignatureDialog = createCallable< export const SignFieldSignatureDialog = createCallable<SignFieldSignatureDialogProps, string | null>(
SignFieldSignatureDialogProps, ({ call, fullName, typedSignatureEnabled, uploadSignatureEnabled, drawSignatureEnabled, initialSignature }) => {
string | null
>(
({
call,
fullName,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
initialSignature,
}) => {
const [localSignature, setLocalSignature] = useState(initialSignature); const [localSignature, setLocalSignature] = useState(initialSignature);
return ( return (
@@ -64,11 +46,7 @@ export const SignFieldSignatureDialog = createCallable<
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button <Button type="button" disabled={!localSignature} onClick={() => call.end(localSignature || null)}>
type="button"
disabled={!localSignature}
onClick={() => call.end(localSignature || null)}
>
<Trans>Sign</Trans> <Trans>Sign</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1,11 +1,3 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Plural, useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import type { TTextFieldMeta } from '@documenso/lib/types/field-meta'; import type { TTextFieldMeta } from '@documenso/lib/types/field-meta';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -17,14 +9,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Textarea } from '@documenso/ui/primitives/textarea'; import { Textarea } from '@documenso/ui/primitives/textarea';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { createCallable } from 'react-call';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const ZSignFieldTextFormSchema = z.object({ const ZSignFieldTextFormSchema = z.object({
text: z.string().min(1, { message: msg`Text is required`.id }), text: z.string().min(1, { message: msg`Text is required`.id }),
@@ -36,80 +28,73 @@ export type SignFieldTextDialogProps = {
fieldMeta?: TTextFieldMeta; fieldMeta?: TTextFieldMeta;
}; };
export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, string | null>( export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, string | null>(({ call, fieldMeta }) => {
({ call, fieldMeta }) => { const { t } = useLingui();
const { t } = useLingui();
const form = useForm<TSignFieldTextFormSchema>({ const form = useForm<TSignFieldTextFormSchema>({
resolver: zodResolver(ZSignFieldTextFormSchema), resolver: zodResolver(ZSignFieldTextFormSchema),
defaultValues: { defaultValues: {
text: '', text: '',
}, },
}); });
return ( return (
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}> <Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{fieldMeta?.label || <Trans>Enter Text</Trans>}</DialogTitle> <DialogTitle>{fieldMeta?.label || <Trans>Enter Text</Trans>}</DialogTitle>
<DialogDescription className="mt-4"> <DialogDescription className="mt-4">
<Trans>Please enter a value</Trans> <Trans>Please enter a value</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((data) => call.end(data.text))}> <form onSubmit={form.handleSubmit((data) => call.end(data.text))}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4" <FormField
disabled={form.formState.isSubmitting} control={form.control}
> name="text"
<FormField render={({ field, fieldState }) => (
control={form.control} <FormItem>
name="text" <FormControl>
render={({ field, fieldState }) => ( <Textarea
<FormItem> id="custom-text"
<FormControl> placeholder={fieldMeta?.placeholder ?? t`Enter your text here`}
<Textarea className={cn('w-full rounded-md', {
id="custom-text" 'border-2 border-red-300 text-left ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
placeholder={fieldMeta?.placeholder ?? t`Enter your text here`} fieldState.error,
className={cn('w-full rounded-md', { })}
'border-2 border-red-300 text-left ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200': {...field}
fieldState.error, />
})} </FormControl>
{...field} <FormMessage />
{fieldMeta?.characterLimit !== undefined && fieldMeta?.characterLimit > 0 && !fieldState.error && (
<div className="text-muted-foreground text-sm">
<Plural
value={fieldMeta?.characterLimit - (field.value?.length ?? 0)}
one="# character remaining"
other="# characters remaining"
/> />
</FormControl> </div>
<FormMessage /> )}
{fieldMeta?.characterLimit !== undefined && </FormItem>
fieldMeta?.characterLimit > 0 && )}
!fieldState.error && ( />
<div className="text-sm text-muted-foreground">
<Plural
value={fieldMeta?.characterLimit - (field.value?.length ?? 0)}
one="# character remaining"
other="# characters remaining"
/>
</div>
)}
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button type="button" variant="secondary" onClick={() => call.end(null)}> <Button type="button" variant="secondary" onClick={() => call.end(null)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit"> <Button type="submit">
<Trans>Enter</Trans> <Trans>Enter</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>
</fieldset> </fieldset>
</form> </form>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}, });
);
@@ -1,22 +1,7 @@
import { useEffect, useMemo, 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 { useForm } from 'react-hook-form';
import { useSearchParams } from 'react-router';
import type { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL, SUPPORT_EMAIL } from '@documenso/lib/constants/app';
IS_BILLING_ENABLED,
NEXT_PUBLIC_WEBAPP_URL,
SUPPORT_EMAIL,
} from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamRequestSchema } from '@documenso/trpc/server/team-router/create-team.types'; import { ZCreateTeamRequestSchema } from '@documenso/trpc/server/team-router/create-team.types';
@@ -32,17 +17,19 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useSearchParams } from 'react-router';
import type { z } from 'zod';
export type TeamCreateDialogProps = { export type TeamCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
@@ -118,9 +105,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
description: _( description: _(msg`We encountered an unknown error while attempting to create a team. Please try again later.`),
msg`We encountered an unknown error while attempting to create a team. Please try again later.`,
),
variant: 'destructive', variant: 'destructive',
}); });
} }
@@ -166,11 +151,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
}, [open, form]); }, [open, form]);
return ( return (
<Dialog <Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button className="flex-shrink-0" variant="secondary"> <Button className="flex-shrink-0" variant="secondary">
@@ -194,15 +175,11 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
{dialogState === 'alert' && ( {dialogState === 'alert' && (
<> <>
<Alert <Alert className="flex flex-col justify-between p-6 sm:flex-row sm:items-center" variant="neutral">
className="flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<AlertDescription className="mt-0"> <AlertDescription className="mt-0">
<Trans> <Trans>
You have reached the maximum number of teams for your plan. Please contact sales You have reached the maximum number of teams for your plan. Please contact sales at{' '}
at <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you would like to <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you would like to adjust your plan.
adjust your plan.
</Trans> </Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -218,10 +195,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
{dialogState === 'form' && ( {dialogState === 'form' && (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField <FormField
control={form.control} control={form.control}
name="teamName" name="teamName"
@@ -264,7 +238,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
<Input className="bg-background" {...field} /> <Input className="bg-background" {...field} />
</FormControl> </FormControl>
{!form.formState.errors.teamUrl && ( {!form.formState.errors.teamUrl && (
<span className="text-xs font-normal text-foreground/50"> <span className="font-normal text-foreground/50 text-xs">
{field.value ? ( {field.value ? (
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}` `${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
) : ( ) : (
@@ -285,16 +259,9 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
<FormItem className="flex items-center space-x-2"> <FormItem className="flex items-center space-x-2">
<FormControl> <FormControl>
<div className="flex items-center"> <div className="flex items-center">
<Checkbox <Checkbox id="inherit-members" checked={field.value} onCheckedChange={field.onChange} />
id="inherit-members"
checked={field.value}
onCheckedChange={field.onChange}
/>
<label <label className="ml-2 text-muted-foreground text-sm" htmlFor="inherit-members">
className="ml-2 text-sm text-muted-foreground"
htmlFor="inherit-members"
>
<Trans>Allow all organisation members to access this team</Trans> <Trans>Allow all organisation members to access this team</Trans>
</label> </label>
</div> </div>
@@ -308,11 +275,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button <Button type="submit" data-testid="dialog-create-team-button" loading={form.formState.isSubmitting}>
type="submit"
data-testid="dialog-create-team-button"
loading={form.formState.isSubmitting}
>
<Trans>Create Team</Trans> <Trans>Create Team</Trans>
</Button> </Button>
</DialogFooter> </DialogFooter>

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