Compare commits

...

15 Commits

Author SHA1 Message Date
ephraimduncan 1bc090cb83 chore: add v1 API deprecation callouts and clean up imports
Add deprecation warning callouts to public API documentation pages.
Remove unused imports from webhook router and hooks.
2026-01-20 15:28:37 +00:00
ephraimduncan fd94a403af docs: mark all v1 API endpoints as deprecated
Add deprecated flag and migration message to all 19 v1 API endpoints
to signal users should migrate to the v2 API.
2026-01-20 14:36:35 +00:00
Ted Liang 158b36a9b7 fix: security CVE-2026-22817 CVE-2026-22818 (#2390) 2026-01-15 18:27:04 +11:00
Lucas Smith fabd69bd62 build: upgrade simplewebauthn packages from v9 to v13 (#2389)
The v9 packages are deprecated. This updates to v13 which includes
breaking API changes: optionsJSON wrapper for auth functions,
renamed properties (authenticator→credential), and base64 encoding
for credential IDs via isoBase64URL helper.
2026-01-15 14:22:37 +11:00
Lucas Smith c976e747e3 fix: dont flatten forms for templates (#2386)
Templates shouldn't have their form flattened until they're
converted to a document.
2026-01-14 12:06:28 +11:00
Lucas Smith 34f512bd55 docs: add OpenCode AI-assisted development guide (#2384)
Adds OpenCode support for AI-assisted development, including custom
commands and skills to help contributors maintain consistency and
streamline common workflows.

#### Changes
- Added "AI-Assisted Development with OpenCode" section to
CONTRIBUTING.md with:
  - Installation instructions and provider configuration
- Documentation for 8 custom commands (/implement, /continue,
/interview, /document, /commit, /create-plan, /create-scratch,
/create-justification)
  - Typical workflow guide
- Clear policy that AI-generated code must be reviewed before submission
- Added .agents/ directory for plans, scratches, and justifications
- Added .opencode/ commands and skills for the agent
- Added helper scripts for creating agent files
2026-01-14 10:10:20 +11:00
Karlo db913e95b6 fix: downgrade pdfjs-dist to version 5.4.296 and update react-pdf to version 10.3.0 (#2383) 2026-01-13 21:01:29 +11:00
Catalin Pit bb3e9583e4 feat: add default recipients for teams and orgs (#2248) 2026-01-13 20:32:00 +11:00
Lucas Smith 5bc73a7471 chore: npm audit fix (#2367) 2026-01-13 16:39:10 +11:00
Lucas Smith 06d7849146 chore: add translations (#2373) 2026-01-13 14:34:26 +11:00
Lucas Smith cef7987a72 feat: add audit logs to document details page (#2379)
- Add collapsible audit logs section with paginated table
- Add View JSON button to inspect raw audit log entries
- Display legacy document ID and recipient roles
- Add admin TRPC endpoint for fetching audit logs
- Add database index on envelopeId for DocumentAuditLog table

<img width="887" height="724" alt="image"
src="https://github.com/user-attachments/assets/aeb904c9-515f-49e1-9f8f-513aef455678"
/>
2026-01-13 14:18:10 +11:00
github-actions[bot] cf6f6bcea0 chore: extract translations (#2363) 2026-01-13 12:49:05 +11:00
Catalin Pit 2f27304750 refactor: simplify field dialog component (#2369) 2026-01-13 12:38:10 +11:00
Konrad 912530ca17 fix: mark document visibility options for translation (#2330) 2026-01-12 10:17:03 +11:00
Konrad a995961c4e fix: mark document auth types for translation (#2331) 2026-01-12 09:28:16 +11:00
112 changed files with 4867 additions and 1216 deletions
View File
View File
@@ -0,0 +1,186 @@
---
date: 2026-01-14
title: Simplewebauthn V13 Upgrade
---
## Overview
Upgrade SimpleWebAuthn packages from v9.x to v13.x to address the deprecation of `@simplewebauthn/types` and take advantage of new features and improvements.
## Current State
The codebase currently uses:
- `@simplewebauthn/browser@9.x`
- `@simplewebauthn/server@9.x`
- `@simplewebauthn/types@9.x`
## Breaking Changes Summary (v9 → v13)
### v10.0.0 Breaking Changes
1. **Minimum Node version raised to Node v20**
2. **`generateRegistrationOptions()` now expects `Base64URLString` for `excludeCredentials` IDs** (no more `type: 'public-key'` needed)
3. **`generateAuthenticationOptions()` now expects `Base64URLString` for `allowCredentials` IDs**
4. **`credentialID` returned from verification methods is now `Base64URLString`** instead of `Uint8Array`
5. **`AuthenticatorDevice.credentialID` is now `Base64URLString`**
6. **`rpID` is now required when calling `generateAuthenticationOptions()`**
7. **`generateRegistrationOptions()` will generate random user IDs** if not provided
8. **`user.id` is treated as base64url string in `startRegistration()`**
9. **`userHandle` is treated as base64url string in `startAuthentication()`**
### v11.0.0 Breaking Changes
1. **Positional arguments in `startRegistration()` and `startAuthentication()` replaced by object**
- Before: `startRegistration(options)`
- After: `startRegistration({ optionsJSON: options })`
- Before: `startAuthentication(options)`
- After: `startAuthentication({ optionsJSON: options })`
2. **`AuthenticatorDevice` type renamed to `WebAuthnCredential`**
- `credentialID``credential.id`
- `credentialPublicKey``credential.publicKey`
3. **`verifyRegistrationResponse()` returns `registrationInfo.credential` instead of individual properties**
- `credentialID``credential.id`
- `credentialPublicKey``credential.publicKey`
- `counter``credential.counter`
- `transports` are now in `credential.transports`
4. **`verifyAuthenticationResponse()` uses `credential` argument instead of `authenticator`**
### v13.0.0 Breaking Changes
1. **`@simplewebauthn/types` package is retired**
- Types are now exported from `@simplewebauthn/browser` and `@simplewebauthn/server`
- Import types from `@simplewebauthn/server` instead
## Files to Update
### Package Changes
1. Remove `@simplewebauthn/types` dependency
2. Update `@simplewebauthn/browser` to `^13.2.2`
3. Update `@simplewebauthn/server` to `^13.2.2`
### Server-side Files
#### 1. `packages/lib/server-only/auth/create-passkey-registration-options.ts`
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
- Remove `type: 'public-key'` from `excludeCredentials` items
- Update `userID` to use `isoUint8Array.fromUTF8String()` for proper encoding
#### 2. `packages/lib/server-only/auth/create-passkey-authentication-options.ts`
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
- Remove `type: 'public-key'` from `allowCredentials` items
#### 3. `packages/lib/server-only/auth/create-passkey-signin-options.ts`
- No changes needed (already using correct options)
#### 4. `packages/lib/server-only/auth/create-passkey.ts`
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
- Update to use new `registrationInfo.credential` structure:
- `credentialID``credential.id`
- `credentialPublicKey``credential.publicKey`
- `counter``credential.counter`
- Note: `credential.id` is now a `Base64URLString`, so `Buffer.from(credentialID)` needs updating
#### 5. `packages/lib/server-only/document/is-recipient-authorized.ts`
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`:
- Change `authenticator: { credentialID, credentialPublicKey, counter }` to `credential: { id, publicKey, counter }`
- Since `credential.id` is now base64url string, convert stored `credentialId` buffer to base64url
#### 6. `packages/auth/server/routes/passkey.ts`
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
- Same changes as `is-recipient-authorized.ts`
#### 7. `packages/trpc/server/auth-router/create-passkey.ts`
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
### Browser-side Files
#### 8. `apps/remix/app/components/dialogs/passkey-create-dialog.tsx`
- Update `startRegistration()` call:
- Before: `startRegistration(passkeyRegistrationOptions)`
- After: `startRegistration({ optionsJSON: passkeyRegistrationOptions })`
#### 9. `apps/remix/app/components/forms/signin.tsx`
- Update `startAuthentication()` call:
- Before: `startAuthentication(options)`
- After: `startAuthentication({ optionsJSON: options })`
#### 10. `apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx`
- Update `startAuthentication()` call:
- Before: `startAuthentication(options)`
- After: `startAuthentication({ optionsJSON: options })`
### Database/Schema Considerations
The database stores `credentialId` as `Bytes`. The new API returns `credential.id` as `Base64URLString`. We need to:
1. When **storing** a new passkey: Convert from `Base64URLString` to `Buffer`
2. When **passing to verification**: Convert from `Buffer` to `Base64URLString`
Use `isoBase64URL` helper from `@simplewebauthn/server/helpers` for these conversions.
## Implementation Steps
### Step 1: Update package.json dependencies
```bash
npm uninstall @simplewebauthn/types
npm install @simplewebauthn/browser@^13.2.2 @simplewebauthn/server@^13.2.2
```
### Step 2: Update type imports
Replace all `@simplewebauthn/types` imports with `@simplewebauthn/server`
### Step 3: Update browser-side API calls
- `startRegistration(options)``startRegistration({ optionsJSON: options })`
- `startAuthentication(options)``startAuthentication({ optionsJSON: options })`
### Step 4: Update server-side registration
- Update `excludeCredentials` format (remove `type: 'public-key'`)
- Update `userID` encoding if needed
- Update `verifyRegistrationResponse()` result handling for new `credential` structure
### Step 5: Update server-side authentication
- Update `allowCredentials` format (remove `type: 'public-key'`)
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
- Handle `Base64URLString` for `credential.id`
### Step 6: Update credential storage/retrieval
- When storing: Convert `Base64URLString` to `Buffer`
- When reading: Convert `Buffer` to `Base64URLString`
### Step 7: Test passkey flows
1. Test passkey creation
2. Test passkey sign-in
3. Test passkey authentication for document signing
4. Test passkey deletion
## Code Examples
### Converting stored Buffer to Base64URLString for verification
```typescript
import { isoBase64URL } from '@simplewebauthn/server/helpers';
// When reading from database (Buffer) and passing to verification
const credential = {
id: isoBase64URL.fromBuffer(passkey.credentialId),
publicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
transports: passkey.transports,
};
```
### Converting Base64URLString to Buffer for storage
```typescript
import { isoBase64URL } from '@simplewebauthn/server/helpers';
// When storing from registration response
const credentialIdBuffer = Buffer.from(
isoBase64URL.toBuffer(registrationInfo.credential.id)
);
```
## Risks and Mitigations
1. **Database compatibility**: The `credentialId` is stored as `Bytes` in the database. The new API uses `Base64URLString`. We need proper conversion functions.
- **Mitigation**: Use `isoBase64URL.fromBuffer()` and `isoBase64URL.toBuffer()` for conversions
2. **Existing passkeys**: Existing passkeys should continue to work as long as conversion is done correctly.
- **Mitigation**: Test with existing passkeys after upgrade
3. **Browser compatibility**: v10+ requires newer browser APIs.
- **Mitigation**: `browserSupportsWebAuthn()` already handles this check
View File
@@ -0,0 +1,103 @@
# V1 API Legacy Deprecation Specification
## Overview
Mark all V1 API endpoints as legacy/deprecated in the OpenAPI documentation to signal that users should migrate to the V2 API.
## Decision Summary
| Decision | Choice |
|----------|--------|
| Deprecation Scope | All V1 endpoints |
| Message Style | Generic for all endpoints |
| Sunset Timeline | No specific date |
| V2 API Status | V2 exists now |
| OpenAPI Info | Update to include deprecation notice |
## Implementation Details
### Files to Modify
1. `packages/api/v1/contract.ts` - Add `deprecated: true` and `description` to all endpoints
2. `packages/api/v1/openapi.ts` - Update the OpenAPI info description
### Documentation Files to Modify
1. `apps/documentation/pages/developers/public-api/index.mdx` - Add deprecation `<Callout type="warning">` under "API V1 - Deprecated" section:
```
<Callout type="warning">
V1 API is deprecated and will be removed in a future release. Please migrate to V2.
</Callout>
```
2. `apps/documentation/pages/developers/public-api/versioning.mdx` - Add callout after "current version is v2" stating V1 is deprecated:
```
<Callout type="warning">
V1 API is deprecated. Please migrate to V2.
</Callout>
```
### Deprecation Message
**Per-endpoint message:**
```
Deprecated. Please migrate to the V2 API.
```
**OpenAPI info description:**
```
[DEPRECATED] The Documenso API for retrieving, creating, updating and deleting documents. Please migrate to V2.
```
### Endpoints to Update (19 total)
| Endpoint | Method | Path | Action |
|----------|--------|------|--------|
| getDocuments | GET | /api/v1/documents | Add deprecation |
| getDocument | GET | /api/v1/documents/:id | Add deprecation |
| downloadSignedDocument | GET | /api/v1/documents/:id/download | Add deprecation |
| createDocument | POST | /api/v1/documents | Add deprecation |
| createTemplate | POST | /api/v1/templates | Add deprecation |
| deleteTemplate | DELETE | /api/v1/templates/:id | Add deprecation |
| getTemplate | GET | /api/v1/templates/:id | Add deprecation |
| getTemplates | GET | /api/v1/templates | Add deprecation |
| createDocumentFromTemplate | POST | /api/v1/templates/:templateId/create-document | Update existing deprecation message |
| generateDocumentFromTemplate | POST | /api/v1/templates/:templateId/generate-document | Add deprecation |
| sendDocument | POST | /api/v1/documents/:id/send | Add deprecation |
| resendDocument | POST | /api/v1/documents/:id/resend | Add deprecation |
| deleteDocument | DELETE | /api/v1/documents/:id | Add deprecation |
| createRecipient | POST | /api/v1/documents/:id/recipients | Add deprecation |
| updateRecipient | PATCH | /api/v1/documents/:id/recipients/:recipientId | Add deprecation |
| deleteRecipient | DELETE | /api/v1/documents/:id/recipients/:recipientId | Add deprecation |
| createField | POST | /api/v1/documents/:id/fields | Add deprecation |
| updateField | PATCH | /api/v1/documents/:id/fields/:fieldId | Add deprecation |
| deleteField | DELETE | /api/v1/documents/:id/fields/:fieldId | Add deprecation |
## Changes Required
### contract.ts Changes
For each endpoint, add:
```typescript
deprecated: true,
description: 'Deprecated. Please migrate to the V2 API.',
```
For endpoints that already have a description, prepend the deprecation notice.
### openapi.ts Changes
Update the info block description from:
```typescript
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
```
to:
```typescript
description: '[DEPRECATED] The Documenso API for retrieving, creating, updating and deleting documents. Please migrate to V2.',
```
## Notes
- The `createDocumentFromTemplate` endpoint's existing deprecation message (pointing to `generateDocumentFromTemplate`) will be replaced with the generic V2 migration message
- No sunset date is included - endpoints are marked deprecated but without a removal timeline
- The V2 API is available and ready for users to migrate to
+80
View File
@@ -0,0 +1,80 @@
---
description: Add and commit changes using conventional commits
allowed-tools: Bash, Read, Glob, Grep
---
Create a git commit for the current changes using the Conventional Commits standard.
## Process
1. **Analyze the changes** by running:
- `git status` to see all modified/untracked files
- `git diff` to see unstaged changes
- `git diff --staged` to see already-staged changes
- `git log --oneline -5` to see recent commit style
2. **Stage appropriate files**:
- Stage all related changes with `git add`
- Do NOT stage files that appear to contain secrets (.env, credentials, API keys, tokens)
- If you detect potential secrets, warn the user and skip those files
3. **Determine the commit type** based on the changes:
- `feat`: New feature or capability
- `fix`: Bug fix
- `docs`: Documentation only
- `style`: Formatting, whitespace (not CSS)
- `refactor`: Code restructuring without behavior change
- `perf`: Performance improvement
- `test`: Adding or updating tests
- `build`: Build system or dependencies
- `ci`: CI/CD configuration
- `chore`: Maintenance tasks, tooling, config
NOTE: Do not use a scope for commits
4. **Write the commit message**:
- **Subject line**: `<type>: <description>`
- Use imperative mood ("add" not "added")
- Lowercase, no period at end
- Max 50 characters if possible, 72 hard limit
- **Body** (if needed): Explain _why_, not _what_
- Wrap at 72 characters
- Separate from subject with blank line
## Commit Format
```
<type>[scope]: <subject>
[optional body explaining WHY this change was made]
```
## Examples
Simple change:
```
fix: handle empty input in parser without throwing
```
With body:
```
feat: add streaming response support
Large responses were causing memory issues in production.
Streaming allows processing chunks incrementally.
```
## Rules
- NEVER commit files that may contain secrets
- NEVER use `git commit --amend` unless the user explicitly requests it
- NEVER use `--no-verify` to skip hooks
- If the pre-commit hook fails, fix the issues and create a NEW commit
- If there are no changes to commit, inform the user and stop
- Use a HEREDOC to pass the commit message to ensure proper formatting
## Execute
Run the git commands to analyze, stage, and commit the changes now.
+112
View File
@@ -0,0 +1,112 @@
---
description: Continue implementing a spec from a previous session
argument-hint: <spec-file-path>
---
You are continuing implementation of a specification that was started in a previous session. Work autonomously until the feature is complete and tests pass.
## Your Task
1. **Read the spec** at `$ARGUMENTS`
2. **Read CODE_STYLE.md** for formatting conventions
3. **Assess current state**:
- Check git status for uncommitted changes
- Run tests to see what's passing/failing (if E2E tests exist)
- Review any existing implementation
4. **Determine what remains** by comparing the spec to the current state
5. **Plan remaining work** using TodoWrite
6. **Continue implementing** until complete
## Assessing Current State
Run these commands to understand where the previous session left off:
```bash
git status # See uncommitted changes
git log --oneline -10 # See recent commits
npm run typecheck -w @documenso/remix # Check for type errors
npm run lint:fix # Check for linting issues
```
Review the code that's already been written to understand:
- What's already implemented
- What's partially done
- What's not started yet
## Implementation Guidelines
### During Implementation
- Follow CODE_STYLE.md strictly (2-space indent, double quotes, braces always, etc.)
- Follow workspace rules for TypeScript, React, TRPC patterns, and Remix conventions
- Mark todos complete as you finish each task
- Commit logical chunks of work
### Code Quality
- No stubbed implementations
- Handle edge cases and error conditions
- Include descriptive error messages with context
- Use async/await for all I/O operations
- Use AppError class when throwing errors
- Use Zod for validation and react-hook-form for forms
### Testing
**Important**: E2E tests are time-consuming. Only write tests for non-trivial functionality.
- Write E2E tests in `packages/app-tests/e2e/` using Playwright
- Test critical user flows and edge cases
- Follow existing E2E test patterns in the codebase
- Use descriptive test names that explain what is being tested
- Skip tests for trivial changes (simple UI tweaks, minor refactors, etc.)
## Autonomous Workflow
Work continuously through these steps:
1. **Implement** - Write the code for the current task
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
3. **Lint** - Run `npm run lint:fix` to fix linting issues
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
5. **Fix** - If tests fail, fix and re-run
6. **Repeat** - Move to next task
## Stopping Conditions
**Stop and report success when:**
- All spec requirements are implemented
- Typecheck passes
- Lint passes
- E2E tests pass (if written for non-trivial functionality)
**Stop and ask for help when:**
- The spec is ambiguous and you need clarification
- You encounter a blocking issue you cannot resolve
- You need to make a decision that significantly deviates from the spec
- External dependencies are missing
## Commands
```bash
# Type checking
npm run typecheck -w @documenso/remix
# Linting
npm run lint:fix
# E2E Tests (only for non-trivial work)
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
npm run test:e2e # Run full E2E test suite
# Development
npm run dev # Start dev server
```
## Begin
Read the spec file and CODE_STYLE.md, assess the current implementation state, then continue where the previous session left off. Use TodoWrite to track your progress throughout.
@@ -0,0 +1,75 @@
---
description: Create a new justification file in .agents/justifications/
argument-hint: <justification-slug> [content]
---
You are creating a new justification file in the `.agents/justifications/` directory.
## Your Task
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
2. **Gather content** - Collect or generate the justification content
3. **Create the file** - Use the create-justification script to generate the file
## Usage
The script will automatically:
- Generate a unique three-word ID (e.g., `swift-emerald-river`)
- Create frontmatter with current date and formatted title
- Save the file as `{id}-{slug}.md` in `.agents/justifications/`
## Creating the File
### Option 1: Direct Content
If you have the content ready, run:
```bash
npx tsx scripts/create-justification.ts "$ARGUMENTS" "Your justification content here"
```
### Option 2: Multi-line Content (Heredoc)
For multi-line content, use heredoc:
```bash
npx tsx scripts/create-justification.ts "$ARGUMENTS" << HEREDOC
Your multi-line
justification content
goes here
HEREDOC
```
### Option 3: Pipe Content
You can also pipe content:
```bash
echo "Your content" | npx tsx scripts/create-justification.ts "$ARGUMENTS"
```
## File Format
The created file will have:
```markdown
---
date: 2026-01-13
title: Justification Title
---
Your content here
```
The title is automatically formatted from the slug (e.g., `architecture-decision``Architecture Decision`).
## Guidelines
- Use descriptive slugs in kebab-case (e.g., `tech-stack-choice`, `api-design-rationale`)
- Include clear reasoning and context for the decision
- The unique ID ensures no filename conflicts
- Files are automatically dated for organization
## Begin
Create a justification file using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification.
+76
View File
@@ -0,0 +1,76 @@
---
description: Create a new plan file in .agents/plans/
argument-hint: <plan-slug> [content]
---
You are creating a new plan file in the `.agents/plans/` directory.
## Your Task
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
2. **Gather content** - Collect or generate the plan content
3. **Create the file** - Use the create-plan script to generate the file
## Usage
The script will automatically:
- Generate a unique three-word ID (e.g., `happy-blue-moon`)
- Create frontmatter with current date and formatted title
- Save the file as `{id}-{slug}.md` in `.agents/plans/`
## Creating the File
### Option 1: Direct Content
If you have the content ready, run:
```bash
npx tsx scripts/create-plan.ts "$ARGUMENTS" "Your plan content here"
```
### Option 2: Multi-line Content (Heredoc)
For multi-line content, use heredoc:
```bash
npx tsx scripts/create-plan.ts "$ARGUMENTS" << HEREDOC
Your multi-line
plan content
goes here
HEREDOC
```
### Option 3: Pipe Content
You can also pipe content:
```bash
echo "Your content" | npx tsx scripts/create-plan.ts "$ARGUMENTS"
```
## File Format
The created file will have:
```markdown
---
date: 2026-01-13
title: Plan Title
---
Your content here
```
The title is automatically formatted from the slug (e.g., `my-feature``My Feature`).
## Guidelines
- Use descriptive slugs in kebab-case (e.g., `user-authentication`, `api-integration`)
- Include clear, actionable plan content
- The unique ID ensures no filename conflicts
- Files are automatically dated for organization
## Begin
Create a plan file using the slug from `$ARGUMENTS` and appropriate content for the planning task.
+75
View File
@@ -0,0 +1,75 @@
---
description: Create a new scratch file in .agents/scratches/
argument-hint: <scratch-slug> [content]
---
You are creating a new scratch file in the `.agents/scratches/` directory.
## Your Task
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
2. **Gather content** - Collect or generate the scratch content
3. **Create the file** - Use the create-scratch script to generate the file
## Usage
The script will automatically:
- Generate a unique three-word ID (e.g., `calm-teal-cloud`)
- Create frontmatter with current date and formatted title
- Save the file as `{id}-{slug}.md` in `.agents/scratches/`
## Creating the File
### Option 1: Direct Content
If you have the content ready, run:
```bash
npx tsx scripts/create-scratch.ts "$ARGUMENTS" "Your scratch content here"
```
### Option 2: Multi-line Content (Heredoc)
For multi-line content, use heredoc:
```bash
npx tsx scripts/create-scratch.ts "$ARGUMENTS" << HEREDOC
Your multi-line
scratch content
goes here
HEREDOC
```
### Option 3: Pipe Content
You can also pipe content:
```bash
echo "Your content" | npx tsx scripts/create-scratch.ts "$ARGUMENTS"
```
## File Format
The created file will have:
```markdown
---
date: 2026-01-13
title: Scratch Title
---
Your content here
```
The title is automatically formatted from the slug (e.g., `quick-notes``Quick Notes`).
## Guidelines
- Use descriptive slugs in kebab-case (e.g., `exploration-ideas`, `temporary-notes`)
- Scratch files are for temporary notes, explorations, or ideas
- The unique ID ensures no filename conflicts
- Files are automatically dated for organization
## Begin
Create a scratch file using the slug from `$ARGUMENTS` and appropriate content for notes or exploration.
+201
View File
@@ -0,0 +1,201 @@
---
description: Generate MDX documentation for a module or feature
argument-hint: <module-path-or-feature>
---
You are creating proper MDX documentation for a module or feature in Documenso using Nextra.
## Your Task
1. **Identify the scope** - What does `$ARGUMENTS` refer to? (file, directory, or feature name)
2. **Read the source code** - Understand the public API, types, and behavior
3. **Read existing docs** - Check if there's documentation to update or reference
4. **Write comprehensive documentation** - Create or update MDX docs in the appropriate location
5. **Update navigation** - Add entry to `_meta.js` if creating a new page
## Documentation Structure
Create documentation in the appropriate location:
- **Developer docs**: `apps/documentation/pages/developers/`
- **User docs**: `apps/documentation/pages/users/`
### File Format
All documentation files must be `.mdx` files with frontmatter:
```mdx
---
title: Page Title
description: Brief description for SEO and meta tags
---
# Page Title
Content starts here...
```
### Navigation
Each directory should have a `_meta.js` file that defines the navigation structure:
```javascript
export default {
index: 'Introduction',
'feature-name': 'Feature Name',
'another-feature': 'Another Feature',
};
```
If creating a new page, add it to the appropriate `_meta.js` file.
### Documentation Format
````mdx
---
title: <Module|Feature Name>
description: Brief description of what this does and when to use it
---
# <Module|Feature Name>
Brief description of what this module/feature does and when to use it.
## Installation
If there are specific packages or imports needed:
```bash
npm install @documenso/package-name
```
## Quick Start
```jsx
// Minimal working example
import { Component } from '@documenso/package';
const Example = () => {
return <Component />;
};
```
## API Reference
### Component/Function Name
Description of what it does.
#### Props/Parameters
| Prop/Param | Type | Description |
| ---------- | -------------------- | ------------------------- |
| prop | `string` | Description of the prop |
| optional | `boolean` (optional) | Optional prop description |
#### Example
```jsx
import { Component } from '@documenso/package';
<Component prop="value" optional={true} />;
```
### Types
#### `TypeName`
```typescript
type TypeName = {
property: string;
optional?: boolean;
};
```
## Examples
### Common Use Case
```jsx
// Full working example
```
### Advanced Usage
```jsx
// More complex example
```
## Related
- [Link to related documentation](/developers/path)
- [Another related page](/users/path)
````
## Guidelines
### Content Quality
- **Be accurate** - Verify behavior by reading the code
- **Be complete** - Document all public API surface
- **Be practical** - Include real, working examples
- **Be concise** - Don't over-explain obvious things
- **Be user-focused** - Write for the target audience (developers or users)
### Code Examples
- Use appropriate language tags: `jsx`, `tsx`, `typescript`, `bash`, `json`
- Show imports when not obvious
- Include expected output in comments where helpful
- Progress from simple to complex
- Use real examples from the codebase when possible
### Formatting
- Always include frontmatter with `title` and `description`
- Use proper markdown headers (h1 for title, h2 for sections)
- Use tables for props/parameters documentation (matching existing style)
- Use code fences with appropriate language tags
- Use Nextra components when appropriate:
- `<Callout type="info">` for notes
- `<Steps>` for step-by-step instructions
- Use relative links for internal documentation (e.g., `/developers/embedding/react`)
### Nextra Components
You can import and use Nextra components:
```jsx
import { Callout, Steps } from 'nextra/components';
<Callout type="info">
This is an informational note.
</Callout>
<Steps>
<Steps.Step>First step</Steps.Step>
<Steps.Step>Second step</Steps.Step>
</Steps>
```
### Maintenance
- Include types inline so docs don't get stale
- Reference source file locations for complex behavior
- Keep examples up-to-date with the codebase
- Update `_meta.js` when adding new pages
## Process
1. **Explore the code** - Read source files to understand the API
2. **Identify the audience** - Is this for developers or users?
3. **Check existing docs** - Look for similar pages to match style
4. **Draft the structure** - Outline sections before writing
5. **Write content** - Fill in each section with frontmatter
6. **Add examples** - Create working code samples
7. **Update navigation** - Add to `_meta.js` if needed
8. **Review** - Read through for clarity and accuracy
## Begin
Analyze `$ARGUMENTS`, read the relevant source code, check existing documentation patterns, and create comprehensive MDX documentation following the Documenso documentation style.
+100
View File
@@ -0,0 +1,100 @@
---
description: Implement a spec from the plans directory
argument-hint: <spec-file-path>
---
You are implementing a specification from the `.agents/plans/` directory. Work autonomously until the feature is complete and tests pass.
## Your Task
1. **Read the spec** at `$ARGUMENTS`
2. **Read CODE_STYLE.md** for formatting conventions
3. **Plan the implementation** using the TodoWrite tool to break down the work
4. **Implement the feature** following the spec and code style
5. **Write E2E tests** only for non-trivial functionality (E2E tests are time-consuming)
6. **Run tests** and fix any failures
7. **Run typecheck and lint** and fix any issues
## Implementation Guidelines
### Before Coding
- Understand the spec's goals and scope
- Identify the desired API from usage examples in the spec
- Review related existing code to understand patterns
- Break the work into discrete tasks using TodoWrite
### During Implementation
- Follow CODE_STYLE.md strictly (2-space indent, double quotes, braces always, etc.)
- Follow workspace rules for TypeScript, React, TRPC patterns, and Remix conventions
- Mark todos complete as you finish each task
- Commit logical chunks of work
### Code Quality
- No stubbed implementations
- Handle edge cases and error conditions
- Include descriptive error messages with context
- Use async/await for all I/O operations
- Use AppError class when throwing errors
- Use Zod for validation and react-hook-form for forms
### Testing
**Important**: E2E tests are time-consuming. Only write tests for non-trivial functionality.
- Write E2E tests in `packages/app-tests/e2e/` using Playwright
- Test critical user flows and edge cases
- Follow existing E2E test patterns in the codebase
- Use descriptive test names that explain what is being tested
- Skip tests for trivial changes (simple UI tweaks, minor refactors, etc.)
## Autonomous Workflow
Work continuously through these steps:
1. **Implement** - Write the code for the current task
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
3. **Lint** - Run `npm run lint:fix` to fix linting issues
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
5. **Fix** - If tests fail, fix and re-run
6. **Repeat** - Move to next task
## Stopping Conditions
**Stop and report success when:**
- All spec requirements are implemented
- Typecheck passes
- Lint passes
- E2E tests pass (if written for non-trivial functionality)
**Stop and ask for help when:**
- The spec is ambiguous and you need clarification
- You encounter a blocking issue you cannot resolve
- You need to make a decision that significantly deviates from the spec
- External dependencies are missing
## Commands
```bash
# Type checking
npm run typecheck -w @documenso/remix
# Linting
npm run lint:fix
# E2E Tests (only for non-trivial work)
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
npm run test:e2e # Run full E2E test suite
# Development
npm run dev # Start dev server
```
## Begin
Read the spec file and CODE_STYLE.md, then start implementing. Use TodoWrite to track your progress throughout.
+57
View File
@@ -0,0 +1,57 @@
---
description: Deep-dive interview to flesh out a spec or design document
agent: build
argument-hint: <file-path>
---
You are conducting a thorough interview to help flesh out and complete a specification or design document.
## Your Task
1. **Read the document** at `$ARGUMENTS`
2. **Analyze it deeply** - identify gaps, ambiguities, unexplored edge cases, and areas needing clarification
3. **Interview the user** by providing a question with some pre-determined options
4. **Write the completed spec** back to the file when the interview is complete
## Interview Guidelines
### Question Quality
- Ask **non-obvious, insightful questions** - avoid surface-level queries
- Focus on: technical implementation details, architectural decisions, edge cases, error handling, UX implications, security considerations, performance tradeoffs, integration points, migration strategies, rollback plans
- Each question should reveal something that would otherwise be missed
- Challenge assumptions embedded in the document
- Explore second and third-order consequences of design decisions
- Use the Web Search and other tools where required to ground questions (e.g. package recommendations)
### Question Strategy
- Start by identifying the 3-5 most critical unknowns or ambiguities
- Use the AskUserQuestion tool with well-crafted options that represent real tradeoffs
- When appropriate, offer multiple valid approaches with their pros/cons as options
- Don't ask about things that are already clearly specified
- Probe deeper when answers reveal new areas of uncertainty
### Topics to Explore (as relevant)
- **Technical**: Data models, API contracts, state management, concurrency, caching, validation
- **UX**: Error states, loading states, empty states, edge cases, accessibility, mobile considerations
- **Operations**: Deployment, monitoring, alerting, debugging, logging, feature flags
- **Security**: Auth, authz, input validation, rate limiting, audit trails
- **Scale**: Performance bottlenecks, data growth, traffic spikes, graceful degradation
- **Integration**: Dependencies, backwards compatibility, versioning, migration path
- **Failure modes**: What happens when X fails? How do we recover? What's the blast radius?
### Interview Flow
1. Ask 2-4 questions at a time (use multiple questions in one when they're related)
2. After each round, incorporate answers and identify follow-up questions
3. Continue until all critical areas are addressed
4. Signal when you believe the interview is complete, but offer to go deeper
## Output
When the interview is complete:
1. Synthesize all gathered information
2. Rewrite/expand the original document with the new details
3. Preserve the document's original structure where sensible, but reorganize if needed
4. Add new sections for areas that weren't originally covered
5. Write the completed spec back to `$ARGUMENTS`
Begin by reading the file and identifying your first set of deep questions.
@@ -0,0 +1,56 @@
---
name: create-justification
description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: decision-making
---
## What I do
I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets:
- A unique three-word identifier (e.g., `swift-emerald-river`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
Multi-line
justification content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `swift-emerald-river-decision-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Decision Name
---
Your content here
```
## When to use me
Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
+56
View File
@@ -0,0 +1,56 @@
---
name: create-plan
description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: planning
---
## What I do
I help you create new plan files in the `.agents/plans/` directory. Each plan file gets:
- A unique three-word identifier (e.g., `happy-blue-moon`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
Multi-line
plan content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `happy-blue-moon-feature-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Feature Name
---
Your content here
```
## When to use me
Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
+56
View File
@@ -0,0 +1,56 @@
---
name: create-scratch
description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: exploration
---
## What I do
I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets:
- A unique three-word identifier (e.g., `calm-teal-cloud`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
Multi-line
scratch content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `calm-teal-cloud-note-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Note Name
---
Your content here
```
## When to use me
Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
+2
View File
@@ -11,6 +11,8 @@
- `npm run format` - Format code with Prettier
- `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.
## Code Style Guidelines
- Use TypeScript for all code; prefer `type` over `interface`
+50
View File
@@ -52,3 +52,53 @@ You can build the project with:
```bash
npm run build
```
## AI-Assisted Development with OpenCode
We use [OpenCode](https://opencode.ai) for AI-assisted development. OpenCode provides custom commands and skills to help maintain consistency and streamline common workflows.
OpenCode works with most major AI providers (Anthropic, OpenAI, Google, etc.) or you can use [Zen](https://opencode.ai/zen) for optimized coding models. Configure your preferred provider in the OpenCode settings.
> **Important**: All AI-generated code must be thoroughly reviewed by the contributor before submitting a PR. You are responsible for understanding and validating every line of code you submit. If we detect that contributors are simply throwing AI-generated code over the wall without proper review, they will be blocked from the repository.
### Getting Started
1. Install OpenCode (see [opencode.ai](https://opencode.ai) for other install methods):
```bash
curl -fsSL https://opencode.ai/install | bash
```
2. Configure your AI provider (or use Zen for optimized models)
3. Run `opencode` in the project root
### Available Commands
Use these commands in OpenCode by typing the command name:
| Command | Description |
| ------------------------------ | -------------------------------------------------------- |
| `/implement <spec-path>` | Implement a spec from `.agents/plans/` autonomously |
| `/continue <spec-path>` | Continue implementing a spec from a previous session |
| `/interview <file-path>` | Deep-dive interview to flesh out a spec or design |
| `/document <module-path>` | Generate MDX documentation for a module or feature |
| `/commit` | Create a conventional commit for staged changes |
| `/create-plan <slug>` | Create a new plan file in `.agents/plans/` |
| `/create-scratch <slug>` | Create a scratch file for notes in `.agents/scratches/` |
| `/create-justification <slug>` | Create a justification file in `.agents/justifications/` |
### Typical Workflow
1. **Create a plan**: Use `/create-plan my-feature` to draft a spec for a new feature
2. **Flesh out the spec**: Use `/interview .agents/plans/<file>.md` to refine requirements
3. **Implement**: Use `/implement .agents/plans/<file>.md` to build the feature
4. **Continue if needed**: Use `/continue .agents/plans/<file>.md` to pick up where you left off
5. **Commit**: Use `/commit` to create a conventional commit
### Agent Files
The `.agents/` directory stores AI-generated artifacts:
- **`.agents/plans/`** - Feature specs and implementation plans
- **`.agents/scratches/`** - Temporary notes and explorations
- **`.agents/justifications/`** - Decision rationale and technical justifications
These files use a unique ID format (`{word}-{word}-{word}-{slug}.md`) to prevent conflicts.
@@ -31,6 +31,10 @@ Our new API V2 supports the following typed SDKs:
## API V1 - Deprecated
<Callout type="warning">
v1 API is deprecated and will be removed in a future release. Please migrate to v2.
</Callout>
Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods.
📖 [Documentation](https://documen.so/api-v2-docs)
@@ -11,6 +11,8 @@ Documenso uses API versioning to manage changes to the public API. This allows u
<Callout type="info">The current version of the API is `v2`.</Callout>
<Callout type="warning">v1 API is deprecated. Please migrate to v2.</Callout>
The API version is specified in the URL. For example, the base URL for the `v2` API is `https://app.documenso.com/api/v2`.
We may make changes to the API without incrementing the version number. We will always try to avoid breaking changes, but in some cases, it may be necessary to make changes that are not backward compatible. In these cases, we will increment the version number and provide information about the changes in the release notes.
@@ -73,7 +73,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
try {
const passkeyRegistrationOptions = await createPasskeyRegistrationOptions();
const registrationResult = await startRegistration(passkeyRegistrationOptions);
const registrationResult = await startRegistration({
optionsJSON: passkeyRegistrationOptions,
});
await createPasskey({
passkeyName,
@@ -82,9 +82,7 @@ export const SignFieldCheckboxDialog = createCallable<
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Sign Checkbox Field</Trans>
</DialogTitle>
<DialogTitle>{fieldMeta.label || <Trans>Select Options</Trans>}</DialogTitle>
<DialogDescription
className={cn('mt-4', {
@@ -143,7 +141,7 @@ export const SignFieldCheckboxDialog = createCallable<
<div className="flex items-center">
<Checkbox
id={`checkbox-value-${index}`}
className="data-[state=checked]:bg-primary border-foreground/30 h-5 w-5"
className="h-5 w-5 border-foreground/30 data-[state=checked]:bg-primary"
checked={field.value.checked}
onCheckedChange={(checked) => {
field.onChange({
@@ -154,7 +152,7 @@ export const SignFieldCheckboxDialog = createCallable<
/>
<label
className="text-muted-foreground ml-2 w-full text-sm"
className="ml-2 w-full text-sm text-muted-foreground"
htmlFor={`checkbox-value-${index}`}
>
{value.value}
@@ -174,7 +172,7 @@ export const SignFieldCheckboxDialog = createCallable<
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Confirm</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -50,11 +50,11 @@ export const SignFieldEmailDialog = createCallable<SignFieldEmailDialogProps, st
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Sign Email</Trans>
<Trans>Enter Email</Trans>
</DialogTitle>
<DialogDescription className="mt-4">
<Trans>Sign your email into the field</Trans>
<Trans>Please enter your email address</Trans>
</DialogDescription>
</DialogHeader>
@@ -83,7 +83,7 @@ export const SignFieldEmailDialog = createCallable<SignFieldEmailDialogProps, st
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Enter</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -48,11 +48,11 @@ export const SignFieldInitialsDialog = createCallable<SignFieldInitialsDialogPro
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Sign Initials</Trans>
<Trans>Enter Initials</Trans>
</DialogTitle>
<DialogDescription className="mt-4">
<Trans>Sign your initials into the field</Trans>
<Trans>Please enter your initials</Trans>
</DialogDescription>
</DialogHeader>
@@ -84,7 +84,7 @@ export const SignFieldInitialsDialog = createCallable<SignFieldInitialsDialogPro
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Enter</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -47,11 +47,11 @@ export const SignFieldNameDialog = createCallable<SignFieldNameDialogProps, stri
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Sign Name</Trans>
<Trans>Enter Name</Trans>
</DialogTitle>
<DialogDescription className="mt-4">
<Trans>Sign your full name into the field</Trans>
<Trans>Please enter your full name</Trans>
</DialogDescription>
</DialogHeader>
@@ -80,7 +80,7 @@ export const SignFieldNameDialog = createCallable<SignFieldNameDialogProps, stri
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Enter</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -22,7 +22,6 @@ import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
@@ -107,12 +106,10 @@ export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps,
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Sign Number Field</Trans>
</DialogTitle>
<DialogTitle>{fieldMeta.label || <Trans>Enter Number</Trans>}</DialogTitle>
<DialogDescription className="mt-4">
<Trans>Insert a value into the number field</Trans>
<Trans>Please enter a number</Trans>
</DialogDescription>
</DialogHeader>
@@ -127,8 +124,6 @@ export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps,
name="number"
render={({ field, fieldState }) => (
<FormItem>
{fieldMeta.label && <FormLabel>{fieldMeta.label}</FormLabel>}
<FormControl>
<Input
placeholder={fieldMeta.placeholder ?? t`Enter your number here`}
@@ -150,7 +145,7 @@ export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps,
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Enter</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -22,7 +22,6 @@ import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Textarea } from '@documenso/ui/primitives/textarea';
@@ -52,12 +51,10 @@ export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, stri
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Sign Text Field</Trans>
</DialogTitle>
<DialogTitle>{fieldMeta?.label || <Trans>Enter Text</Trans>}</DialogTitle>
<DialogDescription className="mt-4">
<Trans>Insert a value into the text field</Trans>
<Trans>Please enter a value</Trans>
</DialogDescription>
</DialogHeader>
@@ -72,8 +69,6 @@ export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, stri
name="text"
render={({ field, fieldState }) => (
<FormItem>
{fieldMeta?.label && <FormLabel>{fieldMeta?.label}</FormLabel>}
<FormControl>
<Textarea
id="custom-text"
@@ -89,7 +84,7 @@ export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, stri
{fieldMeta?.characterLimit !== undefined &&
fieldMeta?.characterLimit > 0 &&
!fieldState.error && (
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
<Plural
value={fieldMeta?.characterLimit - (field.value?.length ?? 0)}
one="# character remaining"
@@ -107,7 +102,7 @@ export const SignFieldTextDialog = createCallable<SignFieldTextDialogProps, stri
</Button>
<Button type="submit">
<Trans>Sign</Trans>
<Trans>Enter</Trans>
</Button>
</DialogFooter>
</fieldset>
@@ -3,7 +3,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { TeamGlobalSettings } from '@prisma/client';
import { DocumentVisibility, OrganisationType } from '@prisma/client';
import { DocumentVisibility, OrganisationType, type RecipientRole } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -17,14 +17,19 @@ import {
isValidLanguageCode,
} from '@documenso/lib/constants/i18n';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import type { TDefaultRecipients } from '@documenso/lib/types/default-recipients';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import {
type TDocumentMetaDateFormat,
ZDocumentMetaTimezoneSchema,
} from '@documenso/lib/types/document-meta';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip';
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
import { Alert } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import { Combobox } from '@documenso/ui/primitives/combobox';
import {
@@ -45,6 +50,10 @@ import {
SelectValue,
} from '@documenso/ui/primitives/select';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
/**
* Can't infer this from the schema since we need to keep the schema inside the component to allow
* it to be dynamic.
@@ -58,6 +67,7 @@ export type TDocumentPreferencesFormSchema = {
includeSigningCertificate: boolean | null;
includeAuditLog: boolean | null;
signatureTypes: DocumentSignatureType[];
defaultRecipients: TDefaultRecipients | null;
delegateDocumentOwnership: boolean | null;
aiFeaturesEnabled: boolean | null;
};
@@ -74,6 +84,7 @@ type SettingsSubset = Pick<
| 'typedSignatureEnabled'
| 'uploadSignatureEnabled'
| 'drawSignatureEnabled'
| 'defaultRecipients'
| 'delegateDocumentOwnership'
| 'aiFeaturesEnabled'
>;
@@ -94,6 +105,7 @@ export const DocumentPreferencesForm = ({
const { t } = useLingui();
const { user, organisations } = useSession();
const currentOrganisation = useCurrentOrganisation();
const optionalTeam = useOptionalCurrentTeam();
const isPersonalLayoutMode = isPersonalLayout(organisations);
const isPersonalOrganisation = currentOrganisation.type === OrganisationType.PERSONAL;
@@ -111,6 +123,7 @@ export const DocumentPreferencesForm = ({
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
message: msg`At least one signature type must be enabled`.id,
}),
defaultRecipients: ZDefaultRecipientsSchema.nullable(),
delegateDocumentOwnership: z.boolean().nullable(),
aiFeaturesEnabled: z.boolean().nullable(),
});
@@ -128,6 +141,9 @@ export const DocumentPreferencesForm = ({
includeSigningCertificate: settings.includeSigningCertificate,
includeAuditLog: settings.includeAuditLog,
signatureTypes: extractTeamSignatureSettings({ ...settings }),
defaultRecipients: settings.defaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: null,
delegateDocumentOwnership: settings.delegateDocumentOwnership,
aiFeaturesEnabled: settings.aiFeaturesEnabled,
},
@@ -519,6 +535,94 @@ export const DocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="defaultRecipients"
render={({ field }) => {
const recipients = field.value ?? [];
return (
<FormItem className="flex-1">
<FormLabel>
<Trans>Default Recipients</Trans>
</FormLabel>
{canInherit && (
<Select
value={field.value === null ? '-1' : '0'}
onValueChange={(value) => field.onChange(value === '-1' ? null : [])}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={'-1'}>
<Trans>Inherit from organisation</Trans>
</SelectItem>
<SelectItem value={'0'}>
<Trans>Override organisation settings</Trans>
</SelectItem>
</SelectContent>
</Select>
)}
{(field.value !== null || !canInherit) && (
<div className="space-y-4">
<DefaultRecipientsMultiSelectCombobox
listValues={recipients}
onChange={field.onChange}
organisationId={!canInherit ? currentOrganisation.id : undefined}
teamId={canInherit ? optionalTeam?.id : undefined}
/>
{recipients.map((recipient, index) => {
return (
<div
key={recipient.email}
className="flex items-center justify-between gap-3 rounded-lg border p-3"
>
<AvatarWithText
avatarFallback={recipientAbbreviation(recipient)}
primaryText={
<span className="text-sm font-medium">
{recipient.name || recipient.email}
</span>
}
secondaryText={
recipient.name ? (
<span className="text-xs text-muted-foreground">
{recipient.email}
</span>
) : undefined
}
className="flex-1"
/>
<div className="flex items-center gap-2">
<RecipientRoleSelect
value={recipient.role}
onValueChange={(role: RecipientRole) => {
field.onChange(
recipients.map((recipient, idx) =>
idx === index ? { ...recipient, role } : recipient,
),
);
}}
/>
</div>
</div>
);
})}
</div>
)}
<FormDescription>
<Trans>Recipients that will be automatically added to new documents.</Trans>
</FormDescription>
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="delegateDocumentOwnership"
+1 -1
View File
@@ -171,7 +171,7 @@ export const SignInForm = ({
const { options, sessionId } = await createPasskeySigninOptions();
const credential = await startAuthentication(options);
const credential = await startAuthentication({ optionsJSON: options });
await authClient.passkey.signIn({
credential: JSON.stringify(credential),
@@ -0,0 +1,90 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { RecipientRole } from '@prisma/client';
import type { TDefaultRecipient } from '@documenso/lib/types/default-recipients';
import { trpc } from '@documenso/trpc/react';
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
type DefaultRecipientsMultiSelectComboboxProps = {
listValues: TDefaultRecipient[];
onChange: (_values: TDefaultRecipient[]) => void;
teamId?: number;
organisationId?: string;
};
export const DefaultRecipientsMultiSelectCombobox = ({
listValues,
onChange,
teamId,
organisationId,
}: DefaultRecipientsMultiSelectComboboxProps) => {
const { _ } = useLingui();
const { data: organisationData, isLoading: isLoadingOrganisation } =
trpc.organisation.member.find.useQuery(
{
organisationId: organisationId!,
query: '',
page: 1,
perPage: 100,
},
{
enabled: !!organisationId,
},
);
const { data: teamData, isLoading: isLoadingTeam } = trpc.team.member.find.useQuery(
{
teamId: teamId!,
query: '',
page: 1,
perPage: 100,
},
{
enabled: !!teamId,
},
);
const members = organisationId ? organisationData?.data : teamData?.data;
const isLoading = organisationId ? isLoadingOrganisation : isLoadingTeam;
const options = members?.map((member) => ({
value: member.email,
label: member.name ? `${member.name} (${member.email})` : member.email,
}));
const value = listValues.map((recipient) => ({
value: recipient.email,
label: recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email,
}));
const onSelectionChange = (selected: Option[]) => {
const updatedRecipients = selected.map((option) => {
const existingRecipient = listValues.find((r) => r.email === option.value);
const member = members?.find((m) => m.email === option.value);
return {
email: option.value,
name: member?.name || option.value,
role: existingRecipient?.role ?? RecipientRole.CC,
};
});
onChange(updatedRecipients);
};
return (
<MultiSelect
commandProps={{ label: _(msg`Select recipients`) }}
options={options}
value={value}
onChange={onSelectionChange}
placeholder={_(msg`Select recipients`)}
hideClearAllButton
hidePlaceholderWhenSelected
loadingIndicator={isLoading ? <p className="text-center text-sm">Loading...</p> : undefined}
emptyIndicator={<p className="text-center text-sm">No members found</p>}
/>
);
};
@@ -90,7 +90,7 @@ export const DocumentSigningAuthPasskey = ({
preferredPasskeyId: passkeyId,
});
const authenticationResponse = await startAuthentication(options);
const authenticationResponse = await startAuthentication({ optionsJSON: options });
await onReauthFormSubmit({
type: DocumentAuth.PASSKEY,
@@ -146,7 +146,7 @@ export const DocumentSigningAuthPasskey = ({
if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) {
return (
<div className="flex h-28 items-center justify-center">
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
<Loader className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
@@ -341,7 +341,9 @@ export const EnvelopeEditorSettingsDialog = ({
{/* Sidebar. */}
<div className="flex w-80 flex-col border-r bg-accent/20">
<DialogHeader className="p-6 pb-4">
<DialogTitle>Document Settings</DialogTitle>
<DialogTitle>
<Trans>Document Settings</Trans>
</DialogTitle>
</DialogHeader>
<nav className="col-span-12 mb-8 flex flex-wrap items-center justify-start gap-x-2 gap-y-4 px-4 md:col-span-3 md:w-full md:flex-col md:items-start md:gap-y-2">
@@ -306,7 +306,7 @@ export const EnvelopeEditorUploadPage = () => {
ref={provided.innerRef}
{...provided.draggableProps}
style={provided.draggableProps.style}
className={`bg-accent/50 flex items-center justify-between rounded-lg p-3 transition-shadow ${
className={`flex items-center justify-between rounded-lg bg-accent/50 p-3 transition-shadow ${
snapshot.isDragging ? 'shadow-md' : ''
}`}
>
@@ -332,7 +332,7 @@ export const EnvelopeEditorUploadPage = () => {
<p className="text-sm font-medium">{localFile.title}</p>
)}
<div className="text-muted-foreground text-xs">
<div className="text-xs text-muted-foreground">
{localFile.isUploading ? (
<Trans>Uploading</Trans>
) : localFile.isError ? (
@@ -345,13 +345,13 @@ export const EnvelopeEditorUploadPage = () => {
<div className="flex items-center space-x-2">
{localFile.isUploading && (
<div className="flex h-6 w-10 items-center justify-center">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{localFile.isError && (
<div className="flex h-6 w-10 items-center justify-center">
<FileWarningIcon className="text-destructive h-4 w-4" />
<FileWarningIcon className="h-4 w-4 text-destructive" />
</div>
)}
@@ -21,17 +21,17 @@ export default function DocumentEditSkeleton() {
</div>
<div className="mt-4 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
<div className="col-span-12 rounded-xl border-2 border-border bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7 dark:bg-background">
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
<Loader className="text-documenso h-12 w-12 animate-spin" />
<Loader className="h-12 w-12 animate-spin text-documenso" />
<p className="text-muted-foreground mt-4">
<p className="mt-4 text-muted-foreground">
<Trans>Loading document...</Trans>
</p>
</div>
</div>
<div className="bg-background border-border col-span-12 rounded-xl border-2 before:rounded-xl lg:col-span-6 xl:col-span-5" />
<div className="col-span-12 rounded-xl border-2 border-border bg-background before:rounded-xl lg:col-span-6 xl:col-span-5" />
</div>
</div>
);
@@ -0,0 +1,210 @@
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminDocumentLogsTableProps = {
envelopeId: string;
};
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
hourCycle: 'h12',
};
export const AdminDocumentLogsTable = ({ envelopeId }: AdminDocumentLogsTableProps) => {
const { _, i18n } = useLingui();
const { toast } = useToast();
const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const [selectedAuditLog, setSelectedAuditLog] = useState<TDocumentAuditLog | null>(null);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isLoadingError } = trpc.admin.document.findAuditLogs.useQuery(
{
envelopeId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
placeholderData: (previousData) => previousData,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
page,
perPage,
});
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
const columns = useMemo(() => {
const parser = new UAParser();
return [
{
header: _(msg`Time`),
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
},
{
header: _(msg`User`),
accessorKey: 'name',
cell: ({ row }) =>
row.original.name || row.original.email ? (
<div>
{row.original.name && (
<p className="truncate" title={row.original.name}>
{row.original.name}
</p>
)}
{row.original.email && (
<p className="truncate" title={row.original.email}>
{row.original.email}
</p>
)}
</div>
) : (
<p>N/A</p>
),
},
{
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => (
<span>{formatDocumentAuditLogAction(i18n, row.original).description}</span>
),
},
{
header: _(msg`IP Address`),
accessorKey: 'ipAddress',
},
{
header: _(msg`Browser`),
cell: ({ row }) => {
if (!row.original.userAgent) {
return 'N/A';
}
parser.setUA(row.original.userAgent);
const result = parser.getResult();
return result.browser.name ?? 'N/A';
},
},
{
header: '',
id: 'actions',
cell: ({ row }) => (
<Button variant="link" size="sm" onClick={() => setSelectedAuditLog(row.original)}>
<Trans>View JSON</Trans>
</Button>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<>
<DataTable
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
error={{
enable: isLoadingError,
}}
skeleton={{
enable: isLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell className="w-1/2 py-4 pr-4">
<div className="ml-2 flex flex-grow flex-col">
<Skeleton className="h-4 w-1/3 max-w-[8rem]" />
<Skeleton className="mt-1 h-4 w-1/2 max-w-[12rem]" />
</div>
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-10 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-10 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-8 rounded-full" />
</TableCell>
</>
),
}}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
<Dialog open={selectedAuditLog !== null} onOpenChange={() => setSelectedAuditLog(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
<Trans>Audit Log Details</Trans>
</DialogTitle>
</DialogHeader>
{selectedAuditLog && (
<div className="group relative">
<div className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100">
<CopyTextButton
value={JSON.stringify(selectedAuditLog, null, 2)}
onCopySuccess={() => toast({ title: _(msg`Copied to clipboard`) })}
/>
</div>
<pre className="max-h-[60vh] overflow-auto whitespace-pre-wrap break-all rounded-lg border border-border bg-muted/50 p-4 font-mono text-xs leading-relaxed text-foreground">
{JSON.stringify(selectedAuditLog, null, 2)}
</pre>
</div>
)}
</DialogContent>
</Dialog>
</>
);
};
@@ -6,6 +6,7 @@ import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { trpc } from '@documenso/trpc/react';
import {
Accordion,
@@ -26,6 +27,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { AdminDocumentDeleteDialog } from '~/components/dialogs/admin-document-delete-dialog';
import { DocumentStatus } from '~/components/general/document/document-status';
import { AdminDocumentJobsTable } from '~/components/tables/admin-document-jobs-table';
import { AdminDocumentLogsTable } from '~/components/tables/admin-document-logs-table';
import { AdminDocumentRecipientItemTable } from '~/components/tables/admin-document-recipient-item-table';
import type { Route } from './+types/documents.$id';
@@ -87,6 +89,10 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
</div>
<div className="mt-4 text-sm text-muted-foreground">
<div>
<Trans>Document ID</Trans>: {mapSecondaryIdToDocumentId(envelope.secondaryId)}
</div>
<div>
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
</div>
@@ -156,6 +162,9 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<Badge size="small" variant="neutral">
{recipient.email}
</Badge>
<Badge size="small" variant="secondary">
{recipient.role}
</Badge>
</div>
</AccordionTrigger>
@@ -175,6 +184,22 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<hr className="my-4" />
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="audit-logs" className="rounded-lg border">
<AccordionTrigger className="px-4">
<h2 className="text-lg font-semibold">
<Trans>Audit Logs</Trans>
</h2>
</AccordionTrigger>
<AccordionContent className="border-t px-4 pt-4">
<AdminDocumentLogsTable envelopeId={envelope.id} />
</AccordionContent>
</AccordionItem>
</Accordion>
<hr className="my-4" />
{envelope && <AdminDocumentDeleteDialog envelopeId={envelope.id} />}
</div>
);
@@ -57,6 +57,7 @@ export default function OrganisationSettingsDocumentPage() {
includeSigningCertificate,
includeAuditLog,
signatureTypes,
defaultRecipients,
delegateDocumentOwnership,
aiFeaturesEnabled,
} = data;
@@ -83,6 +84,7 @@ export default function OrganisationSettingsDocumentPage() {
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
defaultRecipients,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
@@ -106,7 +106,7 @@ export default function DocumentEditPage() {
/>
{recipients.length > 0 && (
<div className="text-muted-foreground flex items-center">
<div className="flex items-center text-muted-foreground">
<Users2 className="mr-2 h-5 w-5" />
<StackAvatarsWithTooltip
@@ -50,6 +50,7 @@ export default function TeamsSettingsPage() {
includeSigningCertificate,
includeAuditLog,
signatureTypes,
defaultRecipients,
delegateDocumentOwnership,
aiFeaturesEnabled,
} = data;
@@ -64,6 +65,7 @@ export default function TeamsSettingsPage() {
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
defaultRecipients,
aiFeaturesEnabled,
...(signatureTypes.length === 0
? {
+8 -9
View File
@@ -36,16 +36,16 @@
"@lingui/react": "^5.6.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@react-router/node": "^7.9.6",
"@react-router/serve": "^7.9.6",
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3",
"@react-router/node": "^7.12.0",
"@react-router/serve": "^7.12.0",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"autoprefixer": "^10.4.22",
"colord": "^2.9.3",
"content-disposition": "^1.0.1",
"framer-motion": "^12.23.24",
"hono": "4.10.6",
"hono": "4.11.4",
"hono-rate-limiter": "^0.4.2",
"hono-react-router-adapter": "^0.6.5",
"input-otp": "^1.4.2",
@@ -65,7 +65,7 @@
"react-hotkeys-hook": "^4.6.2",
"react-icons": "^5.5.0",
"react-rnd": "^10.5.2",
"react-router": "^7.9.6",
"react-router": "^7.12.0",
"recharts": "^2.15.4",
"remeda": "^2.32.0",
"remix-themes": "^2.0.4",
@@ -81,14 +81,13 @@
"@babel/preset-typescript": "^7.28.5",
"@lingui/babel-plugin-lingui-macro": "^5.6.0",
"@lingui/vite-plugin": "^5.6.0",
"@react-router/dev": "^7.9.6",
"@react-router/remix-routes-option-adapter": "^7.9.6",
"@react-router/dev": "^7.12.0",
"@react-router/remix-routes-option-adapter": "^7.12.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^3.4.6",
"@types/luxon": "^3.7.1",
Binary file not shown.
+296 -456
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -64,7 +64,7 @@
"lint-staged": "^16.2.7",
"nanoid": "^5.1.6",
"nodemailer": "^7.0.10",
"pdfjs-dist": "5.4.449",
"pdfjs-dist": "5.4.296",
"pino": "^9.14.0",
"pino-pretty": "^13.1.2",
"playwright": "1.56.1",
@@ -99,8 +99,8 @@
"zod": "^3.25.76"
},
"overrides": {
"pdfjs-dist": "5.4.449",
"pdfjs-dist": "5.4.296",
"typescript": "5.6.2",
"zod": "^3.25.76"
}
}
}
+37 -4
View File
@@ -55,6 +55,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Get all documents',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
getDocument: {
@@ -66,6 +68,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Get a single document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
downloadSignedDocument: {
@@ -78,6 +82,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Download a signed document when the storage transport is S3',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
createDocument: {
@@ -90,6 +96,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Upload a new document and get a presigned URL',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
createTemplate: {
@@ -102,6 +110,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Create a new template and get a presigned URL',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
deleteTemplate: {
@@ -114,6 +124,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a template',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
getTemplate: {
@@ -125,6 +137,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Get a single template',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
getTemplates: {
@@ -137,6 +151,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Get all templates',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
createDocumentFromTemplate: {
@@ -150,7 +166,7 @@ export const ApiContractV1 = c.router(
},
summary: 'Create a new document from an existing template',
deprecated: true,
description: `This has been deprecated in favour of "/api/v1/templates/:templateId/generate-document". You may face unpredictable behavior using this endpoint as it is no longer maintained.`,
description: 'Deprecated. Please migrate to the v2 API.',
},
generateDocumentFromTemplate: {
@@ -165,8 +181,9 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Create a new document from an existing template',
deprecated: true,
description:
'Create a new document from an existing template. Passing in values for title and meta will override the original values defined in the template. If you do not pass in values for recipients, it will use the values defined in the template.',
'Deprecated. Please migrate to the v2 API.\n\nCreate a new document from an existing template. Passing in values for title and meta will override the original values defined in the template. If you do not pass in values for recipients, it will use the values defined in the template.',
},
sendDocument: {
@@ -181,9 +198,9 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Send a document for signing',
// I'm aware this should be in the variable itself, which it is, however it's difficult for users to find in our current UI.
deprecated: true,
description:
'Notes\n\n`sendEmail` - Whether to send an email to the recipients asking them to action the document. If you disable this, you will need to manually distribute the document to the recipients using the generated signing links. Defaults to true',
'Deprecated. Please migrate to the v2 API.\n\nNotes\n\n`sendEmail` - Whether to send an email to the recipients asking them to action the document. If you disable this, you will need to manually distribute the document to the recipients using the generated signing links. Defaults to true',
},
resendDocument: {
@@ -198,6 +215,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Re-send a document for signing',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
deleteDocument: {
@@ -210,6 +229,8 @@ export const ApiContractV1 = c.router(
404: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
createRecipient: {
@@ -224,6 +245,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Create a recipient for a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
updateRecipient: {
@@ -238,6 +261,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Update a recipient for a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
deleteRecipient: {
@@ -252,6 +277,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a recipient from a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
createField: {
@@ -266,6 +293,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Create a field for a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
updateField: {
@@ -280,6 +309,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Update a field for a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
deleteField: {
@@ -294,6 +325,8 @@ export const ApiContractV1 = c.router(
500: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a field from a document',
deprecated: true,
description: 'Deprecated. Please migrate to the v2 API.',
},
},
{
+3 -51
View File
@@ -41,7 +41,7 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import {
getPresignGetUrl,
getPresignPostUrl,
@@ -822,7 +822,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
@@ -911,61 +911,13 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
title: body.title,
...body.meta,
},
formValues: body.formValues,
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (envelope.envelopeItems.length !== 1) {
throw new Error('API V1 does not support envelopes');
}
const firstEnvelopeDocumentData = await prisma.envelopeItem.findFirstOrThrow({
where: {
envelopeId: envelope.id,
},
include: {
documentData: true,
},
});
if (body.formValues) {
const fileName = envelope.title.endsWith('.pdf') ? envelope.title : `${envelope.title}.pdf`;
const pdf = await getFileServerSide(firstEnvelopeDocumentData.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
await prisma.envelope.update({
where: {
id: envelope.id,
},
data: {
formValues: body.formValues,
envelopeItems: {
update: {
where: {
id: firstEnvelopeDocumentData.id,
},
data: {
documentDataId: newDocumentData.id,
},
},
},
},
});
}
if (body.authOptions) {
await prisma.envelope.update({
where: {
+2 -1
View File
@@ -11,7 +11,8 @@ export const OpenAPIV1 = Object.assign(
info: {
title: 'Documenso API',
version: '1.0.0',
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
description:
'[DEPRECATED] The Documenso API for retrieving, creating, updating and deleting documents. Please migrate to the v2 API.',
},
servers: [
{
@@ -0,0 +1,848 @@
import { PDFDocument } from '@cantoo/pdf-lib';
import { expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, RecipientRole } from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
// Form field names in the test PDF
const FORM_FIELDS = {
TEXT_FIELD: 'test_text_field',
COMPANY_NAME: 'company_name',
CHECKBOX: 'accept_terms',
DROPDOWN: 'country',
} as const;
// Test values to insert into form fields
const TEST_FORM_VALUES = {
[FORM_FIELDS.TEXT_FIELD]: 'Hello World',
[FORM_FIELDS.COMPANY_NAME]: 'Documenso Inc.',
[FORM_FIELDS.CHECKBOX]: true,
[FORM_FIELDS.DROPDOWN]: 'Germany',
};
/**
* Helper to check if a PDF has interactive form fields.
* Returns true if the PDF has form fields, false if they've been flattened.
*/
async function pdfHasFormFields(pdfBuffer: Uint8Array): Promise<boolean> {
const pdfDoc = await PDFDocument.load(pdfBuffer);
const form = pdfDoc.getForm();
const fields = form.getFields();
return fields.length > 0;
}
/**
* Helper to get form field names from a PDF.
*/
async function getPdfFormFieldNames(pdfBuffer: Uint8Array): Promise<string[]> {
const pdfDoc = await PDFDocument.load(pdfBuffer);
const form = pdfDoc.getForm();
const fields = form.getFields();
return fields.map((field) => field.getName());
}
/**
* Helper to get the value of a text field in a PDF.
*/
async function getPdfTextFieldValue(
pdfBuffer: Uint8Array,
fieldName: string,
): Promise<string | undefined> {
const pdfDoc = await PDFDocument.load(pdfBuffer);
const form = pdfDoc.getForm();
try {
const textField = form.getTextField(fieldName);
return textField.getText() ?? '';
} catch {
return undefined;
}
}
test.describe.configure({
mode: 'parallel',
});
test.describe('Form Flattening', () => {
const formFieldsPdf = fs.readFileSync(
path.join(__dirname, '../../../../assets/form-fields-test.pdf'),
);
test.describe('Envelope Creation (DOCUMENT type)', () => {
test('should flatten form fields when creating a DOCUMENT envelope with formValues', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Document with Form Values',
formValues: TEST_FORM_VALUES,
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
// Verify the envelope was created with the correct formValues
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
expect(envelope.formValues).toEqual(TEST_FORM_VALUES);
expect(envelope.type).toBe(EnvelopeType.DOCUMENT);
// Get the PDF and verify form fields are flattened
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
const hasFormFields = await pdfHasFormFields(pdfBuffer);
expect(hasFormFields).toBe(false);
});
test('should flatten form fields when creating a DOCUMENT envelope without formValues', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Document without Form Values',
// No formValues - but form should still be flattened for DOCUMENT type
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
// Get the PDF and verify form fields are flattened
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
const hasFormFields = await pdfHasFormFields(pdfBuffer);
expect(hasFormFields).toBe(false);
});
});
test.describe('Template Creation (TEMPLATE type)', () => {
test('should NOT flatten form fields when creating a TEMPLATE envelope', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template with Form Fields',
// Note: formValues can be set but form should NOT be flattened for templates
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
expect(envelope.type).toBe(EnvelopeType.TEMPLATE);
// Get the PDF and verify form fields are NOT flattened
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
const hasFormFields = await pdfHasFormFields(pdfBuffer);
expect(hasFormFields).toBe(true);
// Verify the specific form fields still exist
const fieldNames = await getPdfFormFieldNames(pdfBuffer);
expect(fieldNames).toContain(FORM_FIELDS.TEXT_FIELD);
expect(fieldNames).toContain(FORM_FIELDS.COMPANY_NAME);
expect(fieldNames).toContain(FORM_FIELDS.CHECKBOX);
expect(fieldNames).toContain(FORM_FIELDS.DROPDOWN);
});
test('should preserve form fields in template even when formValues are provided', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template with Form Values',
formValues: TEST_FORM_VALUES,
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
// formValues should be stored in the database
expect(envelope.formValues).toEqual(TEST_FORM_VALUES);
expect(envelope.type).toBe(EnvelopeType.TEMPLATE);
// But the PDF should still have interactive form fields
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
const hasFormFields = await pdfHasFormFields(pdfBuffer);
expect(hasFormFields).toBe(true);
expect(await getPdfTextFieldValue(pdfBuffer, FORM_FIELDS.TEXT_FIELD)).toBe(
TEST_FORM_VALUES[FORM_FIELDS.TEXT_FIELD],
);
});
});
test.describe('Document from Template', () => {
test('should flatten form fields when creating document from template with formValues', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
// First, create a template via API
const templatePayload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template for Document Creation',
recipients: [
{
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
},
],
};
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: templateFormData,
});
expect(templateRes.ok()).toBeTruthy();
const templateResponse = (await templateRes.json()) as TCreateEnvelopeResponse;
// Verify template has form fields
const template = await prisma.envelope.findUniqueOrThrow({
where: { id: templateResponse.id },
include: {
envelopeItems: { include: { documentData: true } },
recipients: true,
},
});
const templatePdfBuffer = await getFileServerSide(template.envelopeItems[0].documentData);
expect(await pdfHasFormFields(templatePdfBuffer)).toBe(true);
// Now create a document from the template with formValues
const useTemplateRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
recipients: [
{
id: template.recipients[0].id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
formValues: TEST_FORM_VALUES,
},
});
expect(useTemplateRes.ok()).toBeTruthy();
expect(useTemplateRes.status()).toBe(200);
const documentResponse = await useTemplateRes.json();
// Get the created document
const document = await prisma.envelope.findFirstOrThrow({
where: {
id: documentResponse.envelopeId,
},
include: {
envelopeItems: { include: { documentData: true } },
},
});
expect(document.type).toBe(EnvelopeType.DOCUMENT);
// Verify form fields are flattened in the created document
const documentPdfBuffer = await getFileServerSide(document.envelopeItems[0].documentData);
const hasFormFields = await pdfHasFormFields(documentPdfBuffer);
expect(hasFormFields).toBe(false);
});
test('should flatten form fields when creating document from template without formValues', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
// Create a template
const templatePayload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template without Form Values',
recipients: [
{
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
},
],
};
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: templateFormData,
});
expect(templateRes.ok()).toBeTruthy();
const templateResponse = (await templateRes.json()) as TCreateEnvelopeResponse;
const template = await prisma.envelope.findUniqueOrThrow({
where: { id: templateResponse.id },
include: {
envelopeItems: { include: { documentData: true } },
recipients: true,
},
});
// Create document from template WITHOUT formValues
const useTemplateRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
recipients: [
{
id: template.recipients[0].id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
// No formValues provided
},
});
expect(useTemplateRes.ok()).toBeTruthy();
const documentResponse = await useTemplateRes.json();
const document = await prisma.envelope.findFirstOrThrow({
where: { id: documentResponse.envelopeId },
include: {
envelopeItems: { include: { documentData: true } },
},
});
expect(document.type).toBe(EnvelopeType.DOCUMENT);
// Form fields should still be flattened even without formValues
const documentPdfBuffer = await getFileServerSide(document.envelopeItems[0].documentData);
const hasFormFields = await pdfHasFormFields(documentPdfBuffer);
expect(hasFormFields).toBe(false);
});
test('should use template formValues when creating document without override', async ({
request,
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
// Create a template with formValues
const templatePayload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template with Default Form Values',
formValues: {
[FORM_FIELDS.TEXT_FIELD]: 'Default Value',
[FORM_FIELDS.COMPANY_NAME]: 'Default Company',
},
recipients: [
{
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
},
],
};
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: templateFormData,
});
expect(templateRes.ok()).toBeTruthy();
const templateResponse = (await templateRes.json()) as TCreateEnvelopeResponse;
const template = await prisma.envelope.findUniqueOrThrow({
where: { id: templateResponse.id },
include: {
envelopeItems: { include: { documentData: true } },
recipients: true,
},
});
// Verify template stored the formValues
expect(template.formValues).toEqual({
[FORM_FIELDS.TEXT_FIELD]: 'Default Value',
[FORM_FIELDS.COMPANY_NAME]: 'Default Company',
});
// Create document from template without providing new formValues
// The template's formValues should be used
const useTemplateRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
recipients: [
{
id: template.recipients[0].id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
// No formValues - should inherit from template
},
});
expect(useTemplateRes.ok()).toBeTruthy();
const documentResponse = await useTemplateRes.json();
const document = await prisma.envelope.findFirstOrThrow({
where: { id: documentResponse.envelopeId },
include: {
envelopeItems: { include: { documentData: true } },
},
});
// Form fields should be flattened
const documentPdfBuffer = await getFileServerSide(document.envelopeItems[0].documentData);
expect(await pdfHasFormFields(documentPdfBuffer)).toBe(false);
});
});
test.describe('Form Values Verification', () => {
test('should correctly insert form values into PDF before flattening', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
// Create a template first (form fields preserved)
const templatePayload: TCreateEnvelopePayload = {
type: EnvelopeType.TEMPLATE,
title: 'Template for Value Verification',
recipients: [
{
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
},
],
};
const templateFormData = new FormData();
templateFormData.append('payload', JSON.stringify(templatePayload));
templateFormData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const templateRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: templateFormData,
});
const templateResponse = (await templateRes.json()) as TCreateEnvelopeResponse;
const template = await prisma.envelope.findUniqueOrThrow({
where: { id: templateResponse.id },
include: {
envelopeItems: { include: { documentData: true } },
recipients: true,
},
});
// Verify template PDF still has form fields
const templatePdfBuffer = await getFileServerSide(template.envelopeItems[0].documentData);
expect(await pdfHasFormFields(templatePdfBuffer)).toBe(true);
// Verify we can read a text field value (should be empty initially)
const initialValue = await getPdfTextFieldValue(templatePdfBuffer, FORM_FIELDS.TEXT_FIELD);
expect(initialValue).toBe('');
// Now create a document with form values
const testValues = {
[FORM_FIELDS.TEXT_FIELD]: 'Inserted Text Value',
[FORM_FIELDS.COMPANY_NAME]: 'Test Company Name',
};
const useTemplateRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
recipients: [
{
id: template.recipients[0].id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
formValues: testValues,
},
});
expect(useTemplateRes.ok()).toBeTruthy();
const documentResponse = await useTemplateRes.json();
const document = await prisma.envelope.findFirstOrThrow({
where: { id: documentResponse.envelopeId },
include: {
envelopeItems: { include: { documentData: true } },
},
});
// The form should be flattened, so we can't read form fields
const documentPdfBuffer = await getFileServerSide(document.envelopeItems[0].documentData);
expect(await pdfHasFormFields(documentPdfBuffer)).toBe(false);
// The values should have been inserted before flattening
// We can't verify the actual text content easily without visual inspection,
// but we can verify the form fields are gone (flattened)
const fieldNames = await getPdfFormFieldNames(documentPdfBuffer);
expect(fieldNames.length).toBe(0);
});
});
test.describe('Edge Cases', () => {
test('should handle PDF without form fields gracefully', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
// Use a PDF without form fields
const examplePdf = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Document with No Form Fields',
formValues: {
nonexistent_field: 'Some Value',
},
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('files', new File([examplePdf], 'example.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
// Should succeed even with formValues for non-existent fields
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: { include: { documentData: true } },
},
});
expect(envelope.formValues).toEqual({ nonexistent_field: 'Some Value' });
});
test('should handle empty formValues object', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Document with Empty Form Values',
formValues: {},
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: { include: { documentData: true } },
},
});
// Form should still be flattened for DOCUMENT type
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
expect(await pdfHasFormFields(pdfBuffer)).toBe(false);
});
test('should handle partial formValues (only some fields)', async ({ request }) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Document with Partial Form Values',
formValues: {
[FORM_FIELDS.TEXT_FIELD]: 'Only this field',
// Other fields not set
},
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append(
'files',
new File([formFieldsPdf], 'form-fields-test.pdf', { type: 'application/pdf' }),
);
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: response.id },
include: {
envelopeItems: { include: { documentData: true } },
},
});
// Should store the partial formValues
expect(envelope.formValues).toEqual({
[FORM_FIELDS.TEXT_FIELD]: 'Only this field',
});
// Form should still be flattened
const documentData = envelope.envelopeItems[0].documentData;
const pdfBuffer = await getFileServerSide(documentData);
expect(await pdfHasFormFields(pdfBuffer)).toBe(false);
});
});
});
@@ -0,0 +1,427 @@
import { expect, test } from '@playwright/test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, RecipientRole } from '@documenso/prisma/client';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { apiSignin } from '../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
test.describe.configure({ mode: 'parallel' });
/**
* Helper function to set default recipients for a team
*/
const setTeamDefaultRecipients = async (
teamId: number,
defaultRecipients: Array<{ email: string; name: string; role: RecipientRole }>,
) => {
const teamSettings = await prisma.teamGlobalSettings.findFirstOrThrow({
where: {
team: {
id: teamId,
},
},
});
await prisma.teamGlobalSettings.update({
where: {
id: teamSettings.id,
},
data: {
defaultRecipients,
},
});
};
test.describe('Default Recipients', () => {
test('[DEFAULT_RECIPIENTS]: default recipients are added to documents created via UI', async ({
page,
}) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
// Get a team member to set as default recipient
const teamMembers = await prisma.organisationMember.findMany({
where: {
organisationId: team.organisationId,
userId: {
not: owner.id,
},
},
include: {
user: true,
},
});
const defaultRecipientUser = teamMembers[0].user;
// Set up default recipients for the team
await setTeamDefaultRecipients(team.id, [
{
email: defaultRecipientUser.email,
name: defaultRecipientUser.name || defaultRecipientUser.email,
role: RecipientRole.CC,
},
]);
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/documents`,
});
// Upload document via UI - this triggers document creation with default recipients
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page
.locator('input[type=file]')
.nth(1)
.evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
]);
await fileChooser.setFiles(path.join(__dirname, '../../../../assets/example.pdf'));
// Wait to be redirected to the edit page (v2 envelope editor)
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
// Extract document ID from URL
const urlParts = page.url().split('/');
const documentId = urlParts.find((part) => part.startsWith('envelope_'));
// Wait for the Recipients card to be visible (v2 envelope editor)
await expect(page.getByRole('heading', { name: 'Recipients' })).toBeVisible();
await expect(page.getByTestId('signer-email-input').first()).not.toBeEmpty();
await page.getByRole('button', { name: 'Add Signer' }).click();
// Add a regular signer using the v2 editor
await page.getByTestId('signer-email-input').last().fill('regular-signer@documenso.com');
await page
.getByPlaceholder(/Recipient/)
.first()
.fill('Regular Signer');
// Wait for autosave to complete
await page.waitForTimeout(3000);
// Verify that default recipient is present in the database
await expect(async () => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
id: documentId,
},
include: {
recipients: true,
},
});
// Should have 2 recipients: the regular signer + the default recipient
expect(envelope.recipients.length).toBe(2);
const defaultRecipient = envelope.recipients.find(
(r) => r.email.toLowerCase() === defaultRecipientUser.email.toLowerCase(),
);
expect(defaultRecipient).toBeDefined();
expect(defaultRecipient?.role).toBe(RecipientRole.CC);
const regularSigner = envelope.recipients.find(
(r) => r.email === 'regular-signer@documenso.com',
);
expect(regularSigner).toBeDefined();
}).toPass();
});
// TODO: Are we intending to allow default recipients to be removed from a document?
test.skip('[DEFAULT_RECIPIENTS]: default recipients cannot be removed from a document', async ({
page,
}) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
// Get a team member to set as default recipient
const teamMembers = await prisma.organisationMember.findMany({
where: {
organisationId: team.organisationId,
userId: {
not: owner.id,
},
},
include: {
user: true,
},
});
const defaultRecipientUser = teamMembers[0].user;
// Set up default recipients for the team
await setTeamDefaultRecipients(team.id, [
{
email: defaultRecipientUser.email,
name: defaultRecipientUser.name || defaultRecipientUser.email,
role: RecipientRole.CC,
},
]);
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/documents`,
});
// Upload document via UI - this triggers document creation with default recipients
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page
.locator('input[type=file]')
.nth(1)
.evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
]);
await fileChooser.setFiles(path.join(__dirname, '../../../../assets/example.pdf'));
// Wait to be redirected to the edit page (v2 envelope editor)
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
// Extract document ID from URL
const urlParts = page.url().split('/');
const documentId = urlParts.find((part) => part.startsWith('envelope_'));
// Replace the default recipient with a regular signer
await page.getByTestId('signer-email-input').first().fill('regular-signer@documenso.com');
await page
.getByPlaceholder(/Recipient/)
.first()
.fill('Regular Signer');
// Wait for autosave to complete
await page.waitForTimeout(3000);
// Wait for recipients to be saved
await expect(async () => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
id: documentId,
},
include: {
recipients: true,
},
});
expect(envelope.recipients.length).toBe(2);
}).toPass();
// Verify that the default recipient's remove button is disabled
// In the v2 editor, default recipients should have a disabled remove button
// Find the fieldset containing the default recipient's email and check if its remove button is disabled
const defaultRecipientRow = page.locator('fieldset').filter({
hasText: defaultRecipientUser.email,
});
// The default recipient row should exist and have a disabled remove button
await expect(defaultRecipientRow).toHaveCount(1);
const removeButton = defaultRecipientRow.getByTestId('remove-signer-button');
await expect(removeButton).toBeDisabled();
});
test('[DEFAULT_RECIPIENTS]: documents created via API have default recipients', async ({
request,
}) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
// Get a team member to set as default recipient
const teamMembers = await prisma.organisationMember.findMany({
where: {
organisationId: team.organisationId,
userId: {
not: owner.id,
},
},
include: {
user: true,
},
});
const defaultRecipientUser = teamMembers[0].user;
// Set up default recipients for the team
await setTeamDefaultRecipients(team.id, [
{
email: defaultRecipientUser.email,
name: defaultRecipientUser.name || defaultRecipientUser.email,
role: RecipientRole.CC,
},
]);
// Create API token
const { token } = await createApiToken({
userId: owner.id,
teamId: team.id,
tokenName: 'test-token',
expiresIn: null,
});
// Create envelope via API
const payload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Test Document with Default Recipients',
recipients: [
{
email: 'api-recipient@documenso.com',
name: 'API Recipient',
role: RecipientRole.SIGNER,
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const pdfData = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
// Verify the envelope has both the API recipient and the default recipient
const envelope = await prisma.envelope.findUniqueOrThrow({
where: {
id: response.id,
},
include: {
recipients: true,
},
});
expect(envelope.recipients.length).toBe(2);
const apiRecipient = envelope.recipients.find((r) => r.email === 'api-recipient@documenso.com');
expect(apiRecipient).toBeDefined();
expect(apiRecipient?.role).toBe(RecipientRole.SIGNER);
const defaultRecipient = envelope.recipients.find(
(r) => r.email.toLowerCase() === defaultRecipientUser.email.toLowerCase(),
);
expect(defaultRecipient).toBeDefined();
expect(defaultRecipient?.role).toBe(RecipientRole.CC);
});
test('[DEFAULT_RECIPIENTS]: documents created from template have default recipients', async ({
page,
}) => {
const { team, owner } = await seedTeam({
createTeamMembers: 2,
});
// Get a team member to set as default recipient
const teamMembers = await prisma.organisationMember.findMany({
where: {
organisationId: team.organisationId,
userId: {
not: owner.id,
},
},
include: {
user: true,
},
});
const defaultRecipientUser = teamMembers[0].user;
// Set up default recipients for the team
await setTeamDefaultRecipients(team.id, [
{
email: defaultRecipientUser.email,
name: defaultRecipientUser.name || defaultRecipientUser.email,
role: RecipientRole.CC,
},
]);
// Create a template
const template = await seedBlankTemplate(owner, team.id);
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set template title
await page.getByLabel('Title').fill('Template with Default Recipients');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
// Add a template recipient
await page.getByPlaceholder('Email').fill('template-recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Template Recipient');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Save template' }).click();
// Use template to create document
await page.waitForURL(`/t/${team.url}/templates`);
await page.getByRole('button', { name: 'Use Template' }).click();
await page.getByRole('button', { name: 'Create as draft' }).click();
// Wait for document to be created
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
const documentId = page.url().split('/').pop();
// Verify the document has both the template recipient and the default recipient
const document = await prisma.envelope.findFirstOrThrow({
where: {
id: documentId,
},
include: {
recipients: true,
},
});
expect(document.recipients.length).toBe(2);
const templateRecipient = document.recipients.find(
(r) => r.email === 'template-recipient@documenso.com',
);
expect(templateRecipient).toBeDefined();
const defaultRecipient = document.recipients.find(
(r) => r.email.toLowerCase() === defaultRecipientUser.email.toLowerCase(),
);
expect(defaultRecipient).toBeDefined();
expect(defaultRecipient?.role).toBe(RecipientRole.CC);
});
});
@@ -505,7 +505,9 @@ test('[TEMPLATE]: should create a document from a template using template docume
});
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
expect(firstDocumentData.data).toEqual(templateWithData.envelopeItems[0].documentData.data);
expect(firstDocumentData.initialData).toEqual(
templateWithData.envelopeItems[0].documentData.data,
);
expect(firstDocumentData.initialData).toEqual(
templateWithData.envelopeItems[0].documentData.initialData,
);
+3 -2
View File
@@ -15,11 +15,12 @@
"@hono/standard-validator": "^0.2.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@simplewebauthn/server": "^13.2.2",
"arctic": "^3.7.0",
"hono": "4.10.6",
"hono": "4.11.4",
"luxon": "^3.7.2",
"nanoid": "^5.1.6",
"ts-pattern": "^5.9.0",
"zod": "^3.25.76"
}
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { sValidator } from '@hono/standard-validator';
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { Hono } from 'hono';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -80,9 +81,9 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
expectedChallenge: challengeToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
authenticator: {
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
credential: {
id: isoBase64URL.fromBuffer(passkey.credentialId),
publicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null);
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { RefObject, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
/**
* Calculate the width and height of a text element.
+9 -6
View File
@@ -1,30 +1,33 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { TDocumentAuth } from '../types/document-auth';
import { DocumentAuth } from '../types/document-auth';
type DocumentAuthTypeData = {
key: TDocumentAuth;
value: string;
value: MessageDescriptor;
};
export const DOCUMENT_AUTH_TYPES: Record<string, DocumentAuthTypeData> = {
[DocumentAuth.ACCOUNT]: {
key: DocumentAuth.ACCOUNT,
value: 'Require account',
value: msg`Require account`,
},
[DocumentAuth.PASSKEY]: {
key: DocumentAuth.PASSKEY,
value: 'Require passkey',
value: msg`Require passkey`,
},
[DocumentAuth.TWO_FACTOR_AUTH]: {
key: DocumentAuth.TWO_FACTOR_AUTH,
value: 'Require 2FA',
value: msg`Require 2FA`,
},
[DocumentAuth.PASSWORD]: {
key: DocumentAuth.PASSWORD,
value: 'Require password',
value: msg`Require password`,
},
[DocumentAuth.EXPLICIT_NONE]: {
key: DocumentAuth.EXPLICIT_NONE,
value: 'None (Overrides global settings)',
value: msg`None (Overrides global settings)`,
},
} satisfies Record<TDocumentAuth, DocumentAuthTypeData>;
@@ -1,23 +1,26 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import type { TDocumentVisibility } from '../types/document-visibility';
type DocumentVisibilityTypeData = {
key: TDocumentVisibility;
value: string;
value: MessageDescriptor;
};
export const DOCUMENT_VISIBILITY: Record<string, DocumentVisibilityTypeData> = {
[DocumentVisibility.ADMIN]: {
key: DocumentVisibility.ADMIN,
value: 'Admins only',
value: msg`Admins only`,
},
[DocumentVisibility.EVERYONE]: {
key: DocumentVisibility.EVERYONE,
value: 'Everyone',
value: msg`Everyone`,
},
[DocumentVisibility.MANAGER_AND_ABOVE]: {
key: DocumentVisibility.MANAGER_AND_ABOVE,
value: 'Managers and above',
value: msg`Managers and above`,
},
} satisfies Record<TDocumentVisibility, DocumentVisibilityTypeData>;
+3 -2
View File
@@ -34,6 +34,7 @@
"@node-rs/bcrypt": "^1.10.7",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.2.6",
"@simplewebauthn/server": "^13.2.2",
"@sindresorhus/slugify": "^3.0.0",
"@team-plain/typescript-sdk": "^5.11.0",
"@vvo/tzdb": "^6.196.0",
@@ -54,7 +55,7 @@
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
"react-pdf": "^10.2.0",
"react-pdf": "^10.3.0",
"remeda": "^2.32.0",
"sharp": "0.34.5",
"skia-canvas": "^3.0.8",
@@ -67,4 +68,4 @@
"@types/luxon": "^3.7.1",
"@types/pg": "^8.15.6"
}
}
}
@@ -1,6 +1,7 @@
import type { Passkey } from '@prisma/client';
import { generateAuthenticationOptions } from '@simplewebauthn/server';
import type { AuthenticatorTransportFuture } from '@simplewebauthn/types';
import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
@@ -53,8 +54,7 @@ export const createPasskeyAuthenticationOptions = async ({
allowCredentials: preferredPasskey
? [
{
id: preferredPasskey.credentialId,
type: 'public-key',
id: isoBase64URL.fromBuffer(preferredPasskey.credentialId),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
transports: preferredPasskey.transports as AuthenticatorTransportFuture[],
},
@@ -1,5 +1,6 @@
import { generateRegistrationOptions } from '@simplewebauthn/server';
import type { AuthenticatorTransportFuture } from '@simplewebauthn/types';
import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
@@ -32,14 +33,13 @@ export const createPasskeyRegistrationOptions = async ({
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: userId.toString(),
userID: Buffer.from(userId.toString()),
userName: user.email,
userDisplayName: user.name ?? undefined,
timeout: PASSKEY_TIMEOUT,
attestationType: 'none',
excludeCredentials: passkeys.map((passkey) => ({
id: passkey.credentialId,
type: 'public-key',
id: isoBase64URL.fromBuffer(passkey.credentialId),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
transports: passkey.transports as AuthenticatorTransportFuture[],
})),
@@ -1,6 +1,7 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { verifyRegistrationResponse } from '@simplewebauthn/server';
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
import type { RegistrationResponseJSON } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { prisma } from '@documenso/prisma';
@@ -83,20 +84,19 @@ export const createPasskey = async ({
});
}
const { credentialPublicKey, credentialID, counter, credentialDeviceType, credentialBackedUp } =
verification.registrationInfo;
const { credentialDeviceType, credentialBackedUp, credential } = verification.registrationInfo;
await prisma.$transaction(async (tx) => {
await tx.passkey.create({
data: {
userId,
name: passkeyName,
credentialId: Buffer.from(credentialID),
credentialPublicKey: Buffer.from(credentialPublicKey),
counter,
credentialId: Buffer.from(isoBase64URL.toBuffer(credential.id)),
credentialPublicKey: Buffer.from(credential.publicKey),
counter: credential.counter,
credentialDeviceType,
credentialBackedUp,
transports: verificationResponse.response.transports,
transports: credential.transports,
},
});
@@ -1,5 +1,6 @@
import type { Envelope, Recipient } from '@prisma/client';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
@@ -252,9 +253,9 @@ const verifyPasskey = async ({
expectedChallenge: verificationToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
authenticator: {
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
credential: {
id: isoBase64URL.fromBuffer(passkey.credentialId),
publicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null); // May want to log this for insights.
@@ -33,7 +33,7 @@ import {
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { putNormalizedPdfFileServerSide } from '../../universal/upload/put-file.server';
import { isDocumentCompleted } from '../../utils/document';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
@@ -334,7 +334,7 @@ const injectFormValuesIntoDocument = async (
fileName = `${envelope.title}.pdf`;
}
const newDocumentData = await putPdfFileServerSide({
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
@@ -11,6 +11,7 @@ import {
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
@@ -184,7 +185,9 @@ export const createEnvelope = async ({
const buffer = await getFileServerSide(documentData);
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer), {
flattenForm: type !== EnvelopeType.TEMPLATE,
});
const titleToUse = item.title || title;
@@ -345,8 +348,22 @@ export const createEnvelope = async ({
const firstEnvelopeItem = envelope.envelopeItems[0];
const defaultRecipients = settings.defaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map(
(recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
}),
);
const allRecipients = [...(data.recipients || []), ...mappedDefaultRecipients];
await Promise.all(
(data.recipients || []).map(async (recipient) => {
allRecipients.map(async (recipient) => {
const recipientAuthOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [],
@@ -1,4 +1,4 @@
import type { Prisma } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { OrganisationType } from '@prisma/client';
import { OrganisationMemberRole } from '@prisma/client';
@@ -63,6 +63,7 @@ export const createOrganisation = async ({
const organisationSetting = await tx.organisationGlobalSettings.create({
data: {
...generateDefaultOrganisationSettings(),
defaultRecipients: Prisma.DbNull,
id: generateDatabaseId('org_setting'),
},
});
@@ -4,7 +4,9 @@ import { AppError } from '../../errors/app-error';
import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
export const normalizePdf = async (pdf: Buffer) => {
export const normalizePdf = async (pdf: Buffer, options: { flattenForm?: boolean } = {}) => {
const shouldFlattenForm = options.flattenForm ?? true;
const pdfDoc = await PDFDocument.load(pdf).catch((e) => {
console.error(`PDF normalization error: ${e.message}`);
@@ -20,8 +22,11 @@ export const normalizePdf = async (pdf: Buffer) => {
}
removeOptionalContentGroups(pdfDoc);
await flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
if (shouldFlattenForm) {
await flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
}
return Buffer.from(await pdfDoc.save());
};
+7 -1
View File
@@ -1,4 +1,9 @@
import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import {
OrganisationGroupType,
OrganisationMemberRole,
Prisma,
TeamMemberRole,
} from '@prisma/client';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -137,6 +142,7 @@ export const createTeam = async ({
const teamSettings = await tx.teamGlobalSettings.create({
data: {
...generateDefaultTeamSettings(),
defaultRecipients: Prisma.DbNull,
id: generateDatabaseId('team_setting'),
},
});
@@ -19,9 +19,11 @@ import { prisma } from '@documenso/prisma';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import type { SupportedLanguageCodes } from '../../constants/i18n';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { ZDefaultRecipientsSchema } from '../../types/default-recipients';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import type { TDocumentEmailSettings } from '../../types/document-email';
import type { TDocumentFormValues } from '../../types/document-form-values';
import type {
TCheckboxFieldMeta,
TDropdownFieldMeta,
@@ -42,7 +44,7 @@ import {
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { putNormalizedPdfFileServerSide } from '../../universal/upload/put-file.server';
import { extractDerivedDocumentMeta } from '../../utils/document';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
@@ -55,6 +57,7 @@ import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { incrementDocumentId } from '../envelope/increment-id';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -117,6 +120,8 @@ export type CreateDocumentFromTemplateOptions = {
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
};
formValues?: TDocumentFormValues;
requestMetadata: ApiRequestMetadata;
};
@@ -303,6 +308,7 @@ export const createDocumentFromTemplate = async ({
folderId,
prefillFields,
attachments,
formValues,
}: CreateDocumentFromTemplateOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
@@ -396,6 +402,30 @@ export const createDocumentFromTemplate = async ({
};
});
const defaultRecipients = settings.defaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const defaultRecipientsFinal: FinalRecipient[] = defaultRecipients.map((recipient) => {
const authOptions = ZRecipientAuthOptionsSchema.parse({});
return {
templateRecipientId: -1,
fields: [],
name: recipient.name || recipient.email,
email: recipient.email,
role: recipient.role,
signingOrder: null,
authOptions: createRecipientAuthOptions({
accessAuth: authOptions.accessAuth,
actionAuth: authOptions.actionAuth,
}),
token: nanoid(),
};
});
const allFinalRecipients = [...finalRecipients, ...defaultRecipientsFinal];
// Key = original envelope item ID
// Value = duplicated envelope item ID.
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
@@ -431,11 +461,19 @@ export const createDocumentFromTemplate = async ({
});
}
const buffer = await getFileServerSide(documentDataToDuplicate);
let buffer = await getFileServerSide(documentDataToDuplicate);
const titleToUse = item.title || finalEnvelopeTitle;
const duplicatedFile = await putPdfFileServerSide({
if (formValues) {
// eslint-disable-next-line require-atomic-updates
buffer = await insertFormValuesInPdf({
pdf: Buffer.from(buffer),
formValues,
});
}
const duplicatedFile = await putNormalizedPdfFileServerSide({
name: titleToUse,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(buffer),
@@ -445,7 +483,7 @@ export const createDocumentFromTemplate = async ({
data: {
type: duplicatedFile.type,
data: duplicatedFile.data,
initialData: duplicatedFile.initialData,
initialData: documentDataToDuplicate.data,
},
});
@@ -515,7 +553,7 @@ export const createDocumentFromTemplate = async ({
documentMetaId: documentMeta.id,
recipients: {
createMany: {
data: finalRecipients.map((recipient) => {
data: allFinalRecipients.map((recipient) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions);
return {
@@ -596,7 +634,7 @@ export const createDocumentFromTemplate = async ({
}
}
Object.values(finalRecipients).forEach(({ token, fields }) => {
Object.values(allFinalRecipients).forEach(({ token, fields }) => {
const recipient = envelope.recipients.find((recipient) => recipient.token === token);
if (!recipient) {
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Admin-Panel"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Nur Admins"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Konfigurieren Sie die Teamrollen für jedes Mitglied"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Bestätigen"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Dokument gesendet"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Dokumenteneinstellungen"
@@ -4321,6 +4324,14 @@ msgstr "Beigefügte Dokument"
msgid "Enclosed Documents"
msgstr "Beigefügte Dokumente"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Eingeben"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Geben Sie einen Namen für Ihren neuen Ordner ein. Ordner helfen Ihnen, Ihre Dateien zu organisieren."
@@ -4329,6 +4340,26 @@ msgstr "Geben Sie einen Namen für Ihren neuen Ordner ein. Ordner helfen Ihnen,
msgid "Enter claim name"
msgstr "Anspruchsname eingeben"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "E-Mail eingeben"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Initialen eingeben"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Namen eingeben"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Nummer eingeben"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Text eingeben"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Geben Sie die Domain ein, die Sie zum Versenden von E-Mails verwenden möchten (ohne http:// oder www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Ereignistyp"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Jeder"
@@ -5222,14 +5254,6 @@ msgstr "Initialen sind erforderlich"
msgid "Initials Settings"
msgstr "Einstellungen für Initialen"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Fügen Sie einen Wert in das Zahlenfeld ein."
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Fügen Sie einen Wert in das Textfeld ein."
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Eingefügt"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Manager"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Manager und höher"
@@ -6133,6 +6158,10 @@ msgstr "Keine Sorge, das passiert! Geben Sie Ihre E-Mail ein, und wir senden Ihn
msgid "None"
msgstr "Keine"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Keine (überschreibt globale Einstellungen)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Bitte kontaktieren Sie den Webseiteninhaber für weitere Unterstützung.
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Bitte geben Sie einen aussagekräftigen Namen für Ihr Token ein. Dies wird Ihnen helfen, es später zu identifizieren."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Bitte gib eine Zahl ein"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Bitte geben Sie einen gültigen Namen ein."
msgid "Please enter a valid number"
msgstr "Bitte geben Sie eine gültige Nummer ein."
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Bitte gib einen Wert ein"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Bitte gib deine E-Mail-Adresse ein"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Bitte gib deinen vollständigen Namen ein"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Bitte geben Sie Ihre Initialen ein"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Bitte als angesehen markieren, um zu vervollständigen"
@@ -7250,6 +7299,22 @@ msgstr "Anfrage"
msgid "Requesting Organisation"
msgstr "Anfragende Organisation"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "2FA erforderlich"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Konto erforderlich"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Passkey erforderlich"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Passwort erforderlich"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Wählen Sie Mitglieder aus, die diesem Team hinzugefügt werden sollen"
msgid "Select Option"
msgstr "Option auswählen"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Optionen auswählen"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Passkey auswählen"
@@ -7884,13 +7953,7 @@ msgstr "Erweiterte Einstellungen anzeigen"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Vorlagen in Ihrem öffentlichen Profil anzeigen, damit Ihre Zielgruppe unterschreiben und schnell loslegen kann"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Unterzeichnen als {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Unterzeichnen als<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Sign Checkbox-Feld"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Dokument signieren"
msgid "Sign Documents"
msgstr "Dokumente unterschreiben"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Signatur-E-Mail"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Anmelden"
msgid "Sign in to your account"
msgstr "Melden Sie sich bei Ihrem Konto an"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Initialen signieren"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Name signieren"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Zahlenfeld signieren"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Ausloggen"
msgid "Sign Signature Field"
msgstr "Signaturfeld signieren"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Textfeld signieren"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Registrieren mit Microsoft"
msgid "Sign Up with OIDC"
msgstr "Registrieren mit OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "E-Mail ins Feld signieren"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Vollständigen Namen ins Feld signieren"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Initialen ins Feld signieren"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+77 -50
View File
@@ -1286,6 +1286,7 @@ msgid "Admin Panel"
msgstr "Admin Panel"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Admins only"
@@ -2573,6 +2574,7 @@ msgid "Configure the team roles for each member"
msgstr "Configure the team roles for each member"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Confirm"
@@ -3729,6 +3731,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Document sent"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Document Settings"
@@ -4316,6 +4319,14 @@ msgstr "Enclosed Document"
msgid "Enclosed Documents"
msgstr "Enclosed Documents"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Enter"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Enter a name for your new folder. Folders help you organise your items."
@@ -4324,6 +4335,26 @@ msgstr "Enter a name for your new folder. Folders help you organise your items."
msgid "Enter claim name"
msgstr "Enter claim name"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "Enter Email"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Enter Initials"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Enter Name"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Enter Number"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Enter Text"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Enter the domain you want to use for sending emails (without http:// or www)"
@@ -4518,6 +4549,7 @@ msgid "Event Type"
msgstr "Event Type"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Everyone"
@@ -5217,14 +5249,6 @@ msgstr "Initials are required"
msgid "Initials Settings"
msgstr "Initials Settings"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Insert a value into the number field"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Insert a value into the text field"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Inserted"
@@ -5733,6 +5757,7 @@ msgid "Manager"
msgstr "Manager"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Managers and above"
@@ -6128,6 +6153,10 @@ msgstr "No worries, it happens! Enter your email and we'll email you a special l
msgid "None"
msgstr "None"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "None (Overrides global settings)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6691,6 +6720,10 @@ msgstr "Please contact the site owner for further assistance."
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Please enter a meaningful name for your token. This will help you identify it later."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Please enter a number"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6701,6 +6734,22 @@ msgstr "Please enter a valid name."
msgid "Please enter a valid number"
msgstr "Please enter a valid number"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Please enter a value"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Please enter your email address"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Please enter your full name"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Please enter your initials"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Please mark as viewed to complete"
@@ -7245,6 +7294,22 @@ msgstr "Request"
msgid "Requesting Organisation"
msgstr "Requesting Organisation"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "Require 2FA"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Require account"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Require passkey"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Require password"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7657,6 +7722,10 @@ msgstr "Select members to add to this team"
msgid "Select Option"
msgstr "Select Option"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Select Options"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Select passkey"
@@ -7879,13 +7948,7 @@ msgstr "Show advanced settings"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Show templates in your public profile for your audience to sign and get started quickly"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7919,10 +7982,6 @@ msgstr "Sign as {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Sign as<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Sign Checkbox Field"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7939,10 +7998,6 @@ msgstr "Sign Document"
msgid "Sign Documents"
msgstr "Sign Documents"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Sign Email"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7965,18 +8020,6 @@ msgstr "Sign In"
msgid "Sign in to your account"
msgstr "Sign in to your account"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Sign Initials"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Sign Name"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Sign Number Field"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7987,10 +8030,6 @@ msgstr "Sign Out"
msgid "Sign Signature Field"
msgstr "Sign Signature Field"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Sign Text Field"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8013,18 +8052,6 @@ msgstr "Sign Up with Microsoft"
msgid "Sign Up with OIDC"
msgstr "Sign Up with OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Sign your email into the field"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Sign your full name into the field"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Sign your initials into the field"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Panel de Administración"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Solo para administradores"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Configura los roles del equipo para cada miembro"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Confirmar"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Documento enviado"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Configuración del Documento"
@@ -4321,6 +4324,14 @@ msgstr "Documento Adjunto"
msgid "Enclosed Documents"
msgstr "Documentos adjuntos"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Ingresar"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Ingrese un nombre para su nueva carpeta. Las carpetas le ayudan a organizar sus elementos."
@@ -4329,6 +4340,26 @@ msgstr "Ingrese un nombre para su nueva carpeta. Las carpetas le ayudan a organi
msgid "Enter claim name"
msgstr "Ingresar nombre de la reclamación"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "Ingresar correo electrónico"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Ingresar iniciales"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Ingresar nombre"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Ingresar número"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Ingresar texto"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Ingresa el dominio que deseas usar para enviar correos electrónicos (sin http:// ni www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Tipo de evento"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Todos"
@@ -5222,14 +5254,6 @@ msgstr "Se requieren iniciales"
msgid "Initials Settings"
msgstr "Configuración de Iniciales"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Introduce un valor en el campo de número"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Introduce un valor en el campo de texto"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Insertado"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Gerente"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Gerentes y superiores"
@@ -6133,6 +6158,10 @@ msgstr "¡No te preocupes, sucede! Ingresa tu correo electrónico y te enviaremo
msgid "None"
msgstr "Ninguno"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Ninguno (anula la configuración global)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Por favor, contacte al propietario del sitio para más asistencia."
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Por favor, ingresa un nombre significativo para tu token. Esto te ayudará a identificarlo más tarde."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Por favor ingresa un número"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Por favor, introduce un nombre válido."
msgid "Please enter a valid number"
msgstr "Por favor, ingresa un número válido"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Por favor ingresa un valor"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Por favor ingresa tu dirección de correo electrónico"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Por favor ingresa tu nombre completo"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Por favor, introduzca sus iniciales"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Por favor marque como visto para completar"
@@ -7250,6 +7299,22 @@ msgstr "Solicitud"
msgid "Requesting Organisation"
msgstr "Organización solicitante"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "Requerir 2FA"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Requerir cuenta"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Requerir llave de acceso"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Requerir contraseña"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Seleccione los miembros para añadir a este equipo"
msgid "Select Option"
msgstr "Seleccionar una opción"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Seleccionar opciones"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Seleccionar clave de acceso"
@@ -7884,13 +7953,7 @@ msgstr "Mostrar configuraciones avanzadas"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Mostrar plantillas en tu perfil público para que tu audiencia firme y comience rápidamente"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Firmar como {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Firmar como<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Campo de casilla de verificación de firma"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Firmar Documento"
msgid "Sign Documents"
msgstr "Firmar documentos"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Correo de firma"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Iniciar sesión"
msgid "Sign in to your account"
msgstr "Inicia sesión en tu cuenta"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Iniciales de firma"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Nombre de firma"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Campo de número de firma"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Cerrar sesión"
msgid "Sign Signature Field"
msgstr "Campo de firma Firma"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Campo de texto de firma"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Registrarse con Microsoft"
msgid "Sign Up with OIDC"
msgstr "Regístrate con OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Firme su correo electrónico en el campo"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Firme su nombre completo en el campo"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Firme sus iniciales en el campo"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Panneau d'administration"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Administrateurs uniquement"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Configurez les rôles d'équipe pour chaque membre"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Confirmer"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Document envoyé"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Paramètres du document"
@@ -4321,6 +4324,14 @@ msgstr "Document joint"
msgid "Enclosed Documents"
msgstr "Documents joints"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Entrer"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Entrez un nom pour votre nouveau dossier. Les dossiers vous aident à organiser vos éléments."
@@ -4329,6 +4340,26 @@ msgstr "Entrez un nom pour votre nouveau dossier. Les dossiers vous aident à or
msgid "Enter claim name"
msgstr "Entrez le nom de la réclamation"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "Entrer ladresse e-mail"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Entrer les initiales"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Entrer le nom"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Entrer le numéro"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Entrer le texte"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Entrez le domaine que vous souhaitez utiliser pour l'envoi des e-mails (sans http:// ou www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Type d'événement"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Tout le monde"
@@ -5222,14 +5254,6 @@ msgstr "Les initiales sont requises"
msgid "Initials Settings"
msgstr "Paramètres des initiales"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Insérez une valeur dans le champ de nombre"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Insérez une valeur dans le champ de texte"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Inséré"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Gestionnaire"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Responsables et supérieur"
@@ -6133,6 +6158,10 @@ msgstr "Pas de soucis, ça arrive ! Entrez votre email et nous vous enverrons un
msgid "None"
msgstr "Aucun"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Aucun (remplace les paramètres globaux)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Veuillez contacter le propriétaire du site pour obtenir de l'aide suppl
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Veuillez entrer un nom significatif pour votre token. Cela vous aidera à l'identifier plus tard."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Veuillez entrer un nombre"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Veuiillez entrer un nom valide."
msgid "Please enter a valid number"
msgstr "Veuillez entrer un numéro valide"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Veuillez entrer une valeur"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Veuillez entrer votre adresse e-mail"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Veuillez entrer votre nom complet"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Veuillez saisir vos initiales"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Veuillez marquer comme vu pour compléter"
@@ -7250,6 +7299,22 @@ msgstr "Requête"
msgid "Requesting Organisation"
msgstr "Organisation Demandeuse"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "Exiger une 2FA"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Exiger un compte"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Exiger une clé daccès"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Exiger un mot de passe"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Sélectionnez des membres à ajouter à cette équipe"
msgid "Select Option"
msgstr "Sélectionner une option"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Sélectionner des options"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Sélectionner la clé d'authentification"
@@ -7884,13 +7953,7 @@ msgstr "Afficher les paramètres avancés"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Afficher des modèles dans votre profil public pour que votre audience puisse signer et commencer rapidement"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Signer comme {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Signer comme<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Champ de case à cocher signe"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Signer le document"
msgid "Sign Documents"
msgstr "Signer des documents"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Email de Signature"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Se connecter"
msgid "Sign in to your account"
msgstr "Connectez-vous à votre compte"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Initiales de Signature"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Nom de Signature"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Champ de nombre à signer"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Déconnexion"
msgid "Sign Signature Field"
msgstr "Champ de signature"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Champ de texte à signer"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "S'inscrire avec Microsoft"
msgid "Sign Up with OIDC"
msgstr "S'inscrire avec OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Signez votre email dans le champ"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Signez votre nom complet dans le champ"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Signez vos initiales dans le champ"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Pannello di Amministrazione"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Solo amministratori"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Configura i ruoli del team per ogni membro"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Conferma"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Documento inviato"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Impostazioni Documento"
@@ -4321,6 +4324,14 @@ msgstr "Documento Allegato"
msgid "Enclosed Documents"
msgstr "Documenti allegati"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Inserisci"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Inserisci un nome per la tua nuova cartella. Le cartelle ti aiutano a organizzare i tuoi elementi."
@@ -4329,6 +4340,26 @@ msgstr "Inserisci un nome per la tua nuova cartella. Le cartelle ti aiutano a or
msgid "Enter claim name"
msgstr "Inserisci nome richiesta"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "Inserisci l'email"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Inserisci le iniziali"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Inserisci il nome"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Inserisci il numero"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Inserisci il testo"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Inserisci il dominio che desideri utilizzare per inviare email (senza http:// o www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Tipo di Evento"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Tutti"
@@ -5222,14 +5254,6 @@ msgstr "Iniziali richieste"
msgid "Initials Settings"
msgstr "Impostazioni Iniziali"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Inserisci un valore nel campo numero"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Inserisci un valore nel campo di testo"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Inserito"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Responsabile"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Manager e superiori"
@@ -6133,6 +6158,10 @@ msgstr "Non ti preoccupare, succede! Inserisci la tua email e ti invieremo un li
msgid "None"
msgstr "Nessuno"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Nessuno (sovrascrive le impostazioni globali)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Si prega di contattare il proprietario del sito per ulteriori assistenza
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Si prega di inserire un nome significativo per il proprio token. Questo ti aiuterà a identificarlo più tardi."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Inserisci un numero"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Per favore inserisci un nome valido."
msgid "Please enter a valid number"
msgstr "Per favore inserisci un numero valido"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Inserisci un valore"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Inserisci il tuo indirizzo email"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Inserisci il tuo nome completo"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Inserisci le tue iniziali"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Per favore segna come visto per completare"
@@ -7250,6 +7299,22 @@ msgstr "Richiesta"
msgid "Requesting Organisation"
msgstr "Organizzazione richiedente"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "Richiedi 2FA"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Richiedi account"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Richiedi passkey"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Richiedi password"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Seleziona membri da aggiungere a questo team"
msgid "Select Option"
msgstr "Seleziona opzione"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Seleziona opzioni"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Seleziona una chiave di accesso"
@@ -7884,13 +7953,7 @@ msgstr "Mostra impostazioni avanzate"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Mostra modelli nel tuo profilo pubblico per il tuo pubblico da firmare e iniziare rapidamente"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Firma come {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Firma come<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Campo casella di controllo firma"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Firma documento"
msgid "Sign Documents"
msgstr "Firma documenti"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Firma email"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Accedi"
msgid "Sign in to your account"
msgstr "Accedi al tuo account"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Firma iniziali"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Firma nome"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Campo numero firma"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Disconnetti"
msgid "Sign Signature Field"
msgstr "Campo firma firma"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Campo testo firma"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Registrati con Microsoft"
msgid "Sign Up with OIDC"
msgstr "Iscriviti con OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Firma la tua email nel campo"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Firma il tuo nome completo nel campo"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Firma le tue iniziali nel campo"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "管理画面"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "管理者のみ"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "メンバーごとにチームロールを設定します"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "確認"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "ドキュメントが送信されました"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "ドキュメント設定"
@@ -4321,6 +4324,14 @@ msgstr "同封文書"
msgid "Enclosed Documents"
msgstr "同封された文書"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "入力してください"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "新しいフォルダ名を入力してください。フォルダを使うとアイテムを整理できます。"
@@ -4329,6 +4340,26 @@ msgstr "新しいフォルダ名を入力してください。フォルダを使
msgid "Enter claim name"
msgstr "クレーム名を入力"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "メールアドレスを入力してください"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "イニシャルを入力してください"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "氏名を入力してください"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "番号を入力してください"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "テキストを入力してください"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "メール送信に使用したいドメインを入力してください(http:// や www は不要)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "イベントタイプ"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "全員"
@@ -5222,14 +5254,6 @@ msgstr "イニシャルは必須です"
msgid "Initials Settings"
msgstr "イニシャルの設定"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "数値フィールドに値を入力してください"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "テキストフィールドに値を入力してください"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "挿入済み"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "マネージャー"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "マネージャー以上"
@@ -6133,6 +6158,10 @@ msgstr "大丈夫です。メールアドレスを入力していただければ
msgid "None"
msgstr "なし"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "なし(グローバル設定を上書き)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "詳しくはサイトのオーナーにお問い合わせください。
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "トークンの用途が分かる名前を入力してください。後から識別しやすくなります。"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "数値を入力してください"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "有効な名前を入力してください。"
msgid "Please enter a valid number"
msgstr "有効な数値を入力してください"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "値を入力してください"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "メールアドレスを入力してください"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "氏名を入力してください"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "イニシャルを入力してください"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "完了するには閲覧済みにしてください"
@@ -7250,6 +7299,22 @@ msgstr "リクエスト"
msgid "Requesting Organisation"
msgstr "要求元の組織"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "2 要素認証を必須にする"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "アカウントを必須にする"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "パスキーを必須にする"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "パスワードを必須にする"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "このチームに追加するメンバーを選択"
msgid "Select Option"
msgstr "オプションを選択"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "オプションを選択"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "パスキーを選択"
@@ -7884,13 +7953,7 @@ msgstr "詳細設定を表示"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "オーディエンスがすぐに署名を開始できるよう、公開プロフィールにテンプレートを表示します"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "{0} として署名 <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "<0>{0} <1>({1})</1></0>として署名"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "署名用チェックボックスフィールド"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "文書に署名"
msgid "Sign Documents"
msgstr "ドキュメントに署名"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "署名用メールアドレス"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "サインイン"
msgid "Sign in to your account"
msgstr "アカウントにサインイン"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "署名用イニシャル"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "署名用氏名"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "署名用数値フィールド"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "サインアウト"
msgid "Sign Signature Field"
msgstr "署名用署名フィールド"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "署名用テキストフィールド"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Microsoftでサインアップ"
msgid "Sign Up with OIDC"
msgstr "OIDC でサインアップ"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "メールアドレスをフィールドに入力してください"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "氏名をフィールドに入力してください"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "イニシャルをフィールドに入力してください"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "관리자 패널"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "관리자 전용"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "각 구성원에 대한 팀 역할을 구성하세요."
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "확인"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "문서가 전송되었습니다."
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "문서 설정"
@@ -4321,6 +4324,14 @@ msgstr "동봉 문서"
msgid "Enclosed Documents"
msgstr "동봉된 문서들"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "입력하세요"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "새 폴더 이름을 입력하세요. 폴더는 항목을 정리하는 데 도움이 됩니다."
@@ -4329,6 +4340,26 @@ msgstr "새 폴더 이름을 입력하세요. 폴더는 항목을 정리하는
msgid "Enter claim name"
msgstr "클레임 이름 입력"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "이메일을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "이니셜을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "이름을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "숫자를 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "텍스트를 입력하세요"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "이메일 발신에 사용할 도메인을 입력하세요( http:// 또는 www 제외)."
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "이벤트 유형"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "모두"
@@ -5222,14 +5254,6 @@ msgstr "이니셜은 필수 항목입니다"
msgid "Initials Settings"
msgstr "이니셜 설정"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "숫자 필드에 값을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "텍스트 필드에 값을 입력하세요"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "삽입됨"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "관리자"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "관리자 이상"
@@ -6133,6 +6158,10 @@ msgstr "걱정하지 마세요. 이메일을 입력하시면 비밀번호를 재
msgid "None"
msgstr "없음"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "없음(전역 설정 무시)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "사이트 소유자에게 추가 지원을 요청해 주세요."
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "토큰을 나중에 식별할 수 있도록 의미 있는 이름을 입력해 주세요."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "숫자를 입력하세요"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "올바른 이름을 입력해 주세요."
msgid "Please enter a valid number"
msgstr "올바른 숫자를 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "값을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "이메일 주소를 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "전체 이름을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "이니셜을 입력하세요"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "완료하려면 확인함으로 표시해 주세요"
@@ -7250,6 +7299,22 @@ msgstr "요청"
msgid "Requesting Organisation"
msgstr "요청하는 조직"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "2단계 인증 필수"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "계정 필수"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "패스키 필수"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "비밀번호 필수"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "이 팀에 추가할 구성원을 선택하세요."
msgid "Select Option"
msgstr "옵션 선택"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "옵션 선택"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "패스키 선택"
@@ -7884,13 +7953,7 @@ msgstr "고급 설정 표시"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "공개 프로필에 템플릿을 표시해, 사용자가 빠르게 서명하고 시작할 수 있도록 하세요"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "{0} <0>({1})</0>로 서명"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "<0>{0} <1>({1})</1></0>로 서명"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "체크박스 필드 서명"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "문서 서명"
msgid "Sign Documents"
msgstr "문서 서명"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "이메일 서명"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "로그인"
msgid "Sign in to your account"
msgstr "계정에 로그인"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "이니셜 서명"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "이름 서명"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "숫자 필드 서명"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "로그아웃"
msgid "Sign Signature Field"
msgstr "서명 필드 서명"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "텍스트 필드 서명"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Microsoft로 가입"
msgid "Sign Up with OIDC"
msgstr "OIDC로 가입"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "필드에 이메일을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "필드에 전체 이름을 입력하세요"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "필드에 이니셜을 입력하세요"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Beheerderspaneel"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Alleen beheerders"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Configureer de teamrollen voor elk lid"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Bevestigen"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Document verzonden"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Documentinstellingen"
@@ -4321,6 +4324,14 @@ msgstr "Bijgevoegd document"
msgid "Enclosed Documents"
msgstr "Bijgevoegde documenten"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Invoeren"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Voer een naam in voor je nieuwe map. Mappen helpen je je items te organiseren."
@@ -4329,6 +4340,26 @@ msgstr "Voer een naam in voor je nieuwe map. Mappen helpen je je items te organi
msgid "Enter claim name"
msgstr "Voer claimnaam in"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "E-mailadres invoeren"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Initialen invoeren"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Naam invoeren"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Nummer invoeren"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Tekst invoeren"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Voer het domein in dat je wilt gebruiken voor het verzenden van e-mails (zonder http:// of www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Gebeurtenistype"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Iedereen"
@@ -5222,14 +5254,6 @@ msgstr "Initialen zijn verplicht"
msgid "Initials Settings"
msgstr "Initialen-instellingen"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Vul een waarde in het nummerveld in"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Vul een waarde in het tekstveld in"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Ingevoegd"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Manager"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Managers en hoger"
@@ -6133,6 +6158,10 @@ msgstr "Geen zorgen, dat gebeurt wel eens! Vul je email in en we sturen je ee
msgid "None"
msgstr "Geen"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Geen (Overschrijft globale instellingen)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Neem contact op met de site-eigenaar voor verdere hulp."
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Voer een betekenisvolle naam in voor je token. Hiermee kun je het later herkennen."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Voer een nummer in"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Voer een geldige naam in."
msgid "Please enter a valid number"
msgstr "Voer een geldig nummer in"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Voer een waarde in"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Voer je e-mailadres in"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Voer je volledige naam in"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Vul je initialen in"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Markeer als bekeken om te voltooien"
@@ -7250,6 +7299,22 @@ msgstr "Verzoek"
msgid "Requesting Organisation"
msgstr "Aanvragende organisatie"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "2FA vereisen"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Account vereisen"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Passkey vereisen"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Wachtwoord vereisen"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Selecteer leden om aan dit team toe te voegen"
msgid "Select Option"
msgstr "Selecteer optie"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Selecteer opties"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Passkey selecteren"
@@ -7884,13 +7953,7 @@ msgstr "Geavanceerde instellingen weergeven"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Toon sjablonen in je openbare profiel zodat je publiek snel kan ondertekenen en beginnen"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Ondertekenen als {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Ondertekenen als<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Checkboxveld ondertekenen"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Document ondertekenen"
msgid "Sign Documents"
msgstr "Documenten ondertekenen"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "E-mail ondertekenen"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Inloggen"
msgid "Sign in to your account"
msgstr "Log in op je account"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Initialen ondertekenen"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Naam ondertekenen"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Nummer-veld ondertekenen"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Uitloggen"
msgid "Sign Signature Field"
msgstr "Handtekeningveld ondertekenen"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Tekstveld ondertekenen"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Registreren met Microsoft"
msgid "Sign Up with OIDC"
msgstr "Registreren met OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Schrijf uw e-mailadres in het veld"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Schrijf uw volledige naam in het veld"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Schrijf uw initialen in het veld"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+79 -52
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "Panel administratora"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Tylko dla administratorów"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "Skonfiguruj role w zespole dla każdego użytkownika"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Potwierdź"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Wysłano dokument"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Ustawienia dokumentu"
@@ -4319,7 +4322,15 @@ msgstr "Załączony dokument"
#: packages/lib/server-only/pdf/render-audit-logs.ts
msgid "Enclosed Documents"
msgstr "Załączone dokumenty"
msgstr "Załączniki"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "Wpisz lub wprowadź wartość"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
@@ -4329,6 +4340,26 @@ msgstr "Wpisz nazwę nowego folderu. Foldery pomagają uporządkować elementy."
msgid "Enter claim name"
msgstr "Wpisz nazwę"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "Wpisz adres e-mail"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "Wpisz inicjały"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "Wpisz imię i nazwisko"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "Wpisz liczbę"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "Wpisz tekst"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Wpisz domenę do wysyłania wiadomości (bez http:// lub www)"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "Rodzaj zdarzenia"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Wszyscy"
@@ -5222,14 +5254,6 @@ msgstr "Inicjały są wymagane"
msgid "Initials Settings"
msgstr "Ustawienia inicjałów"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Wpisz wartość do pola liczby"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Wpisz wartość do pola tekstowego"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Wypełniono"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "Manager"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Managerowie i wyżej"
@@ -6133,6 +6158,10 @@ msgstr "Nie martw się! Wpisz swój adres e-mail, a my wyślemy Ci specjalny lin
msgid "None"
msgstr "Brak"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "Brak (odziedzicz metodę uwierzytelniania)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "Skontaktuj się z właścicielem dokumentu, aby uzyskać pomoc."
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Wpisz nazwę tokena. Pomoże to później w jego identyfikacji."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "Wpisz liczbę"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "Wpisz prawidłową nazwę."
msgid "Please enter a valid number"
msgstr "Wpisz prawidłową liczbę"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "Wpisz wartość"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "Wpisz swój adres e-mail"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "Wpisz swoje pełne imię i nazwisko"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "Wprowadź swoje inicjały"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Oznacz jako wyświetlony, aby zakończyć"
@@ -7250,6 +7299,22 @@ msgstr "Żądanie"
msgid "Requesting Organisation"
msgstr "Wnioskująca organizacja"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "Wymagana weryfikacja dwuetapowa"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "Wymagane konto"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "Wymagany klucz dostępu"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "Wymagane hasło"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "Wybierz użytkowników, których chcesz dodać do zespołu"
msgid "Select Option"
msgstr "Wybierz opcję"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "Wybierz opcje"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Wybierz klucz dostępu"
@@ -7884,13 +7953,7 @@ msgstr "Pokaż ustawienia zaawansowane"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Pokaż szablony w profilu publicznym, aby szybko podpisać dokument"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "Podpisz jako {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Podpisz jako<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Podpisz pole wyboru"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "Podpisz dokument"
msgid "Sign Documents"
msgstr "Podpisz dokumenty"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Podpisz adres e-mail"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "Zaloguj się"
msgid "Sign in to your account"
msgstr "Zaloguj się na swoje konto"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Podpisz inicjały"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Podpisz nazwę"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Podpisz pole liczby"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "Wyloguj"
msgid "Sign Signature Field"
msgstr "Podpisz pole podpisu"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Podpisz pole tekstowe"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "Zarejestruj się przez Microsoft"
msgid "Sign Up with OIDC"
msgstr "Zarejestruj się przez OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Podpisz adres e-mail w polu"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Podpisz nazwę w polu"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Podpisz inicjały w polu"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+77 -50
View File
@@ -1286,6 +1286,7 @@ msgid "Admin Panel"
msgstr "Painel de Administração"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "Apenas administradores"
@@ -2573,6 +2574,7 @@ msgid "Configure the team roles for each member"
msgstr "Configure as funções da equipe para cada membro"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "Confirmar"
@@ -3729,6 +3731,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "Documento enviado"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "Configurações do Documento"
@@ -4316,6 +4319,14 @@ msgstr "Documento Anexo"
msgid "Enclosed Documents"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr ""
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Digite um nome para sua nova pasta. As pastas ajudam você a organizar seus itens."
@@ -4324,6 +4335,26 @@ msgstr "Digite um nome para sua nova pasta. As pastas ajudam você a organizar s
msgid "Enter claim name"
msgstr "Digite o nome da reivindicação"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr ""
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "Digite o domínio que você deseja usar para enviar e-mails (sem http:// ou www)"
@@ -4518,6 +4549,7 @@ msgid "Event Type"
msgstr "Tipo de Evento"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "Todos"
@@ -5217,14 +5249,6 @@ msgstr "Iniciais são obrigatórias"
msgid "Initials Settings"
msgstr "Configurações de Iniciais"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "Insira um valor no campo numérico"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "Insira um valor no campo de texto"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "Inserido"
@@ -5733,6 +5757,7 @@ msgid "Manager"
msgstr "Gerente"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "Gerentes e acima"
@@ -6128,6 +6153,10 @@ msgstr "Não se preocupe, acontece! Digite seu e-mail e enviaremos um link espec
msgid "None"
msgstr "Nenhum"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6691,6 +6720,10 @@ msgstr "Por favor, entre em contato com o proprietário do site para mais assist
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "Por favor, insira um nome significativo para seu token. Isso ajudará você a identificá-lo mais tarde."
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6701,6 +6734,22 @@ msgstr "Por favor, insira um nome válido."
msgid "Please enter a valid number"
msgstr "Por favor, insira um número válido"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr ""
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "Por favor, marque como visualizado para completar"
@@ -7245,6 +7294,22 @@ msgstr "Solicitação"
msgid "Requesting Organisation"
msgstr "Organização Solicitante"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr ""
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr ""
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr ""
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr ""
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7657,6 +7722,10 @@ msgstr "Selecione membros para adicionar a esta equipe"
msgid "Select Option"
msgstr "Selecionar Opção"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "Selecionar passkey"
@@ -7879,13 +7948,7 @@ msgstr "Mostrar configurações avançadas"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "Mostre modelos em seu perfil público para seu público assinar e começar rapidamente"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7919,10 +7982,6 @@ msgstr "Assinar como {0} <0>({1})</0>"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "Assinar como<0>{0} <1>({1})</1></0>"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "Assinar Campo de Caixa de Seleção"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7939,10 +7998,6 @@ msgstr "Assinar Documento"
msgid "Sign Documents"
msgstr "Assinar Documentos"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "Assinar E-mail"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7965,18 +8020,6 @@ msgstr "Entrar"
msgid "Sign in to your account"
msgstr "Entre na sua conta"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "Assinar Iniciais"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "Assinar Nome"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "Assinar Campo Numérico"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7987,10 +8030,6 @@ msgstr "Sair"
msgid "Sign Signature Field"
msgstr "Assinar Campo de Assinatura"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "Assinar Campo de Texto"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8013,18 +8052,6 @@ msgstr "Inscrever-se com Microsoft"
msgid "Sign Up with OIDC"
msgstr "Inscrever-se com OIDC"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "Assine seu e-mail no campo"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "Assine seu nome completo no campo"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "Assine suas iniciais no campo"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+78 -51
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-06 04:17\n"
"PO-Revision-Date: 2026-01-13 02:39\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1291,6 +1291,7 @@ msgid "Admin Panel"
msgstr "管理面板"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Admins only"
msgstr "仅限管理员"
@@ -2578,6 +2579,7 @@ msgid "Configure the team roles for each member"
msgstr "配置每个成员的团队角色"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "Confirm"
msgstr "确认"
@@ -3734,6 +3736,7 @@ msgctxt "Audit log format"
msgid "Document sent"
msgstr "文档已发送"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Document Settings"
msgstr "文档设置"
@@ -4321,6 +4324,14 @@ msgstr "随附文档"
msgid "Enclosed Documents"
msgstr "随附文件"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter"
msgstr "输入"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "为新文件夹输入名称。文件夹可以帮助您整理项目。"
@@ -4329,6 +4340,26 @@ msgstr "为新文件夹输入名称。文件夹可以帮助您整理项目。"
msgid "Enter claim name"
msgstr "输入声明名称"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Enter Email"
msgstr "输入邮箱"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Enter Initials"
msgstr "输入姓名首字母"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Enter Name"
msgstr "输入姓名"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Enter Number"
msgstr "输入数字"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Enter Text"
msgstr "输入文本"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Enter the domain you want to use for sending emails (without http:// or www)"
msgstr "输入您希望用于发送邮件的域名(不含 http:// 或 www"
@@ -4523,6 +4554,7 @@ msgid "Event Type"
msgstr "事件类型"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
#: packages/ui/components/document/document-visibility-select.tsx
msgid "Everyone"
msgstr "所有人"
@@ -5222,14 +5254,6 @@ msgstr "必须填写姓名首字母缩写"
msgid "Initials Settings"
msgstr "姓名首字母设置"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Insert a value into the number field"
msgstr "在数字字段中输入一个值"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Insert a value into the text field"
msgstr "在文本字段中输入一个值"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Inserted"
msgstr "已插入"
@@ -5738,6 +5762,7 @@ msgid "Manager"
msgstr "管理者"
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: packages/lib/constants/document-visibility.ts
msgid "Managers and above"
msgstr "管理者及以上"
@@ -6133,6 +6158,10 @@ msgstr "别担心,这很常见!输入你的邮箱,我们会给你发送一
msgid "None"
msgstr "无"
#: packages/lib/constants/document-auth.ts
msgid "None (Overrides global settings)"
msgstr "无(覆盖全局设置)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -6696,6 +6725,10 @@ msgstr "请联系站点所有者以获取进一步帮助。"
msgid "Please enter a meaningful name for your token. This will help you identify it later."
msgstr "请输入一个有意义的令牌名称,以便日后识别。"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Please enter a number"
msgstr "请输入一个数字"
#: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -6706,6 +6739,22 @@ msgstr "请输入有效姓名。"
msgid "Please enter a valid number"
msgstr "请输入有效的数字"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Please enter a value"
msgstr "请输入一个值"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Please enter your email address"
msgstr "请输入您的邮箱地址"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Please enter your full name"
msgstr "请输入您的全名"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Please enter your initials"
msgstr "请输入您的姓名首字母缩写"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
msgid "Please mark as viewed to complete"
msgstr "请标记为已查看以完成"
@@ -7250,6 +7299,22 @@ msgstr "请求"
msgid "Requesting Organisation"
msgstr "请求的组织"
#: packages/lib/constants/document-auth.ts
msgid "Require 2FA"
msgstr "需要双重身份验证"
#: packages/lib/constants/document-auth.ts
msgid "Require account"
msgstr "需要账户"
#: packages/lib/constants/document-auth.ts
msgid "Require passkey"
msgstr "需要通行密钥"
#: packages/lib/constants/document-auth.ts
msgid "Require password"
msgstr "需要密码"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@@ -7662,6 +7727,10 @@ msgstr "选择要添加到此团队的成员"
msgid "Select Option"
msgstr "选择选项"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Select Options"
msgstr "选择选项"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey"
msgstr "选择通行密钥"
@@ -7884,13 +7953,7 @@ msgstr "显示高级设置"
msgid "Show templates in your public profile for your audience to sign and get started quickly"
msgstr "在公开资料中展示模板,方便你的用户快速签署上手"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
@@ -7924,10 +7987,6 @@ msgstr "以 {0} <0>{1}</0> 的身份签署"
msgid "Sign as<0>{0} <1>({1})</1></0>"
msgstr "以<0>{0} <1>{1}</1></0>的身份签署"
#: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx
msgid "Sign Checkbox Field"
msgstr "签署复选框字段"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -7944,10 +8003,6 @@ msgstr "签署文档"
msgid "Sign Documents"
msgstr "签署文档"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign Email"
msgstr "签署电子邮件字段"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Sign field"
@@ -7970,18 +8025,6 @@ msgstr "登录"
msgid "Sign in to your account"
msgstr "登录到你的账号"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign Initials"
msgstr "签署姓名首字母字段"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign Name"
msgstr "签署姓名字段"
#: apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
msgid "Sign Number Field"
msgstr "签署数字字段"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/menu-switcher.tsx
#: apps/remix/app/components/general/org-menu-switcher.tsx
@@ -7992,10 +8035,6 @@ msgstr "登出"
msgid "Sign Signature Field"
msgstr "签署签名字段"
#: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
msgid "Sign Text Field"
msgstr "签署文本字段"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@@ -8018,18 +8057,6 @@ msgstr "使用 Microsoft 注册"
msgid "Sign Up with OIDC"
msgstr "使用 OIDC 注册"
#: apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
msgid "Sign your email into the field"
msgstr "在字段中填写您的电子邮件地址"
#: apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
msgid "Sign your full name into the field"
msgstr "在字段中填写您的全名"
#: apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
msgid "Sign your initials into the field"
msgstr "在字段中填写您的姓名首字母缩写"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
+14
View File
@@ -0,0 +1,14 @@
import { RecipientRole } from '@prisma/client';
import { z } from 'zod';
export const ZDefaultRecipientSchema = z.object({
email: z.string().email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
});
export type TDefaultRecipient = z.infer<typeof ZDefaultRecipientSchema>;
export const ZDefaultRecipientsSchema = z.array(ZDefaultRecipientSchema);
export type TDefaultRecipients = z.infer<typeof ZDefaultRecipientsSchema>;
@@ -47,10 +47,13 @@ export const putPdfFileServerSide = async (file: File) => {
/**
* Uploads a pdf file and normalizes it.
*/
export const putNormalizedPdfFileServerSide = async (file: File) => {
export const putNormalizedPdfFileServerSide = async (
file: File,
options: { flattenForm?: boolean } = {},
) => {
const buffer = Buffer.from(await file.arrayBuffer());
const normalized = await normalizePdf(buffer);
const normalized = await normalizePdf(buffer, options);
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
+1
View File
@@ -137,6 +137,7 @@ export const generateDefaultOrganisationSettings = (): Omit<
// emailReplyToName: null,
emailDocumentSettings: DEFAULT_DOCUMENT_EMAIL_SETTINGS,
defaultRecipients: null,
aiFeaturesEnabled: false,
};
};
+1 -1
View File
@@ -7,6 +7,6 @@ export const extractInitials = (text: string) =>
.slice(0, 2)
.join('');
export const recipientAbbreviation = (recipient: Recipient) => {
export const recipientAbbreviation = (recipient: Pick<Recipient, 'name' | 'email'>) => {
return extractInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase();
};
+1
View File
@@ -204,6 +204,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
emailReplyTo: null,
// emailReplyToName: null,
defaultRecipients: null,
aiFeaturesEnabled: null,
};
};
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "OrganisationGlobalSettings" ADD COLUMN "defaultRecipients" JSONB;
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "defaultRecipients" JSONB;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX IF NOT EXISTS "DocumentAuditLog_envelopeId_idx" ON "DocumentAuditLog"("envelopeId");
+8 -2
View File
@@ -472,6 +472,8 @@ model DocumentAuditLog {
ipAddress String?
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
@@index([envelopeId])
}
enum DocumentDataType {
@@ -806,7 +808,7 @@ enum OrganisationMemberInviteStatus {
DECLINED
}
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';", "import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';"])
model OrganisationGlobalSettings {
id String @id
organisation Organisation?
@@ -824,6 +826,8 @@ model OrganisationGlobalSettings {
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
defaultRecipients Json? /// [DefaultRecipient[]] @zod.custom.use(ZDefaultRecipientsSchema)
emailId String?
email OrganisationEmail? @relation(fields: [emailId], references: [id])
@@ -840,7 +844,7 @@ model OrganisationGlobalSettings {
aiFeaturesEnabled Boolean @default(false)
}
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';", "import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';"])
model TeamGlobalSettings {
id String @id
team Team?
@@ -859,6 +863,8 @@ model TeamGlobalSettings {
uploadSignatureEnabled Boolean?
drawSignatureEnabled Boolean?
defaultRecipients Json? /// [DefaultRecipient[]] @zod.custom.use(ZDefaultRecipientsSchema)
emailId String?
email OrganisationEmail? @relation(fields: [emailId], references: [id])
+3
View File
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { TDefaultRecipient } from '@documenso/lib/types/default-recipients';
import type {
TDocumentAuthOptions,
TRecipientAuthOptions,
@@ -26,6 +27,8 @@ declare global {
type FieldMeta = TFieldMetaNotOptionalSchema;
type EnvelopeAttachmentType = TEnvelopeAttachmentType;
type DefaultRecipient = TDefaultRecipient;
}
}
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"@trpc/client": "11.8.1",
"@trpc/react-query": "11.8.1",
@@ -0,0 +1,66 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { parseDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZFindDocumentAuditLogsRequestSchema,
ZFindDocumentAuditLogsResponseSchema,
} from './find-document-audit-logs.types';
export const findDocumentAuditLogsRoute = adminProcedure
.input(ZFindDocumentAuditLogsRequestSchema)
.output(ZFindDocumentAuditLogsResponseSchema)
.query(async ({ input }) => {
const {
envelopeId,
page = 1,
perPage = 50,
orderByColumn = 'createdAt',
orderByDirection = 'desc',
} = input;
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
{
type: 'envelopeId',
id: envelopeId,
},
EnvelopeType.DOCUMENT,
),
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const [data, count] = await Promise.all([
prisma.documentAuditLog.findMany({
where: { envelopeId: envelope.id },
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
[orderByColumn]: orderByDirection,
},
}),
prisma.documentAuditLog.count({
where: { envelopeId: envelope.id },
}),
]);
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
return {
data: parsedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof parsedData>;
});
@@ -0,0 +1,17 @@
import { z } from 'zod';
import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZFindDocumentAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({
envelopeId: z.string(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).optional(),
});
export const ZFindDocumentAuditLogsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentAuditLogSchema.array(),
});
export type TFindDocumentAuditLogsRequest = z.infer<typeof ZFindDocumentAuditLogsRequestSchema>;
export type TFindDocumentAuditLogsResponse = z.infer<typeof ZFindDocumentAuditLogsResponseSchema>;
@@ -8,6 +8,7 @@ import { deleteUserRoute } from './delete-user';
import { disableUserRoute } from './disable-user';
import { enableUserRoute } from './enable-user';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
import { findDocumentJobsRoute } from './find-document-jobs';
import { findDocumentsRoute } from './find-documents';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
@@ -56,6 +57,7 @@ export const adminRouter = router({
delete: deleteDocumentRoute,
reseal: resealDocumentRoute,
findJobs: findDocumentJobsRoute,
findAuditLogs: findDocumentAuditLogsRoute,
},
recipient: {
update: updateRecipientRoute,
@@ -1,4 +1,4 @@
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
import type { RegistrationResponseJSON } from '@simplewebauthn/server';
import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
@@ -1,3 +1,5 @@
import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
@@ -80,11 +82,16 @@ export const createEnvelopeRoute = authenticatedProcedure
});
}
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
});
const { id: documentDataId } = await putNormalizedPdfFileServerSide(
{
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
},
{
flattenForm: type !== EnvelopeType.TEMPLATE,
},
);
return {
title: file.name,
@@ -34,6 +34,7 @@ export const useEnvelopeRoute = authenticatedProcedure
prefillFields,
override,
attachments,
formValues,
} = payload;
ctx.logger.info({
@@ -79,7 +80,10 @@ export const useEnvelopeRoute = authenticatedProcedure
// Process uploaded files and create document data for them
const uploadedFiles = await Promise.all(
filesToUpload.map(async (file) => {
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
// We disable flattening here since `createDocumentFromTemplate` will handle it.
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file, {
flattenForm: false,
});
return {
name: file.name,
@@ -146,6 +150,7 @@ export const useEnvelopeRoute = authenticatedProcedure
prefillFields,
override,
attachments,
formValues,
});
// Distribute document if requested
@@ -2,6 +2,7 @@ import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -108,6 +109,8 @@ export const ZUseEnvelopePayloadSchema = z.object({
}),
)
.optional(),
formValues: ZDocumentFormValuesSchema.optional(),
});
export const ZUseEnvelopeRequestSchema = zodFormData({
@@ -1,4 +1,4 @@
import { OrganisationType } from '@prisma/client';
import { OrganisationType, Prisma } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -36,6 +36,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
defaultRecipients,
delegateDocumentOwnership,
// Branding related settings.
@@ -145,6 +146,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
delegateDocumentOwnership: derivedDelegateDocumentOwnership,
// Branding related settings.
@@ -1,6 +1,7 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
@@ -22,6 +23,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
typedSignatureEnabled: z.boolean().optional(),
uploadSignatureEnabled: z.boolean().optional(),
drawSignatureEnabled: z.boolean().optional(),
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
delegateDocumentOwnership: z.boolean().nullish(),
// Branding related settings.
@@ -53,6 +53,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// emailReplyToName,
emailDocumentSettings,
// Default recipients settings.
defaultRecipients,
// AI features settings.
aiFeaturesEnabled,
} = data;
@@ -165,6 +167,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// emailReplyToName,
emailDocumentSettings:
emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
// AI features settings.
aiFeaturesEnabled,
@@ -1,6 +1,7 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
@@ -40,6 +41,8 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
// emailReplyToName: z.string().nullish(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
// Default recipients settings.
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
// AI features settings.
aiFeaturesEnabled: z.boolean().nullish(),
}),
@@ -197,7 +197,9 @@ export const templateRouter = router({
attachments,
} = payload;
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file);
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file, {
flattenForm: false,
});
ctx.logger.info({
input: {
@@ -468,6 +470,7 @@ export const templateRouter = router({
externalId,
override,
attachments,
formValues,
} = input;
ctx.logger.info({
@@ -507,6 +510,7 @@ export const templateRouter = router({
externalId,
override,
attachments,
formValues,
});
if (distributeDocument) {
@@ -8,6 +8,7 @@ import {
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -172,6 +173,8 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
}),
)
.optional(),
formValues: ZDocumentFormValuesSchema.optional(),
});
export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema;
@@ -1,4 +1,4 @@
import { Prisma, WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
import type { Prisma, WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';

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