Compare commits
127 Commits
v1.7.1-rc.
...
v1.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e7e9e17c9 | |||
| b3ccb3d26f | |||
| b17370c153 | |||
| 0c53f5b061 | |||
| ed6157de80 | |||
| 5e08d0cffb | |||
| 5565aff7a3 | |||
| 428acf4ac3 | |||
| f4b1e5104e | |||
| a687064a42 | |||
| 8ec69388a5 | |||
| f3da11b3e7 | |||
| fc84ee8ec2 | |||
| 4282a96ee7 | |||
| 2aae7435f8 | |||
| bdd33bd335 | |||
| 9e8d0ac906 | |||
| f27d0f342c | |||
| 4326e27a2a | |||
| 62806298cf | |||
| 87186e08b1 | |||
| b27fd800ed | |||
| 98d85b086d | |||
| 04293968c6 | |||
| e6d4005cd1 | |||
| 337bdb3553 | |||
| ab654a63d8 | |||
| dcb7c2436f | |||
| fa33f83696 | |||
| b15e1d6c47 | |||
| cd5adce7df | |||
| 11e483f1c4 | |||
| 2e2bc8382f | |||
| 1f3a9b578b | |||
| 83e7a3c222 | |||
| 9ef8b1f0c3 | |||
| 0eff336175 | |||
| 9bdd5c31cc | |||
| 57ad7c150b | |||
| b0829e6cdf | |||
| 08a446fefd | |||
| f15f9ecdd1 | |||
| 979e3f3e71 | |||
| 876803b5db | |||
| 1c87cb1e0d | |||
| 5398026b80 | |||
| f2439abbc9 | |||
| 5a6e031c90 | |||
| bcc3b70335 | |||
| 5a26610a01 | |||
| 5d7a979baf | |||
| 552825b79e | |||
| 786566bae4 | |||
| cb23357b42 | |||
| 0078162159 | |||
| 19e23d8ef3 | |||
| e3b7ec82a3 | |||
| 23a0537648 | |||
| f6bcf921d5 | |||
| 451723a8ab | |||
| 9b769e7e33 | |||
| 61ea4971ad | |||
| ffc61af904 | |||
| efbe94aea8 | |||
| 1b10c55758 | |||
| 3da4603a47 | |||
| dcc2ac8a71 | |||
| 5158584955 | |||
| 54c0c6be14 | |||
| 927a24249c | |||
| a50c758b07 | |||
| cc249357b3 | |||
| 011dabcc04 | |||
| 4fa6dc1e21 | |||
| 32b65c4d49 | |||
| de880aa821 | |||
| dc5723c386 | |||
| c57d1dc55d | |||
| 4dd95016b1 | |||
| 04b1ce1aab | |||
| 885349ad94 | |||
| 28514ba2e7 | |||
| 8aa6d8e602 | |||
| 378e515843 | |||
| f42e600e3f | |||
| 88eaec91c9 | |||
| f199183c78 | |||
| 0cee07aed3 | |||
| f76f87ff1c | |||
| 6020336792 | |||
| 634b30aa54 | |||
| 7fc497a642 | |||
| e30ceeb038 | |||
| 872762661a | |||
| 5fcd8610c9 | |||
| b8310237e4 | |||
| 4a6238dc52 | |||
| 6fa5f63b69 | |||
| c7564ba8f7 | |||
| eafd7c551b | |||
| 514edf01d3 | |||
| 1a73c68d07 | |||
| 1a9dcadba5 | |||
| e0c948c2ac | |||
| 0bd2760792 | |||
| abc559d923 | |||
| 9ffdbe9c81 | |||
| 2c1a18bafc | |||
| a2db5e9642 | |||
| 4ec9dc78c1 | |||
| faf2bd5384 | |||
| d40ed94b74 | |||
| cd3d9b701b | |||
| e40f47a73c | |||
| 64ea4a6f9f | |||
| 18115e95d7 | |||
| e736261056 | |||
| 2e57da7549 | |||
| 574454db0a | |||
| f05b670d93 | |||
| 318149fbf3 | |||
| 5f19dcf25c | |||
| c99cf4b848 | |||
| 18ec40f6af | |||
| ddee8a8272 | |||
| efb2bc94ab | |||
| 97ee69e7a0 |
@ -93,6 +93,8 @@ NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=
|
||||
NEXT_PRIVATE_SMTP_FROM_NAME="Documenso"
|
||||
# REQUIRED: Defines the email address to use as the from address.
|
||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||
# OPTIONAL: Defines the service for nodemailer
|
||||
NEXT_PRIVATE_SMTP_SERVICE=
|
||||
# OPTIONAL: The API key to use for Resend.com
|
||||
NEXT_PRIVATE_RESEND_API_KEY=
|
||||
# OPTIONAL: The API key to use for MailChannels.
|
||||
@ -137,3 +139,6 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
|
||||
# [[REDIS]]
|
||||
NEXT_PRIVATE_REDIS_URL=
|
||||
NEXT_PRIVATE_REDIS_TOKEN=
|
||||
|
||||
# [[LOGGER]]
|
||||
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY=
|
||||
|
||||
54
.github/workflows/publish.yml
vendored
@ -89,22 +89,35 @@ jobs:
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:latest \
|
||||
--amend documenso/documenso-amd64:latest \
|
||||
--amend documenso/documenso-arm64:latest \
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:latest \
|
||||
--amend documenso/documenso-amd64:latest \
|
||||
--amend documenso/documenso-arm64:latest
|
||||
|
||||
docker manifest push documenso/documenso:latest
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
documenso/documenso:rc \
|
||||
--amend documenso/documenso-amd64:rc \
|
||||
--amend documenso/documenso-arm64:rc
|
||||
|
||||
docker manifest push documenso/documenso:rc
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$GIT_SHA \
|
||||
--amend documenso/documenso-amd64:$GIT_SHA \
|
||||
--amend documenso/documenso-arm64:$GIT_SHA \
|
||||
--amend documenso/documenso-arm64:$GIT_SHA
|
||||
|
||||
docker manifest create \
|
||||
documenso/documenso:$APP_VERSION \
|
||||
--amend documenso/documenso-amd64:$APP_VERSION \
|
||||
--amend documenso/documenso-arm64:$APP_VERSION \
|
||||
--amend documenso/documenso-arm64:$APP_VERSION
|
||||
|
||||
docker manifest push documenso/documenso:latest
|
||||
docker manifest push documenso/documenso:$GIT_SHA
|
||||
docker manifest push documenso/documenso:$APP_VERSION
|
||||
|
||||
@ -113,21 +126,34 @@ jobs:
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:latest \
|
||||
--amend ghcr.io/documenso/documenso-amd64:latest \
|
||||
--amend ghcr.io/documenso/documenso-arm64:latest \
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:latest \
|
||||
--amend ghcr.io/documenso/documenso-amd64:latest \
|
||||
--amend ghcr.io/documenso/documenso-arm64:latest
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:latest
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:rc \
|
||||
--amend ghcr.io/documenso/documenso-amd64:rc \
|
||||
--amend ghcr.io/documenso/documenso-arm64:rc
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:rc
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$GIT_SHA \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$GIT_SHA \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$GIT_SHA \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$GIT_SHA
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/documenso/documenso:$APP_VERSION \
|
||||
--amend ghcr.io/documenso/documenso-amd64:$APP_VERSION \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$APP_VERSION \
|
||||
--amend ghcr.io/documenso/documenso-arm64:$APP_VERSION
|
||||
|
||||
docker manifest push ghcr.io/documenso/documenso:latest
|
||||
docker manifest push ghcr.io/documenso/documenso:$GIT_SHA
|
||||
docker manifest push ghcr.io/documenso/documenso:$APP_VERSION
|
||||
|
||||
38
.github/workflows/translations-extract.yml
vendored
@ -1,38 +0,0 @@
|
||||
# Extract and compile translations for all PRs.
|
||||
|
||||
name: 'Extract and compile translations'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and compile translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Extract and compile translations
|
||||
run: |
|
||||
npm run translate:extract
|
||||
npm run translate:compile
|
||||
|
||||
- name: Check and commit any files created
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@documenso.com'
|
||||
git add packages/lib/translations
|
||||
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
|
||||
8
.github/workflows/translations-upload.yml
vendored
@ -21,14 +21,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Extract and compile translations
|
||||
run: |
|
||||
npm run translate:extract
|
||||
npm run translate:compile
|
||||
- name: Extract translations
|
||||
run: npm run translate:extract
|
||||
|
||||
- name: Check and commit any files created
|
||||
run: |
|
||||
|
||||
@ -13,9 +13,4 @@ node "$MONOREPO_ROOT/scripts/copy-wellknown.cjs"
|
||||
git add "$MONOREPO_ROOT/apps/web/public/"
|
||||
git add "$MONOREPO_ROOT/apps/marketing/public/"
|
||||
|
||||
echo "Extract and compile translations"
|
||||
npm run translate:extract
|
||||
npm run translate:compile
|
||||
git add "$MONOREPO_ROOT/packages/lib/translations/"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
@ -27,9 +27,6 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"index": "Getting Started",
|
||||
"contributing-translations": "Contributing Translations"
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Contributing Translations
|
||||
description: Learn how to contribute translations to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Contributing Translations
|
||||
|
||||
We are always open for help with translations! Currently we utilise AI to generate the initial translations for new languages, which are then improved over time by our awesome community.
|
||||
|
||||
If you are looking for development notes on translations, you can find them [here](/developers/local-development/translations).
|
||||
|
||||
<Callout type="info">
|
||||
Contributions are made through GitHub Pull Requests, so you will need a GitHub account to
|
||||
contribute.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
We store our translations in PO files, which are located in our GitHub repository [here](https://github.com/documenso/documenso/tree/main/packages/lib/translations).
|
||||
|
||||
The translation files are organized into folders represented by their respective language codes (`en` for English, `de` for German, etc). Each language folder contains three PO files:
|
||||
|
||||
1. `web.po`: Translations for the web application
|
||||
2. `marketing.po`: Translations for the marketing application
|
||||
3. `common.po`: Shared translations between web and marketing
|
||||
|
||||
Each PO file contains translations which look like this:
|
||||
|
||||
```po
|
||||
#: apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
||||
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
||||
```
|
||||
|
||||
- `msgid`: The original text in English (never edit this manually)
|
||||
- `msgstr`: The translated text in the target language
|
||||
|
||||
<Callout type="warning">
|
||||
Notice the `<0>` tags? These represent HTML elements and must remain in both the `msgid` and `msgstr`. Make sure to translate the content between these tags while keeping the tags intact.
|
||||
</Callout>
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Updating Existing Translations
|
||||
|
||||
1. Fork the repository.
|
||||
2. Navigate to the appropriate language folder.
|
||||
3. Open the PO file you want to update (web.po, marketing.po, or common.po).
|
||||
4. Make your changes, ensuring you follow the PO file format.
|
||||
5. Commit your changes with a message such as `chore: update German translations`
|
||||
6. Create a Pull Request.
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
If you want to add translations for a language that doesn't exist yet:
|
||||
|
||||
1. Create an issue in our GitHub repository requesting the addition of the new language.
|
||||
2. Wait for our team to review and approve the request.
|
||||
3. Once approved, we will set up the necessary files and kickstart the translations with AI to provide initial coverage.
|
||||
|
||||
## Need Help?
|
||||
|
||||
<Callout type="info">
|
||||
If you have any questions, hop into our [Discord](https://documen.so/discord) and ask us directly!
|
||||
</Callout>
|
||||
|
||||
Thank you for helping make Documenso more accessible to users around the world!
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Contributing Guide
|
||||
title: Getting started
|
||||
description: Learn how to contribute to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
9
apps/documentation/pages/developers/embedding/_meta.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"index": "Get Started",
|
||||
"react": "React Integration",
|
||||
"vue": "Vue Integration",
|
||||
"svelte": "Svelte Integration",
|
||||
"solid": "Solid Integration",
|
||||
"preact": "Preact Integration",
|
||||
"css-variables": "CSS Variables"
|
||||
}
|
||||
120
apps/documentation/pages/developers/embedding/css-variables.mdx
Normal file
@ -0,0 +1,120 @@
|
||||
---
|
||||
title: CSS Variables
|
||||
description: Learn about all available CSS variables for customizing your embedded signing experience
|
||||
---
|
||||
|
||||
# CSS Variables
|
||||
|
||||
Platform customers have access to a comprehensive set of CSS variables that can be used to customize the appearance of the embedded signing experience. These variables control everything from colors to spacing and can be used to match your application's design system.
|
||||
|
||||
## Available Variables
|
||||
|
||||
### Colors
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ----------------------- | ---------------------------------- | -------------- |
|
||||
| `background` | Base background color | System default |
|
||||
| `foreground` | Base text color | System default |
|
||||
| `muted` | Muted/subtle background color | System default |
|
||||
| `mutedForeground` | Muted/subtle text color | System default |
|
||||
| `popover` | Popover/dropdown background color | System default |
|
||||
| `popoverForeground` | Popover/dropdown text color | System default |
|
||||
| `card` | Card background color | System default |
|
||||
| `cardBorder` | Card border color | System default |
|
||||
| `cardBorderTint` | Card border tint/highlight color | System default |
|
||||
| `cardForeground` | Card text color | System default |
|
||||
| `fieldCard` | Field card background color | System default |
|
||||
| `fieldCardBorder` | Field card border color | System default |
|
||||
| `fieldCardForeground` | Field card text color | System default |
|
||||
| `widget` | Widget background color | System default |
|
||||
| `widgetForeground` | Widget text color | System default |
|
||||
| `border` | Default border color | System default |
|
||||
| `input` | Input field border color | System default |
|
||||
| `primary` | Primary action/button color | System default |
|
||||
| `primaryForeground` | Primary action/button text color | System default |
|
||||
| `secondary` | Secondary action/button color | System default |
|
||||
| `secondaryForeground` | Secondary action/button text color | System default |
|
||||
| `accent` | Accent/highlight color | System default |
|
||||
| `accentForeground` | Accent/highlight text color | System default |
|
||||
| `destructive` | Destructive/danger action color | System default |
|
||||
| `destructiveForeground` | Destructive/danger text color | System default |
|
||||
| `ring` | Focus ring color | System default |
|
||||
| `warning` | Warning/alert color | System default |
|
||||
|
||||
### Spacing and Layout
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------- | ------------------------------- | -------------- |
|
||||
| `radius` | Border radius size in REM units | System default |
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's how to use these variables in your embedding implementation:
|
||||
|
||||
```jsx
|
||||
const cssVars = {
|
||||
// Colors
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
primary: '#0000ff',
|
||||
primaryForeground: '#ffffff',
|
||||
accent: '#4f46e5',
|
||||
destructive: '#ef4444',
|
||||
|
||||
// Spacing
|
||||
radius: '0.5rem'
|
||||
};
|
||||
|
||||
// React/Preact
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
cssVars={cssVars}
|
||||
/>
|
||||
|
||||
// Vue
|
||||
<EmbedDirectTemplate
|
||||
:token="token"
|
||||
:cssVars="cssVars"
|
||||
/>
|
||||
|
||||
// Svelte
|
||||
<EmbedDirectTemplate
|
||||
{token}
|
||||
cssVars={cssVars}
|
||||
/>
|
||||
|
||||
// Solid
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
cssVars={cssVars}
|
||||
/>
|
||||
```
|
||||
|
||||
## Color Format
|
||||
|
||||
Colors can be specified in any valid CSS color format:
|
||||
|
||||
- Hexadecimal: `#ff0000`
|
||||
- RGB: `rgb(255, 0, 0)`
|
||||
- HSL: `hsl(0, 100%, 50%)`
|
||||
- Named colors: `red`
|
||||
|
||||
The colors will be automatically converted to the appropriate format internally.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Maintain Contrast**: When customizing colors, ensure there's sufficient contrast between background and foreground colors for accessibility.
|
||||
|
||||
2. **Test Dark Mode**: If you haven't disabled dark mode, test your color variables in both light and dark modes.
|
||||
|
||||
3. **Use Your Brand Colors**: Align the primary and accent colors with your brand's color scheme for a cohesive look.
|
||||
|
||||
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
|
||||
|
||||
## Related
|
||||
|
||||
- [React Integration](/developers/embedding/react)
|
||||
- [Vue Integration](/developers/embedding/vue)
|
||||
- [Svelte Integration](/developers/embedding/svelte)
|
||||
- [Solid Integration](/developers/embedding/solid)
|
||||
- [Preact Integration](/developers/embedding/preact)
|
||||
@ -11,7 +11,11 @@ Our embedding feature lets you integrate our document signing experience into yo
|
||||
|
||||
Embedding is currently available for all users on a **Teams Plan** and above, as well as **Early Adopter's** within a team (Early Adopters can create a team for free).
|
||||
|
||||
In the future, we will roll out a **Platform Plan** that will offer additional enhancements for embedding, including the option to remove Documenso branding for a more customized experience.
|
||||
Our **Platform Plan** offers enhanced customization features including:
|
||||
|
||||
- Custom CSS and styling variables
|
||||
- Dark mode controls
|
||||
- The removal of Documenso branding from the embedding experience
|
||||
|
||||
## How Embedding Works
|
||||
|
||||
@ -22,6 +26,49 @@ Embedding with Documenso allows you to handle document signing in two main ways:
|
||||
|
||||
_For most use-cases we recommend using direct templates, however if you have a need for a more advanced integration, we are happy to help you get started._
|
||||
|
||||
## Customization Options
|
||||
|
||||
### Styling and Theming
|
||||
|
||||
Platform customers have access to advanced styling options to customize the embedding experience:
|
||||
|
||||
1. **Custom CSS**: You can provide custom CSS to style the embedded component:
|
||||
|
||||
```jsx
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
/>
|
||||
```
|
||||
|
||||
2. **CSS Variables**: Fine-tune the appearance using CSS variables for colors, spacing, and more:
|
||||
|
||||
```jsx
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
cssVars={{
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
For a complete list of available CSS variables and their usage, see our [CSS Variables](/developers/embedding/css-variables) documentation.
|
||||
|
||||
3. **Dark Mode Control**: Disable dark mode if it doesn't match your application's theme:
|
||||
|
||||
```jsx
|
||||
<EmbedDirectTemplate token={token} darkModeDisabled={true} />
|
||||
```
|
||||
|
||||
These customization options are available for both Direct Templates and Signing Token embeds.
|
||||
|
||||
## Supported Frameworks
|
||||
|
||||
We support embedding across a range of popular JavaScript frameworks, including:
|
||||
@ -120,12 +167,11 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
||||
|
||||
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
||||
|
||||
## Stay Tuned for the Platform Plan
|
||||
## Related
|
||||
|
||||
While embedding is already a powerful tool, we're working on a **Platform Plan** that will introduce even more functionality. This plan will offer:
|
||||
|
||||
- Additional customization options
|
||||
- The ability to remove Documenso branding
|
||||
- Additional controls for the signing experience
|
||||
|
||||
More details will be shared as we approach the release.
|
||||
- [React Integration](/developers/embedding/react)
|
||||
- [Vue Integration](/developers/embedding/vue)
|
||||
- [Svelte Integration](/developers/embedding/svelte)
|
||||
- [Solid Integration](/developers/embedding/solid)
|
||||
- [Preact Integration](/developers/embedding/preact)
|
||||
- [CSS Variables](/developers/embedding/css-variables)
|
||||
|
||||
@ -44,6 +44,9 @@ const MyEmbeddingComponent = () => {
|
||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| css | string (optional) | Custom CSS to style the embedded component (Platform Plan only) |
|
||||
| cssVars | object (optional) | CSS variables for customizing colors, spacing, etc. (Platform Plan only) |
|
||||
| darkModeDisabled | boolean (optional) | Disable dark mode functionality (Platform Plan only) |
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
@ -75,3 +78,30 @@ const MyEmbeddingComponent = () => {
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
|
||||
### Styling and Theming (Platform Plan)
|
||||
|
||||
Platform customers have access to advanced styling options:
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-preact';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'your-token';
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
|
||||
return (
|
||||
<EmbedDirectTemplate token={token} css={customCss} cssVars={cssVars} darkModeDisabled={true} />
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@ -44,6 +44,9 @@ const MyEmbeddingComponent = () => {
|
||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| css | string (optional) | Custom CSS to style the embedded component (Platform Plan only) |
|
||||
| cssVars | object (optional) | CSS variables for customizing colors, spacing, etc. (Platform Plan only) |
|
||||
| darkModeDisabled | boolean (optional) | Disable dark mode functionality (Platform Plan only) |
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
@ -75,3 +78,34 @@ const MyEmbeddingComponent = () => {
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
|
||||
### Styling and Theming (Platform Plan)
|
||||
|
||||
Platform customers have access to advanced styling options:
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
// Custom CSS
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
// CSS Variables
|
||||
cssVars={{
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
// Dark Mode Control
|
||||
darkModeDisabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@ -44,6 +44,9 @@ const MyEmbeddingComponent = () => {
|
||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| css | string (optional) | Custom CSS to style the embedded component (Platform Plan only) |
|
||||
| cssVars | object (optional) | CSS variables for customizing colors, spacing, etc. (Platform Plan only) |
|
||||
| darkModeDisabled | boolean (optional) | Disable dark mode functionality (Platform Plan only) |
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
@ -75,3 +78,30 @@ const MyEmbeddingComponent = () => {
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
|
||||
### Styling and Theming (Platform Plan)
|
||||
|
||||
Platform customers have access to advanced styling options:
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-solid';
|
||||
|
||||
const MyEmbeddingComponent = () => {
|
||||
const token = 'your-token';
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
|
||||
return (
|
||||
<EmbedDirectTemplate token={token} css={customCss} cssVars={cssVars} darkModeDisabled={true} />
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@ -46,6 +46,9 @@ If you have a direct link template, you can simply provide the token for the tem
|
||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| css | string (optional) | Custom CSS to style the embedded component (Platform Plan only) |
|
||||
| cssVars | object (optional) | CSS variables for customizing colors, spacing, etc. (Platform Plan only) |
|
||||
| darkModeDisabled | boolean (optional) | Disable dark mode functionality (Platform Plan only) |
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
@ -77,3 +80,28 @@ const MyEmbeddingComponent = () => {
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
|
||||
### Styling and Theming (Platform Plan)
|
||||
|
||||
Platform customers have access to advanced styling options:
|
||||
|
||||
```html
|
||||
<script lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-svelte';
|
||||
|
||||
const token = 'your-token';
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
<EmbedDirectTemplate {token} css="{customCss}" cssVars="{cssVars}" darkModeDisabled="{true}" />
|
||||
```
|
||||
|
||||
@ -46,6 +46,9 @@ If you have a direct link template, you can simply provide the token for the tem
|
||||
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||
| externalId | string (optional) | The external ID to be used for the document that will be created upon completion |
|
||||
| css | string (optional) | Custom CSS to style the embedded component (Platform Plan only) |
|
||||
| cssVars | object (optional) | CSS variables for customizing colors, spacing, etc. (Platform Plan only) |
|
||||
| darkModeDisabled | boolean (optional) | Disable dark mode functionality (Platform Plan only) |
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
@ -77,3 +80,35 @@ const MyEmbeddingComponent = () => {
|
||||
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||
|
||||
### Styling and Theming (Platform Plan)
|
||||
|
||||
Platform customers have access to advanced styling options:
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-vue';
|
||||
|
||||
const token = ref('your-token');
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedDirectTemplate
|
||||
:token="token"
|
||||
:css="customCss"
|
||||
:cssVars="cssVars"
|
||||
:darkModeDisabled="true"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
@ -11,6 +11,10 @@ Digitally signing documents requires a signing certificate in `.p12` format. You
|
||||
|
||||
Follow the steps below to create a free, self-signed certificate for local development.
|
||||
|
||||
<Callout type="warning">
|
||||
These steps should be run on a UNIX based system, otherwise you may run into an error.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
|
||||
### Generate Private Key
|
||||
@ -38,11 +42,17 @@ You will be prompted to enter some information, such as the certificate's Common
|
||||
Combine the private key and the self-signed certificate to create a `.p12` certificate. Use the following command:
|
||||
|
||||
```bash
|
||||
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt
|
||||
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -legacy
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
If you get the error "Error: Failed to get private key bags", add the `-legacy` flag to the command `openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -legacy`.
|
||||
When running the application in Docker, you may encounter permission issues when attempting to sign documents using your certificate (.p12) file. This happens because the application runs as a non-root user inside the container and needs read access to the certificate.
|
||||
|
||||
To resolve this, you'll need to update the certificate file permissions to allow the container user 1001, which runs NextJS, to read it:
|
||||
|
||||
```bash
|
||||
sudo chown 1001 certificate.p12
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
@ -54,8 +64,8 @@ Note that for local development, the password can be left empty.
|
||||
|
||||
### Add Certificate to the Project
|
||||
|
||||
Finally, add the certificate to the project. Place the `certificate.p12` file in the `/apps/web/resources` directory. If the directory doesn't exist, create it.
|
||||
Use the `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` environment variable to point at the certificate you created.
|
||||
|
||||
The final file path should be `/apps/web/resources/certificate.p12`.
|
||||
Details about environment variables associated with certificates can be found [here](/developers/self-hosting/signing-certificate#configure-documenso-to-use-the-certificate).
|
||||
|
||||
</Steps>
|
||||
|
||||
534
apps/documentation/pages/developers/public-api/reference.mdx
Normal file
@ -0,0 +1,534 @@
|
||||
---
|
||||
title: API Reference
|
||||
description: Reference documentation for the Documenso public API.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# API Reference
|
||||
|
||||
The Swagger UI for the API is available at [/api/v1/openapi](https://app.documenso.com/api/v1/openapi). This page provides detailed information about the API endpoints, request and response formats, and authentication requirements.
|
||||
|
||||
## Upload a Document
|
||||
|
||||
Uploading a document to your Documenso account requires a two-step process.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Create Document
|
||||
|
||||
First, you need to make a `POST` request to the `/api/v1/documents` endpoint, which takes a JSON payload with the following fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"externalId": "string",
|
||||
"recipients": [
|
||||
{
|
||||
"name": "string",
|
||||
"email": "user@example.com",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 0
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"subject": "string",
|
||||
"message": "string",
|
||||
"timezone": "Etc/UTC",
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": "string",
|
||||
"signingOrder": "PARALLEL"
|
||||
},
|
||||
"authOptions": {
|
||||
"globalAccessAuth": "ACCOUNT",
|
||||
"globalActionAuth": "ACCOUNT"
|
||||
},
|
||||
"formValues": {
|
||||
"additionalProp1": "string",
|
||||
"additionalProp2": "string",
|
||||
"additionalProp3": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `title` _(required)_ - This represents the document's title.
|
||||
- `externalId` - This is an optional field that you can use to store an external identifier for the document. This can be useful for tracking the document in your system.
|
||||
- `recipients` _(required)_ - This is an array of recipient objects. Each recipient object has the following fields:
|
||||
- `name` - The name of the recipient.
|
||||
- `email` - The email address of the recipient.
|
||||
- `role` - The role of the recipient. See the [available roles](/users/signing-documents#roles).
|
||||
- `signingOrder` - The order in which the recipient should sign the document. This is an integer value starting from 0.
|
||||
- `meta` - This object contains additional metadata for the document. It has the following fields:
|
||||
- `subject` - The subject of the email that will be sent to the recipients.
|
||||
- `message` - The message of the email that will be sent to the recipients.
|
||||
- `timezone` - The timezone in which the document should be signed.
|
||||
- `dateFormat` - The date format that should be used in the document.
|
||||
- `redirectUrl` - The URL to which the user should be redirected after signing the document.
|
||||
- `signingOrder` - The signing order for the document. This can be either `SEQUENTIAL` or `PARALLEL`.
|
||||
- `authOptions` - This object contains authentication options for the document. It has the following fields:
|
||||
- `globalAccessAuth` - The authentication level required to access the document. This can be either `ACCOUNT` or `null`.
|
||||
- If the document is set to `ACCOUNT`, all recipients must authenticate with their Documenso account to access it.
|
||||
- The document can be accessed without a Documenso account if it's set to `null`.
|
||||
- `globalActionAuth` - The authentication level required to perform actions on the document. This can be `ACCOUNT`, `PASSKEY`, `TWO_FACTOR_AUTH`, or `null`.
|
||||
- If the document is set to `ACCOUNT`, all recipients must authenticate with their Documenso account to perform actions on the document.
|
||||
- If it's set to `PASSKEY`, all recipients must have the passkey active to perform actions on the document.
|
||||
- If it's set to `TWO_FACTOR_AUTH`, all recipients must have the two-factor authentication active to perform actions on the document.
|
||||
- If it's set to `null`, all the recipients can perform actions on the document without any authentication.
|
||||
- `formValues` - This object contains additional form values for the document. This property only works with native PDF fields and accepts three types: number, text and boolean.
|
||||
|
||||
<Callout type="info">
|
||||
The `globalActionAuth` property is only available for Enterprise accounts.
|
||||
</Callout>
|
||||
|
||||
Here's an example of the JSON payload for uploading a document:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "my-document.pdf",
|
||||
"externalId": "12345",
|
||||
"recipients": [
|
||||
{
|
||||
"name": "Alex Blake",
|
||||
"email": "alexblake@email.com",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1
|
||||
},
|
||||
{
|
||||
"name": "Ash Drew",
|
||||
"email": "ashdrew@email.com",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 0
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"subject": "Sign the document",
|
||||
"message": "Hey there, please sign this document.",
|
||||
"timezone": "Europe/London",
|
||||
"dateFormat": "Day, Month Year",
|
||||
"redirectUrl": "https://mysite.com/welcome",
|
||||
"signingOrder": "SEQUENTIAL"
|
||||
},
|
||||
"authOptions": {
|
||||
"globalAccessAuth": "ACCOUNT",
|
||||
"globalActionAuth": "PASSKEY"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Upload to S3
|
||||
|
||||
A successful API call to the `/api/v1/documents` endpoint returns a JSON response containing the upload URL, document ID, and recipient information.
|
||||
|
||||
The upload URL is a pre-signed S3 URL that you can use to upload the document to the Documenso (or your) S3 bucket. You need to make a `PUT` request to this URL to upload the document.
|
||||
|
||||
```json
|
||||
{
|
||||
"uploadUrl": "https://<url>/<bucket-name>/<id>/my-document.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credentials>&X-Amz-Date=<date>&X-Amz-Expires=3600&X-Amz-Signature=<signature>&X-Amz-SignedHeaders=host&x-id=PutObject",
|
||||
"documentId": 51,
|
||||
"recipients": [
|
||||
{
|
||||
"recipientId": 11,
|
||||
"name": "Alex Blake",
|
||||
"email": "alexblake@email.com",
|
||||
"token": "<unique-signer-token>",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1,
|
||||
"signingUrl": "https://app.documenso.com/sign/<unique-signer-token>"
|
||||
},
|
||||
{
|
||||
"recipientId": 12,
|
||||
"name": "Ash Drew",
|
||||
"email": "ashdrew@email.com",
|
||||
"token": "<unique-signer-token>",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 0,
|
||||
"signingUrl": "https://app.documenso.com/sign/<unique-signer-token>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When you make the `PUT` request to the pre-signed URL, you need to include the document file you want to upload. The image below shows how to upload a document to the S3 bucket via Postman.
|
||||
|
||||

|
||||
|
||||
Here's an example of how to upload a document using cURL:
|
||||
|
||||
```bash
|
||||
curl --location --request PUT 'https://<url>/<bucket-name>/<id>/my-document.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credentials>&X-Amz-Date=<date>&X-Amz-Expires=3600&X-Amz-Signature=<signature>&X-Amz-SignedHeaders=host&x-id=PutObject' \
|
||||
--form '=@"/Users/my-user/Documents/documenso.pdf"'
|
||||
```
|
||||
|
||||
Once the document is successfully uploaded, you can access it in your Documenso account dashboard. The screenshot below shows the document that was uploaded via the API.
|
||||
|
||||

|
||||
|
||||
</Steps>
|
||||
|
||||
## Generate Document From Template
|
||||
|
||||
Documenso allows you to generate documents from templates. This is useful when you have a standard document format you want to reuse.
|
||||
|
||||
The API endpoint for generating a document from a template is `/api/v1/templates/{templateId}/generate-document`, and it takes a JSON payload with the following fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"externalId": "string",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"email": "user@example.com",
|
||||
"signingOrder": 0
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"subject": "string",
|
||||
"message": "string",
|
||||
"timezone": "string",
|
||||
"dateFormat": "string",
|
||||
"redirectUrl": "string",
|
||||
"signingOrder": "PARALLEL"
|
||||
},
|
||||
"authOptions": {
|
||||
"globalAccessAuth": "ACCOUNT",
|
||||
"globalActionAuth": "ACCOUNT"
|
||||
},
|
||||
"formValues": {
|
||||
"additionalProp1": "string",
|
||||
"additionalProp2": "string",
|
||||
"additionalProp3": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The JSON payload is identical to the payload for uploading a document, so you can read more about the fields in the [Create Document](/developers/public-api/reference#create-document) step. For this API endpoint, the `recipients` property is required.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Grab the Template ID
|
||||
|
||||
The first step is to retrieve the template ID from the Documenso dashboard. You can find the template ID in the URL by navigating to the template details page.
|
||||
|
||||

|
||||
|
||||
In this case, the template ID is "99999".
|
||||
|
||||
### Retrieve the Recipient(s) ID(s)
|
||||
|
||||
Once you have the template ID, the next step involves retrieving the ID(s) of the recipient(s) from the template. You can do this by making a GET request to `/api/v1/templates/{template-id}`.
|
||||
|
||||
A successful response looks as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 0,
|
||||
"externalId": "string",
|
||||
"type": "PUBLIC",
|
||||
"title": "string",
|
||||
"userId": 0,
|
||||
"teamId": 0,
|
||||
"templateDocumentDataId": "string",
|
||||
"createdAt": "2024-10-11T08:46:58.247Z",
|
||||
"updatedAt": "2024-10-11T08:46:58.247Z",
|
||||
"templateMeta": {
|
||||
"id": "string",
|
||||
"subject": "string",
|
||||
"message": "string",
|
||||
"timezone": "string",
|
||||
"dateFormat": "string",
|
||||
"templateId": 0,
|
||||
"redirectUrl": "string",
|
||||
"signingOrder": "PARALLEL"
|
||||
},
|
||||
"directLink": {
|
||||
"token": "string",
|
||||
"enabled": true
|
||||
},
|
||||
"templateDocumentData": {
|
||||
"id": "string",
|
||||
"type": "S3_PATH",
|
||||
"data": "string"
|
||||
},
|
||||
"Field": [
|
||||
{
|
||||
"id": 0,
|
||||
"recipientId": 0,
|
||||
"type": "SIGNATURE",
|
||||
"page": 0,
|
||||
"positionX": "string",
|
||||
"positionY": "string",
|
||||
"width": "string",
|
||||
"height": "string"
|
||||
}
|
||||
],
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 0,
|
||||
"email": "user@example.com",
|
||||
"name": "string",
|
||||
"signingOrder": 0,
|
||||
"authOptions": "string",
|
||||
"role": "CC"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You'll need the recipient(s) ID(s) for the next step.
|
||||
|
||||
### Generate the Document
|
||||
|
||||
To generate a document from the template, you need to make a POST request to the `/api/v1/templates/{template-id}/generate-document` endpoint.
|
||||
|
||||
At the minimum, you must provide the `recipients` array in the JSON payload. Here's an example of the JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"recipients": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Ash Drew",
|
||||
"email": "ashdrew@email.com",
|
||||
"signingOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Filling the `recipients` array with the corresponding recipient for each template placeholder recipient is recommended. For example, if the template has two placeholders, you should provide at least two recipients in the `recipients` array. Otherwise, the document will be sent to inexistent recipients such as `<recipient.1@documenso.com>`. However, the recipients can always be edited via the API or the web app.
|
||||
|
||||
A successful response will contain the document ID and recipient(s) information.
|
||||
|
||||
```json
|
||||
{
|
||||
"documentId": 999,
|
||||
"recipients": [
|
||||
{
|
||||
"recipientId": 0,
|
||||
"name": "Ash Drew",
|
||||
"email": "ashdrew@email.com",
|
||||
"token": "<signing-token>",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": null,
|
||||
"signingUrl": "https://app.documenso.com/sign/<signing-token>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can now access the document in your Documenso account dashboard. The screenshot below shows the document that was generated from the template.
|
||||
|
||||

|
||||
|
||||
</Steps>
|
||||
|
||||
## Add Fields to Document
|
||||
|
||||
The API allows you to add fields to a document via the `/api/v1/documents/{documentId}/fields` endpoint. This is useful when you want to add fields to a document before sending it to recipients.
|
||||
|
||||
To add fields to a document, you need to make a `POST` request with a JSON payload containing the field(s) information.
|
||||
|
||||
```json
|
||||
{
|
||||
"recipientId": 0,
|
||||
"type": "SIGNATURE",
|
||||
"pageNumber": 0,
|
||||
"pageX": 0,
|
||||
"pageY": 0,
|
||||
"pageWidth": 0,
|
||||
"pageHeight": 0,
|
||||
"fieldMeta": {
|
||||
"label": "string",
|
||||
"placeholder": "string",
|
||||
"required": true,
|
||||
"readOnly": true,
|
||||
"type": "text",
|
||||
"text": "string",
|
||||
"characterLimit": 0
|
||||
}
|
||||
}
|
||||
|
||||
// or
|
||||
|
||||
[
|
||||
{
|
||||
"recipientId": 0,
|
||||
"type": "SIGNATURE",
|
||||
"pageNumber": 0,
|
||||
"pageX": 0,
|
||||
"pageY": 0,
|
||||
"pageWidth": 0,
|
||||
"pageHeight": 0
|
||||
},
|
||||
{
|
||||
"recipientId": 0,
|
||||
"type": "TEXT",
|
||||
"pageNumber": 0,
|
||||
"pageX": 0,
|
||||
"pageY": 0,
|
||||
"pageWidth": 0,
|
||||
"pageHeight": 0,
|
||||
"fieldMeta": {
|
||||
"label": "string",
|
||||
"placeholder": "string",
|
||||
"required": true,
|
||||
"readOnly": true,
|
||||
"type": "text",
|
||||
"text": "string",
|
||||
"characterLimit": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<Callout type="info">This endpoint accepts either one field or an array of fields.</Callout>
|
||||
|
||||
Before adding fields to a document, you need each recipient's ID. If the document already has recipients, you can query the document to retrieve the recipient's details. If the document has no recipients, you need to add a recipient via the UI or API before adding a field.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Retrieve the Recipient(s) ID(s)
|
||||
|
||||
Perform a `GET` request to the `/api/v1/documents/{id}` to retrieve the details of a specific document, including the recipient's information.
|
||||
|
||||
An example response would look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 137,
|
||||
"externalId": null,
|
||||
"userId": 3,
|
||||
"teamId": null,
|
||||
"title": "documenso.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "<document-data-id>",
|
||||
"createdAt": "2024-10-11T12:29:12.725Z",
|
||||
"updatedAt": "2024-10-11T12:29:12.725Z",
|
||||
"completedAt": null,
|
||||
"recipients": [
|
||||
{
|
||||
"id": 55,
|
||||
"documentId": 137,
|
||||
"email": "ashdrew@email.com",
|
||||
"name": "Ash Drew",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": null,
|
||||
"token": "<signing-token>",
|
||||
"signedAt": null,
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT",
|
||||
"signingUrl": "https://app.documenso.com/sign/<signing-token>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
From this response, you'll only need the recipient ID, which is `55` in this case.
|
||||
|
||||
### (OR) Add a Recipient
|
||||
|
||||
If the document doesn't already have recipient(s), you can add recipient(s) via the API. Make a `POST` request to the `/api/v1/documents/{documentId}/recipients` endpoint with the recipient information. This endpoint takes the following JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"email": "user@example.com",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 0,
|
||||
"authOptions": {
|
||||
"actionAuth": "ACCOUNT"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">The `authOptions` property is only available for Enterprise accounts.</Callout>
|
||||
|
||||
Here's an example of the JSON payload for adding a recipient:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Ash Drew",
|
||||
"email": "ashdrew@email.com",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 0
|
||||
}
|
||||
```
|
||||
|
||||
A successful request will return a JSON response with the newly added recipient. You can now use the recipient ID to add fields to the document.
|
||||
|
||||
### Add Field(s)
|
||||
|
||||
Now you can make a `POST` request to the `/api/v1/documents/{documentId}/fields` endpoint with the field(s) information. Here's an example:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"recipientId": 55,
|
||||
"type": "SIGNATURE",
|
||||
"pageNumber": 1,
|
||||
"pageX": 50,
|
||||
"pageY": 20,
|
||||
"pageWidth": 25,
|
||||
"pageHeight": 5
|
||||
},
|
||||
{
|
||||
"recipientId": 55,
|
||||
"type": "TEXT",
|
||||
"pageNumber": 1,
|
||||
"pageX": 20,
|
||||
"pageY": 50,
|
||||
"pageWidth": 30,
|
||||
"pageHeight": 7.5,
|
||||
"fieldMeta": {
|
||||
"label": "Address",
|
||||
"placeholder": "32 New York Street, 41241",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"type": "text",
|
||||
"text": "32 New York Street, 41241",
|
||||
"characterLimit": 40
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The `text` field represents the default value of the field. If the user doesn't provide any other
|
||||
value, this is the value that will be used to sign the field.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning">
|
||||
It's important to pass the `type` in the `fieldMeta` property for the advanced fields. [Read more
|
||||
here](#a-note-on-advanced-fields)
|
||||
</Callout>
|
||||
|
||||
A successful request will return a JSON response with the newly added fields. The image below illustrates the fields added to the document via the API.
|
||||
|
||||

|
||||
|
||||
</Steps>
|
||||
|
||||
#### A Note on Advanced Fields
|
||||
|
||||
The advanced fields are: text, checkbox, radio, number, and select. Whenever you append any of these advanced fields to a document, you need to pass the `type` in the `fieldMeta` property:
|
||||
|
||||
```json
|
||||
...
|
||||
"fieldMeta": {
|
||||
"type": "text",
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Replace the `text` value with the corresponding field type:
|
||||
|
||||
- For the `TEXT` field it should be `text`.
|
||||
- For the `CHECKBOX` field it should be `checkbox`.
|
||||
- For the `RADIO` field it should be `radio`.
|
||||
- For the `NUMBER` field it should be `number`.
|
||||
- For the `SELECT` field it should be `select`. (check this before merge)
|
||||
|
||||
You must pass this property at all times, even if you don't need to set any other properties. If you don't, the endpoint will throw an error.
|
||||
@ -133,7 +133,7 @@ volumes:
|
||||
After updating the volume binding, save the `compose.yml` file and run the following command to start the containers:
|
||||
|
||||
```bash
|
||||
docker-compose --env-file ./.env -d up
|
||||
docker-compose --env-file ./.env up -d
|
||||
```
|
||||
|
||||
The command will start the PostgreSQL database and the Documenso application containers.
|
||||
|
||||
@ -20,6 +20,7 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
|
||||
- `document.opened`
|
||||
- `document.signed`
|
||||
- `document.completed`
|
||||
- `document.rejected`
|
||||
|
||||
## Create a webhook subscription
|
||||
|
||||
@ -36,7 +37,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
|
||||
To create a new webhook subscription, you need to provide the following information:
|
||||
|
||||
- Enter the webhook URL that will receive the event payload.
|
||||
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`.
|
||||
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`.
|
||||
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
|
||||
|
||||

|
||||
@ -53,45 +54,55 @@ You can edit or delete your webhook subscriptions by clicking the "**Edit**" or
|
||||
|
||||
The payload sent to the webhook URL contains the following fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------------------------------------- | --------- | ---------------------------------------------------- |
|
||||
| `event` | string | The type of event that triggered the webhook. |
|
||||
| `payload.id` | number | The id of the document. |
|
||||
| `payload.userId` | number | The id of the user who owns the document. |
|
||||
| `payload.authOptions` | json? | Authentication options for the document. |
|
||||
| `payload.formValues` | json? | Form values for the document. |
|
||||
| `payload.title` | string | The name of the document. |
|
||||
| `payload.status` | string | The current status of the document. |
|
||||
| `payload.documentDataId` | string | The identifier for the document data. |
|
||||
| `payload.createdAt` | datetime | The creation date and time of the document. |
|
||||
| `payload.updatedAt` | datetime | The last update date and time of the document. |
|
||||
| `payload.completedAt` | datetime? | The completion date and time of the document. |
|
||||
| `payload.deletedAt` | datetime? | The deletion date and time of the document. |
|
||||
| `payload.teamId` | number? | The id of the team. |
|
||||
| `payload.documentData.id` | string | The id of the document data. |
|
||||
| `payload.documentData.type` | string | The type of the document data. |
|
||||
| `payload.documentData.data` | string | The data of the document. |
|
||||
| `payload.documentData.initialData` | string | The initial data of the document. |
|
||||
| `payload.Recipient[].id` | number | The id of the recipient. |
|
||||
| `payload.Recipient[].documentId` | number? | The id the document associated with the recipient. |
|
||||
| `payload.Recipient[].templateId` | number? | The template identifier for the recipient. |
|
||||
| `payload.Recipient[].email` | string | The email address of the recipient. |
|
||||
| `payload.Recipient[].name` | string | The name of the recipient. |
|
||||
| `payload.Recipient[].token` | string | The token associated with the recipient. |
|
||||
| `payload.Recipient[].expired` | datetime? | The expiration status of the recipient. |
|
||||
| `payload.Recipient[].signedAt` | datetime? | The date and time the recipient signed the document. |
|
||||
| `payload.Recipient[].authOptions.accessAuth` | json? | Access authentication options. |
|
||||
| `payload.Recipient[].authOptions.actionAuth` | json? | Action authentication options. |
|
||||
| `payload.Recipient[].role` | string | The role of the recipient. |
|
||||
| `payload.Recipient[].readStatus` | string | The read status of the document by the recipient. |
|
||||
| `payload.Recipient[].signingStatus` | string | The signing status of the recipient. |
|
||||
| `payload.Recipient[].sendStatus` | string | The send status of the document to the recipient. |
|
||||
| `createdAt` | datetime | The creation date and time of the webhook event. |
|
||||
| `webhookEndpoint` | string | The endpoint URL where the webhook is sent. |
|
||||
|
||||
## Webhook event payload example
|
||||
|
||||
When an event that you have subscribed to occurs, Documenso will send a POST request to the specified webhook URL with a payload containing information about the event.
|
||||
| Field | Type | Description |
|
||||
| -------------------------------------------- | --------- | ----------------------------------------------------- |
|
||||
| `event` | string | The type of event that triggered the webhook. |
|
||||
| `payload.id` | number | The id of the document. |
|
||||
| `payload.externalId` | string? | External identifier for the document. |
|
||||
| `payload.userId` | number | The id of the user who owns the document. |
|
||||
| `payload.authOptions` | json? | Authentication options for the document. |
|
||||
| `payload.formValues` | json? | Form values for the document. |
|
||||
| `payload.visibility` | string | Document visibility (e.g., EVERYONE). |
|
||||
| `payload.title` | string | The title of the document. |
|
||||
| `payload.status` | string | The current status of the document. |
|
||||
| `payload.documentDataId` | string | The identifier for the document data. |
|
||||
| `payload.createdAt` | datetime | The creation date and time of the document. |
|
||||
| `payload.updatedAt` | datetime | The last update date and time of the document. |
|
||||
| `payload.completedAt` | datetime? | The completion date and time of the document. |
|
||||
| `payload.deletedAt` | datetime? | The deletion date and time of the document. |
|
||||
| `payload.teamId` | number? | The id of the team if document belongs to a team. |
|
||||
| `payload.templateId` | number? | The id of the template if created from template. |
|
||||
| `payload.source` | string | The source of the document (e.g., DOCUMENT, TEMPLATE) |
|
||||
| `payload.documentMeta.id` | string | The id of the document metadata. |
|
||||
| `payload.documentMeta.subject` | string? | The subject of the document. |
|
||||
| `payload.documentMeta.message` | string? | The message associated with the document. |
|
||||
| `payload.documentMeta.timezone` | string | The timezone setting for the document. |
|
||||
| `payload.documentMeta.password` | string? | The password protection if set. |
|
||||
| `payload.documentMeta.dateFormat` | string | The date format used in the document. |
|
||||
| `payload.documentMeta.redirectUrl` | string? | The URL to redirect after signing. |
|
||||
| `payload.documentMeta.signingOrder` | string | The signing order (e.g., PARALLEL, SEQUENTIAL). |
|
||||
| `payload.documentMeta.typedSignatureEnabled` | boolean | Whether typed signatures are enabled. |
|
||||
| `payload.documentMeta.language` | string | The language of the document. |
|
||||
| `payload.documentMeta.distributionMethod` | string | The method of distributing the document. |
|
||||
| `payload.documentMeta.emailSettings` | json? | Email notification settings. |
|
||||
| `payload.Recipient[].id` | number | The id of the recipient. |
|
||||
| `payload.Recipient[].documentId` | number? | The id of the document for this recipient. |
|
||||
| `payload.Recipient[].templateId` | number? | The template id if from a template. |
|
||||
| `payload.Recipient[].email` | string | The email address of the recipient. |
|
||||
| `payload.Recipient[].name` | string | The name of the recipient. |
|
||||
| `payload.Recipient[].token` | string | The unique token for this recipient. |
|
||||
| `payload.Recipient[].documentDeletedAt` | datetime? | When the document was deleted for this recipient. |
|
||||
| `payload.Recipient[].expired` | datetime? | When the recipient's access expired. |
|
||||
| `payload.Recipient[].signedAt` | datetime? | When the recipient signed the document. |
|
||||
| `payload.Recipient[].authOptions` | json? | Authentication options for this recipient. |
|
||||
| `payload.Recipient[].signingOrder` | number? | The order in which this recipient should sign. |
|
||||
| `payload.Recipient[].rejectionReason` | string? | The reason if the recipient rejected the document. |
|
||||
| `payload.Recipient[].role` | string | The role of the recipient (e.g., SIGNER, VIEWER). |
|
||||
| `payload.Recipient[].readStatus` | string | Whether the recipient has read the document. |
|
||||
| `payload.Recipient[].signingStatus` | string | The signing status of this recipient. |
|
||||
| `payload.Recipient[].sendStatus` | string | The sending status for this recipient. |
|
||||
| `createdAt` | datetime | The creation date and time of the webhook event. |
|
||||
| `webhookEndpoint` | string | The endpoint URL where the webhook is sent. |
|
||||
|
||||
## Example payloads
|
||||
|
||||
@ -104,9 +115,11 @@ Example payload for the `document.created` event:
|
||||
"event": "DOCUMENT_CREATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
@ -114,7 +127,43 @@ Example payload for the `document.created` event:
|
||||
"updatedAt": "2024-04-22T11:44:43.341Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:44:44.779Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
@ -128,9 +177,11 @@ Example payload for the `document.sent` event:
|
||||
"event": "DOCUMENT_SENT",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
@ -139,6 +190,22 @@ Example payload for the `document.sent` event:
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
@ -147,12 +214,12 @@ Example payload for the `document.sent` event:
|
||||
"email": "signer2@documenso.com",
|
||||
"name": "Signer 2",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "VIEWER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
@ -165,12 +232,12 @@ Example payload for the `document.sent` event:
|
||||
"email": "signer1@documenso.com",
|
||||
"name": "Signer 1",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"authOptions": null,
|
||||
"signingOrder": 2,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
@ -190,9 +257,11 @@ Example payload for the `document.opened` event:
|
||||
"event": "DOCUMENT_OPENED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
@ -201,6 +270,22 @@ Example payload for the `document.opened` event:
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
@ -209,24 +294,18 @@ Example payload for the `document.opened` event:
|
||||
"email": "signer2@documenso.com",
|
||||
"name": "Signer 2",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "VIEWER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
],
|
||||
"documentData": {
|
||||
"id": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"type": "S3_PATH",
|
||||
"data": "9753/xzqrshtlpokm/documenso.pdf",
|
||||
"initialData": "9753/xzqrshtlpokm/documenso.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:50:26.174Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
@ -240,9 +319,11 @@ Example payload for the `document.signed` event:
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "COMPLETED",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
@ -251,6 +332,22 @@ Example payload for the `document.signed` event:
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 51,
|
||||
@ -259,12 +356,15 @@ Example payload for the `document.signed` event:
|
||||
"email": "signer1@documenso.com",
|
||||
"name": "Signer 1",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
@ -284,9 +384,11 @@ Example payload for the `document.completed` event:
|
||||
"event": "DOCUMENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "COMPLETED",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
@ -295,11 +397,21 @@ Example payload for the `document.completed` event:
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"documentData": {
|
||||
"id": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"type": "S3_PATH",
|
||||
"data": "bk9p1h7x0s3m/documenso-signed.pdf",
|
||||
"initialData": "9753/xzqrshtlpokm/documenso.pdf"
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
@ -309,12 +421,15 @@ Example payload for the `document.completed` event:
|
||||
"email": "signer2@documenso.com",
|
||||
"name": "Signer 2",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:51:10.055Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "VIEWER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
@ -327,12 +442,15 @@ Example payload for the `document.completed` event:
|
||||
"email": "signer1@documenso.com",
|
||||
"name": "Signer 1",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 2,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
@ -345,6 +463,71 @@ Example payload for the `document.completed` event:
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `document.rejected` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_REJECTED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "Signer",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:48:07.569Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": "I do not agree with the terms",
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "REJECTED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:48:07.945Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
## Availability
|
||||
|
||||
Webhooks are available to individual users and teams.
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"signing-documents": "Signing Documents",
|
||||
"templates": "Templates",
|
||||
"direct-links": "Direct Signing Links",
|
||||
"document-visibility": "Document Visibility",
|
||||
"teams": "Teams",
|
||||
"-- Legal Overview": {
|
||||
"type": "separator",
|
||||
"title": "Legal Overview"
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
---
|
||||
title: Document Visibility
|
||||
description: Learn how to control the visibility of your team documents.
|
||||
---
|
||||
|
||||
# Team's Document Visibility
|
||||
|
||||
By default, all documents created in a team are visible to all team members. However, you can control the visibility of your documents by changing the document's visibility settings.
|
||||
|
||||
To set the visibility of a document, click on the **Document visibility** dropdown in the document's settings panel.
|
||||
|
||||

|
||||
|
||||
The document visibility can be set to one of the following options:
|
||||
|
||||
- **Everyone** - The document is visible to all team members.
|
||||
- **Managers and above** - The document is visible to people with the role of Manager or above.
|
||||
- **Admin only** - The document is only visible to the team's admins.
|
||||
6
apps/documentation/pages/users/teams/_meta.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"preferences": "Preferences",
|
||||
"document-visibility": "Document Visibility",
|
||||
"sender-details": "Email Sender Details",
|
||||
"branding-preferences": "Branding Preferences"
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Branding Preferences
|
||||
description: Learn how to set the branding preferences for your team account.
|
||||
---
|
||||
|
||||
# Branding Preferences
|
||||
|
||||
You can set the branding preferences for your team account by going to the **Branding Preferences** tab in the team's settings dashboard.
|
||||
|
||||

|
||||
|
||||
On this page, you can:
|
||||
|
||||
- **Upload a Logo** - Upload your team's logo to be displayed instead of the default Documenso logo.
|
||||
- **Set the Brand Website** - Enter the URL of your team's website to be displayed in the email communications sent by the team.
|
||||
- **Add Additional Brand Details** - You can add additional information to display at the bottom of the emails sent by the team. This can include contact information, social media links, and other relevant details.
|
||||
45
apps/documentation/pages/users/teams/document-visibility.mdx
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Document Visibility
|
||||
description: Learn how to control the visibility of your team documents.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Team's Document Visibility
|
||||
|
||||
The default document visibility option allows you to control who can view and access the documents uploaded to your team account. The document visibility can be set to one of the following options:
|
||||
|
||||
- **Everyone** - The document is visible to all team members.
|
||||
- **Managers and above** - The document is visible to team members with the role of _Manager or above_ and _Admin_.
|
||||
- **Admin only** - The document is only visible to the team's admins.
|
||||
|
||||

|
||||
|
||||
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [team's general preferences page](/users/teams/preferences) and selecting a different visibility option.
|
||||
|
||||
<Callout type="warning">
|
||||
If the team member uploading the document has a role lower than the default document visibility,
|
||||
the document visibility will be set to a lower visibility level matching the team member's role.
|
||||
</Callout>
|
||||
|
||||
Here's how it works:
|
||||
|
||||
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to "_Everyone_".
|
||||
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Managers and above_".
|
||||
- Otherwise, the document's visibility is set to the default document visibility.
|
||||
|
||||
You can change the visibility of a document at any time by editing the document and selecting a different visibility option.
|
||||
|
||||

|
||||
|
||||
<Callout type="warning">
|
||||
Updating the default document visibility in the team's general settings will not affect the
|
||||
visibility of existing documents. You will need to update the visibility of each document
|
||||
individually.
|
||||
</Callout>
|
||||
|
||||
## A Note on Document Access
|
||||
|
||||
The `document owner` (the user who created the document) always has access to the document, regardless of the document's visibility settings. This means that even if a document is set to "Admins only", the document owner can still view and edit the document.
|
||||
|
||||
The `recipient` (the user who receives the document for signature, approval, etc.) also has access to the document, regardless of the document's visibility settings. This means that even if a document is set to "Admins only", the recipient can still view and sign the document.
|
||||
19
apps/documentation/pages/users/teams/preferences.mdx
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Preferences
|
||||
description: Learn how to manage your team's global preferences.
|
||||
---
|
||||
|
||||
# Preferences
|
||||
|
||||
You can manage your team's global preferences by clicking on the **Preferences** tab in the team's settings dashboard.
|
||||
|
||||

|
||||
|
||||
The preferences page allows you to update the following settings:
|
||||
|
||||
- **Document Visibility** - Set the default visibility of the documents created by team members. Learn more about [document visibility](/users/teams/document-visibility).
|
||||
- **Default Document Language** - This setting allows you to set the default language for the documents uploaded in the team account. The default language is used as the default language in the email communications with the document recipients. You can change the language for individual documents when uploading them.
|
||||
- **Sender Details** - Set whether the sender's name should be included in the emails sent by the team. Learn more about [sender details](/users/teams/sender-details).
|
||||
- **Typed Signature** - It controls whether the document recipients can sign the documents with a typed signature or not. If enabled, the recipients can sign the document using either a drawn or a typed signature. If disabled, the recipients can only sign the documents usign a drawn signature. This setting can also be changed for individual documents when uploading them.
|
||||
- **Include the Signing Certificate** - This setting controls whether the signing certificate should be included in the signed documents. If enabled, the signing certificate is included in the signed documents. If disabled, the signing certificate is not included in the signed documents. Regardless of this setting, the signing certificate is always available in the document's audit log page.
|
||||
- **Branding Preferences** - Set the branding preferences and defaults for the team account. Learn more about [branding preferences](/users/teams/branding-preferences).
|
||||
14
apps/documentation/pages/users/teams/sender-details.mdx
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Email Sender Details
|
||||
description: Learn how to update the sender details for your team's email notifications.
|
||||
---
|
||||
|
||||
## Sender Details
|
||||
|
||||
If the **Sender Details** setting is enabled, the emails sent by the team will include the sender's name. The email will say:
|
||||
|
||||
> "Example User" on behalf of "Example Team" has invited you to sign "document.pdf"
|
||||
|
||||
If the **Sender Details** setting is disabled, the emails sent by the team will not include the sender's name. The email will say:
|
||||
|
||||
> "Example Team" has invited you to sign "document.pdf"
|
||||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
BIN
apps/documentation/public/teams/team-branding-preferences.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 128 KiB |
BIN
apps/documentation/public/teams/team-preferences.webp
Normal file
|
After Width: | Height: | Size: 165 KiB |
@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Cal.com Chooses Documenso for DPA and BAA Scalability and Compliance
|
||||
description: Learn how Cal.com scales their Data Processing Agreement (DPA) and Business Associate Agreement (BAA) processes with Documenso’s open source platform as they grow.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-10-11
|
||||
tags:
|
||||
- Customer Story
|
||||
- Open Startup
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/cal2.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="Scheduling Infrastructure for Everyone"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Scheduling Infrastructure for Everyone.
|
||||
</figcaption>
|
||||
</figure>
|
||||
TL;DR: Cal.com uses Documenso’s template direct links to facilitate low-friction compliance paperwork, enhancing scalability and user experience.
|
||||
|
||||
## Cal.com – The Most Public Private Company
|
||||
|
||||
[Cal.com](Cal.com) is an open source company that needs no introduction. Founded in 2021 by Bailey Pumfleet and Peer Richelsen, it quickly evolved from an open source alternative to the widespread but limited scheduling platform Calendly into the internet’s most beloved scheduling solution. Starting with just two founders, Cal.com has grown into a team of 22, facilitating millions of bookings per year for its ever-growing user and customer base.
|
||||
|
||||
Their commitment to transparency is evident as they follow the [open startup movement](https://cal.com/open), opening up not only their source code but also providing insights into their business operations. Their Commercial Open Source Software (COSS) model, combining a company and an open source project, has inspired a whole cohort of startups joining the space—not least of which is Documenso.
|
||||
|
||||
## The Need
|
||||
|
||||
At this point, Cal.com serves customers of all sizes, from single users to large enterprises. To provide the best product for their customers, they are certified for SOC 2, HIPAA, GDPR, and many other compliance regulations. One challenge that comes with this is the increasing number of waivers that need to be signed when onboarding customers. Business Associate Agreements (BAAs) and Data Processing Agreements (DPAs) are two of the more commonly known examples. To get these signed with minimal effort for both sides, they were looking for a solution to handle these at scale.
|
||||
|
||||
> We love open source.
|
||||
|
||||
— Peer Richelsen, Co-Founder, Cal.com
|
||||
|
||||
Being an open source company, they also prefer open source in their vendors—for both the shared philosophy and the higher level of trust. The goal was to integrate signing into the checkout process as seamlessly as possible.
|
||||
|
||||
## The Solution
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/cal.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="Cal.com direct link template to sign a DPA"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Sign a DPA with Cal by clicking a link anytime.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Documenso offers exactly this solution through direct link templates, enabling Cal.com to:
|
||||
|
||||
- Provide Immediate Access: Customers can access and sign necessary compliance documents through direct links at any time.
|
||||
- Enhance User Experience: Users are immediately forwarded to onboarding after signing.
|
||||
- Ensure Easy Access: The documents are stored within the company’s team account, allowing easy access for anyone who needs them.
|
||||
|
||||
Direct Link templates can also easily be embedded, using the [Documenso widget](https://documen.so/embedded). Embedding anywhere, pre-Filling the templates and notfiying the compliance team at certain point of the flow are a few of the many option the team now has in continously enhanceing their onboard and compliance UX.
|
||||
|
||||
Read more about our direct link templates here: [Direct Link Signing](https://docs.documenso.com/users/direct-links).
|
||||
|
||||
## The Journey
|
||||
|
||||
Initially, Cal.com’s team approached the new solution with skepticism. As Bailey reflected:
|
||||
|
||||
> We were intrigued but skeptical at first, as we put a lot of thought into compliance and doing things right. Documenso’s documentation and support showcased how their direct link templates could meet our needs while being highly compliant.
|
||||
|
||||
This experience highlights Documenso’s trust philosophy. We strive to be transparent in everything we do and let people judge for themselves. It also shows that we neither want nor get trust by default. Doing things right is a conversation worth having, especially in a space as opaque as digital signatures. It goes without saying that the whole team is hyped to have Cal.com on board and yet another open source company joining the open signing movement 🚀
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 'Customer Story Prisma: 4 Reasons why Prisma chose Documenso for Signatures'
|
||||
description: We are happy to welcome Prisma, another OSS company, as a customer. Read here why they choose us.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-09-26
|
||||
tags:
|
||||
- Prisma
|
||||
- Customer Story
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/prisma.png"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="Primsa Landing Page We simplify database migration, connection pooling, database queries, and readable data models."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Prisma uses Documenso for collaborative team signing.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; Prisma is now using Documenso, and [we added visibility scopes](https://docs.documenso.com/users/document-visibility)
|
||||
|
||||
# Prisma
|
||||
|
||||
Prisma is an open-source company known for its modern OSS ORM (Object-Relational Mapping) tools that simplify database interactions for developers. Their flagship product, Prisma ORM, provides a type-safe way to query databases like PostgreSQL, MySQL, and many more. With the addition of Prisma Studio, an intuitive database management interface, Prisma makes it easier and more efficient for developers to work with databases. With their new additions, Prisma Pulse and Accelerate, you can react to real-time database changes and optimize your queries. And they are completely [open source](https://github.com/prisma/prisma)!
|
||||
|
||||
# We choose Prisma too!
|
||||
|
||||
I discovered Prisma when planning the tech stack for the [first version of Documenso](https://github.com/documenso/documenso/releases/tag/0.9-developer-preview). Prisma has felt natural to use since day one and has been the base of our database architecture ever since. It's great to see them develop and grow with us.
|
||||
|
||||
# Why they choose us
|
||||
|
||||
## 1. Signature Flows
|
||||
|
||||
Documenso signing flows are highly configurable, designed to adapt to the needs of any document signing process. Whether you're working with different roles, varying settings, or specific delivery methods, Documenso offers the flexibility to suit your requirements. You can choose to send documents via email, share a manual link, generate a link through the API, or even use a static direct link for quick access—all while ensuring a smooth signing experience.
|
||||
|
||||
Additionally, you can create templates to streamline and reuse common workflows, saving valuable time. Direct link templates enable users to drive the flow themselves, providing a straightforward path for signing. For a seamless experience, Documenso also allows you to embed the signing process directly into your website, ensuring an uninterrupted, integrated workflow tailored to your needs.
|
||||
|
||||
## 2. Modern UX
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/dsux.png"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="A completed document in Documenso, ready to download."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
We call Documenso's design "Happy Minimalism"
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
We’ve crafted Documenso with a sleek, modern interface that makes it incredibly easy to use. Whether you’re signing documents, managing workflows, or fine-tuning settings, its intuitive design allows you to accomplish tasks quickly and effortlessly. More than just powerful, Documenso is a pleasure to navigate—designed to be accessible to everyone, no matter their level of tech experience.
|
||||
|
||||
## 3. Teams
|
||||
|
||||
### Teamwork Makes the Dream Work
|
||||
|
||||
Documenso makes teamwork a breeze with its team management features. You can easily set up and organize teams, making it simple to share and manage documents and workflows together. This is a lifesaver for larger organizations or teams spread across different departments, ensuring everyone stays in sync and on track. Different visibility scopes ensure private documents stay private and others are shared for easy collaboration.
|
||||
|
||||
### Document Visibility
|
||||
|
||||
Collaboration within a team often demands different levels of access to documents. For instance, the Documenso team at Prisma needed a way to set custom visibility on some documents while keeping others accessible to everyone. To address this need, we introduced role-based visibility scopes. This feature allows teams to manage documents more effectively. They can make certain documents visible only to managers or, in special cases, restricted to admins. This ensures sensitive information stays protected while general documents remain accessible to those who need them.
|
||||
|
||||
Learn more about visibility scopes and [how they can benefit your team here](https://docs.documenso.com/users/document-visibility).
|
||||
|
||||
## 4. OSS!
|
||||
|
||||
As you might know, we are open-source! This means you can peek under the hood, tweak things to your liking, and even contribute to making the platform better. We love the community-driven aspect of open-source, and it aligns perfectly with our goal to keep improving and innovating with input from our users.
|
||||
|
||||
So, whether you're looking to streamline your document workflows or just need a solid, reliable platform, Documenso has got your back. And we're thrilled to serve another OSS company and help make the space more open.
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
86
apps/marketing/content/blog/go-fork-yourself.mdx
Normal file
@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Go Fork Yourself
|
||||
description: Curious about our take on open-source and code forking? Discover why we see forking not as a threat but as a vital part of the Open Source ecosystem.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-10-03
|
||||
tags:
|
||||
- Culture
|
||||
- Open Startup
|
||||
- Open Source
|
||||
---
|
||||
|
||||
> TLDR; At Documenso, we see OSS as co-owned by all. Forking—collaborative or not—is part of the open-source spirit.
|
||||
|
||||
## Freedom vs. Ownership
|
||||
|
||||
Recently, there has been a lot of debate on the subject of forks and the usage of OSS IP (Open Source Software Intellectual Property). While I mostly aim to stay out of these controversies (as there is no “winning”), I wanted to take this opportunity to share my views on IP and forking culture here at Documenso. I don’t presume this is the ideal path, but for me, it’s the only path that makes sense.
|
||||
|
||||
What these issues show foremost, in my opinion, is that the concept of Open Source is still evolving. I have heard many say, “Open Source is clearly defined” and that there is no ambiguity anymore. That may be true on the legal side, but there are vast differences in how these rules are interpreted and lived out. Here are a few questions to illustrate the point:
|
||||
|
||||
1. Is it okay to use an open-source project without ever giving back?
|
||||
2. Is it okay to fork (some might say copy) an OSS product and build something on top of it?
|
||||
3. Are we morally obliged to fight those who provide different answers to these questions than we do?
|
||||
|
||||
## Embracing Forks and Collaboration
|
||||
|
||||
Since starting Documenso, I’ve thought a lot about what it actually means to be Open Source for us. So far, it has been about openness in working with everyone, from contributors to customers and sharing our work transparently. For this, we have been richly rewarded with attention and reach. This collaborative give-and-take is what people commonly associate with being Open Source, and it seems ideal.
|
||||
|
||||
Yet, there are the questions mentioned above. And while these may be contentious, my take is straightforward:
|
||||
|
||||
1. Yes.
|
||||
2. Yes.
|
||||
3. No.
|
||||
|
||||
I say this because, to me, the principles of Open Source are rooted in freedom and collaboration. That means allowing others to use, improve, or even compete with what you’ve built without feeling possessive over the code. The beauty of Open Source lies in its openness—its ability to be forked, reused, and adapted by anyone.
|
||||
|
||||
You may answer these questions differently for your own reasons. One thing I’ve found lacking in the discourse is the fact that Open Source is still being treated as socially proprietary. If it’s under an open-source license, you can fork it and try to improve upon the original, and there’s nothing wrong with that. The same is true for closed-source startups. Yet in Open Source, there’s a notion that it’s somehow “dirty,” even though the license explicitly allows it.
|
||||
|
||||
## Forking in Action: Real-World Examples
|
||||
|
||||
When the team behind **Node.js** disagreed with its governance and pace of development, they forked the project to create **io.js**. This wasn’t seen as dirty but as a necessary push for change. In fact, the fork resulted in positive changes—better community governance and faster development—which eventually led to the merge of the two projects under the Node.js Foundation. It shows that forking can be a catalyst for improvement, not just competition.
|
||||
|
||||
## The Misconception of “Exploitative” Usage
|
||||
|
||||
However, sometimes forks don’t merge back but still bring positive change. A good example is **Jenkins**, which was forked from **Hudson** over disagreements in governance after Oracle acquired Sun Microsystems. Jenkins quickly overtook Hudson in terms of community support, development, and innovation. Rather than being seen as a hostile move, the fork enabled Jenkins to become a thriving project, better aligned with the open-source ethos of collaboration and transparency. It emphasizes that forking isn’t inherently exploitative; it can simply be a way to realize a project’s full potential.
|
||||
|
||||
And then there’s **MariaDB**, a fork of **MySQL**. After Oracle acquired MySQL, many in the community feared the project’s open-source nature could be compromised. The fork preserved its spirit, and MariaDB has since grown to become a popular and thriving database. It’s a reminder that sometimes, forking is not just acceptable—it’s necessary to uphold the values and freedoms of open-source software.
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/owncode.jpeg"
|
||||
width="1200"
|
||||
height="675"
|
||||
alt="Meme: If everyone owns the code, no one does."
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Funny Meme to drive the point home.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
My view is that the code is not “your” code, just as Documenso’s code is not “our” code. It’s been co-owned by the world ever since we published the repo under AGPL V3. That is the whole point. It’s finally not owned by anyone (cue the “everyone/no one” meme). Open Source is for everyone, even competitors. Yet, we are still treating the licenses as extensions of the old, proprietary world and defending perceived injustices based on that model.
|
||||
|
||||
> Side Note: Full compliance with all license and other legal rules is a given here.
|
||||
|
||||
## Documenso’s Approach: Co-Ownership and Community
|
||||
|
||||
So, if you want to fork Documenso and build a business on it, you can. Whether that’s a cool thing to do is another matter. Whether you do a better job than us is also another matter (you won’t). But if you do, I’ll be the first to join. But why not join us from the start since you already have the upside? We exist because we believe this to be the best way forward—not because we force it.
|
||||
|
||||
## The Bigger Picture: Open-Source as Progress
|
||||
|
||||
I’ve also thought a lot about question #3. I understand the impulse to fight anyone who doesn’t appreciate this collaborative approach, but there is no part of this model that backs that up. You are free to “exploit” as long as it’s in a way that adds value. The fallacy is in considering someone else using the OSS part for their business as treason, which it’s not. It’s the whole point.
|
||||
|
||||
While some might say this is theoretical and that reality is different, this is the version of Open Source on which we are building Documenso. The point here is that OSS companies must be resilient to handle forking and competition; without this resilience, an open source driven economy can’t thrive. The focus on freedom and collaboration means being prepared for forks and challenges as part of the growth, not as threats.
|
||||
|
||||
Of course, all of this applies to Documenso, the OSS project, not Documenso Inc., the company, which is very much a privately owned, for-profit entity. However, since the goal is to scale Documenso to the entire world, there is plenty of room to see everyone as co-owners of the Open Source project rather than as competitors. In the end, Open Source is about progress through freedom. If you don’t like how we run things, go fork yourself and hold us accountable. We don’t own this; we just happened to start it.
|
||||
|
||||
> Since this article is open source as well, you are free to fork it and change it here: [https://documen.so/repo](https://documen.so/repo)
|
||||
|
||||
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
64
apps/marketing/content/blog/project-babel.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Project Babel
|
||||
description: We are announcing Project Babel - an initiative to support all languages of the world on Documenso.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-11-08
|
||||
tags:
|
||||
- Languages
|
||||
- Community
|
||||
- Open Source
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/babel.png"
|
||||
width="800"
|
||||
height="800"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">The tower of Babel to add some Gravitas to this project.</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; We are opening up translations to the community. Read this to add a language: https://documen.so/babel-fish
|
||||
|
||||
## Announcing Project Babel: Powering Documenso with Global Language Support
|
||||
|
||||
At Documenso, we believe that open source is more than just a software philosophy—it’s a way to build solutions that are open to all. Now, we’re happy to take that mission further with Project Babel, a community-driven initiative designed to bring worldwide language support to the Documenso platform. This project aims to enable Documenso to support as many languages as possible.
|
||||
|
||||
## Why Language Support Matters
|
||||
|
||||
We already have customers from 36 different countries and are seeing traffic from even more. When it comes to critical tools like signature platforms, having a user interface in your native language can make all the difference. No matter who and where you are, your team deserves tools that are fully accessible and intuitive. That’s why we’re making it our goal to support every language, and we need your help to make it happen! We’re building Documenso as a truly global public commodity.
|
||||
|
||||
## The Vision Behind Project Babel
|
||||
|
||||
The goal of Project Babel is simple but bold: We want to out-ship and out-customize every other document signing tool worldwide. How? By leveraging the collective power of our global community.
|
||||
|
||||
Unlike closed-source software, where localization means waiting for updates from the core team, Project Babel lets anyone contribute a new language, improve an existing translation, or customize the experience to meet local cultural nuances. This flexibility isn’t just a bonus—it’s the baseline for truly global products.
|
||||
|
||||
Through Project Babel, you can help make Documenso the most inclusive e-signature tool. Whether by adding a language you speak or fine-tuning existing translations, you’re shaping a platform that works for everyone, everywhere.
|
||||
|
||||
## How You Can Contribute
|
||||
|
||||
We’ve created a simple GitHub-based contribution flow to get started. We’ll improve the flow and user experience as the project progresses. As always, your contributions are highly valued.
|
||||
|
||||
Check out the contribution guide here: [https://documen.so/babel-fish](https://documen.so/babel-fish)
|
||||
|
||||
## Open Source Makes It Possible
|
||||
|
||||
Closed-source solutions can’t keep up with the speed or depth of customization that open source offers. While other companies might take months or years to localize their products, Documenso can adapt and grow in real-time, thanks to contributions from our community. Whether it’s a small regional dialect or a widely spoken language, Project Babel ensures that Documenso evolves to meet the needs of people everywhere.
|
||||
|
||||
> More importantly, this initiative empowers users. It allows you to control your software experience, ensuring it reflects your culture, language, and unique needs.
|
||||
|
||||
Project Babel is more than a localization effort—it’s the first step toward democratizing access to highly customized software for everyone, no matter where they are or what language they speak. We’re incredibly excited about this initiative, but it can only succeed with your participation. We invite you to join us in making Documenso the most linguistically inclusive platform out there.
|
||||
|
||||
Ready to get started? Check out the full tutorial and become part of the Babel community today! Let’s build open signing for the world: https://documen.so/babel-fish
|
||||
|
||||
If you have any questions or comments, reach out on [Twitter / X](https://twitter.com/eltimuro) (DMs open) or [Discord](https://documen.so/discord).
|
||||
|
||||
Thinking about switching to a modern signing platform? Reach out anytime: [https://documen.so/sales](https://documen.so/sales)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
@ -8,6 +8,114 @@ Check out what's new in the latest version and read our thoughts on it. For more
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.8.0: Team Preferences, Signature Rejection, and Document Distribution
|
||||
|
||||
We're excited to announce the release of Documenso v1.8.0! This update brings powerful new features to enhance your document signing process. Here's what's new:
|
||||
|
||||
## 🌟 Key New Features
|
||||
|
||||
### 1. Team Preferences
|
||||
|
||||
Introducing **Team Preferences**, allowing administrators to configure settings and preferences that apply to documents across the entire team. This feature ensures consistency and simplifies management by letting you set default options, permissions, and preferences that automatically apply to all team members.
|
||||
|
||||

|
||||
|
||||
### 2. Signature Rejection
|
||||
|
||||
Recipients now have the option to **reject signatures**. This feature enhances communication by allowing recipients to decline signing, providing feedback or requesting changes before the document is finalized.
|
||||
|
||||
<video
|
||||
src="/changelog/v1_8_0/reject-document.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
### 3. Document Distribution Settings
|
||||
|
||||
With the new **Document Distribution Settings**, you have greater control over how your documents are shared. Distribute communications via our automated emails and templates or take full control using our API and your own notifications infrastructure.
|
||||
|
||||
## 🔧 Other Improvements
|
||||
|
||||
- **Support for Gmail SMTP Service**: Adds support for using Gmail as your SMTP service provider.
|
||||
- **Certificate and Email Translations**: Added support for multiple languages in document certificates and emails, enhancing the experience for international users.
|
||||
- **Field Movement Fixes**: Resolved issues related to moving fields within documents, improving the document preparation experience.
|
||||
- **Docker Environment Update**: Improved Docker setup for smoother deployments and better environment consistency.
|
||||
- **Billing Access Improvements**: Users now have uninterrupted access to billing information, simplifying account management.
|
||||
- **Support Time Windows for 2FA Tokens**: Enhanced two-factor authentication by supporting time windows in 2FA tokens, improving flexibility.
|
||||
|
||||
## 💡 Recent Features
|
||||
|
||||
Don't forget to take advantage of these powerful features from our recent releases:
|
||||
|
||||
- **Signing Order**: Define the sequence in which recipients sign your documents for a structured signing process.
|
||||
- **Document Visibility Controls**: Manage who can view your documents and at what stages, offering greater privacy and control.
|
||||
- **Embedded Signing Experience**: Integrate the signing process directly into your own applications for a seamless user experience.
|
||||
|
||||
**👏 Thank You**
|
||||
|
||||
As always, we're grateful for the community's contributions and feedback. Your support helps us improve Documenso and deliver a top-notch open-source document signing solution.
|
||||
|
||||
We hope you enjoy the new features in Documenso v1.8.0. Happy signing!
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.7.1: Signing order and document visibility
|
||||
|
||||
We're excited to introduce Documenso v1.7.1, bringing you improved control over your document signing process. Here are the key updates:
|
||||
|
||||
## 🌟 Key New Features
|
||||
|
||||
### 1. Signing Order
|
||||
|
||||
Specify the sequence in which recipients sign your documents. This ensures a structured signing process, particularly useful for complex agreements or hierarchical approvals.
|
||||
|
||||
<video
|
||||
src="/changelog/signing-order-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
### 2. Document Visibility
|
||||
|
||||
Manage who can view your documents and when. This feature offers greater privacy and flexibility in your document sharing workflows.
|
||||
|
||||
<video
|
||||
src="/changelog/document-visibility-demo.mp4"
|
||||
className="aspect-video w-full"
|
||||
autoPlay
|
||||
loop
|
||||
controls
|
||||
/>
|
||||
|
||||
## 🔧 Other Improvements
|
||||
|
||||
- Added language switcher for easier language selection
|
||||
- Support for smaller field bounds in documents
|
||||
- Improved select field UX
|
||||
- Enhanced template functionality for advanced fields
|
||||
- Added authOptions to the API
|
||||
- Various UI refinements and bug fixes
|
||||
|
||||
## 💡 Recent Features
|
||||
|
||||
Don't forget about these powerful features from our recent v1.7.0 release:
|
||||
|
||||
- Embedded Signing Experience
|
||||
- Copy and Paste Fields
|
||||
- Customizable Signature Colors
|
||||
|
||||
## 👏 Thank You
|
||||
|
||||
As always, we're grateful for our community's contributions and feedback. Your input continues to shape Documenso into the leading open-source document signing solution.
|
||||
|
||||
We're eager to see how these new features enhance your document workflows. Enjoy exploring Documenso v1.7.1!
|
||||
|
||||
---
|
||||
|
||||
# Documenso v1.7.0: Embedded Signing, Copy and Paste, and More
|
||||
|
||||
We're thrilled to announce the release of Documenso v1.7.0, packed with exciting new features and improvements that enhance document signing flexibility, user experience, and global accessibility.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.7.1-rc.3",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
@ -34,7 +34,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-plausible": "^3.10.1",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
|
||||
BIN
apps/marketing/public/blog/babel.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
apps/marketing/public/blog/cal.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
apps/marketing/public/blog/cal2.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
apps/marketing/public/blog/dsux.png
Normal file
|
After Width: | Height: | Size: 692 KiB |
BIN
apps/marketing/public/blog/owncode.jpeg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/marketing/public/blog/prisma.png
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
apps/marketing/public/changelog/document-visibility-demo.mp4
Normal file
BIN
apps/marketing/public/changelog/signing-order-demo.mp4
Normal file
BIN
apps/marketing/public/changelog/v1_8_0/reject-document.mp4
Normal file
BIN
apps/marketing/public/changelog/v1_8_0/team-global-settings.jpeg
Normal file
|
After Width: | Height: | Size: 169 KiB |
23
apps/marketing/src/app/(marketing)/[content]/content.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import type { DocumentTypes } from 'contentlayer/generated';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
const mdxComponents: MDXComponents = {
|
||||
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
|
||||
<Image {...props} alt={props.alt ?? ''} />
|
||||
),
|
||||
};
|
||||
|
||||
export type ContentPageContentProps = {
|
||||
document: DocumentTypes;
|
||||
};
|
||||
|
||||
export const ContentPageContent = ({ document }: ContentPageContentProps) => {
|
||||
const MDXContent = useMDXComponent(document.body.code);
|
||||
|
||||
return <MDXContent components={mdxComponents} />;
|
||||
};
|
||||
@ -1,12 +1,11 @@
|
||||
import Image from 'next/image';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { allDocuments } from 'contentlayer/generated';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
import { ContentPageContent } from './content';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
||||
@ -19,19 +18,13 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
|
||||
return { title: document.title };
|
||||
};
|
||||
|
||||
const mdxComponents: MDXComponents = {
|
||||
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
|
||||
<Image {...props} alt={props.alt ?? ''} />
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* A generic catch all page for the root level that checks for content layer documents.
|
||||
*
|
||||
* Will render the document if it exists, otherwise will return a 404.
|
||||
*/
|
||||
export default function ContentPage({ params }: { params: { content: string } }) {
|
||||
setupI18nSSR();
|
||||
export default async function ContentPage({ params }: { params: { content: string } }) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const post = allDocuments.find((post) => post._raw.flattenedPath === params.content);
|
||||
|
||||
@ -39,11 +32,9 @@ export default function ContentPage({ params }: { params: { content: string } })
|
||||
notFound();
|
||||
}
|
||||
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<article className="prose dark:prose-invert mx-auto">
|
||||
<MDXContent components={mdxComponents} />
|
||||
<ContentPageContent document={post} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
23
apps/marketing/src/app/(marketing)/blog/[post]/content.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import type { BlogPost } from 'contentlayer/generated';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
const mdxComponents: MDXComponents = {
|
||||
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
|
||||
<Image {...props} alt={props.alt ?? ''} />
|
||||
),
|
||||
};
|
||||
|
||||
export type BlogPostContentProps = {
|
||||
post: BlogPost;
|
||||
};
|
||||
|
||||
export const BlogPostContent = ({ post }: BlogPostContentProps) => {
|
||||
const MdxContent = useMDXComponent(post.body.code);
|
||||
|
||||
return <MdxContent components={mdxComponents} />;
|
||||
};
|
||||
@ -1,16 +1,15 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { allBlogPosts } from 'contentlayer/generated';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
import { CallToAction } from '~/components/(marketing)/call-to-action';
|
||||
|
||||
import { BlogPostContent } from './content';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
||||
@ -42,14 +41,8 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mdxComponents: MDXComponents = {
|
||||
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
|
||||
<Image {...props} alt={props.alt ?? ''} />
|
||||
),
|
||||
};
|
||||
|
||||
export default function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
setupI18nSSR();
|
||||
export default async function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const post = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
|
||||
|
||||
@ -57,8 +50,6 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<article className="prose dark:prose-invert mx-auto py-8">
|
||||
@ -87,7 +78,7 @@ export default function BlogPostPage({ params }: { params: { post: string } }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MDXContent components={mdxComponents} />
|
||||
<BlogPostContent post={post} />
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
<ul className="not-prose flex list-none flex-row space-x-2 px-0">
|
||||
|
||||
@ -9,8 +9,8 @@ export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
};
|
||||
|
||||
export default function BlogPage() {
|
||||
const { i18n } = setupI18nSSR();
|
||||
export default async function BlogPage() {
|
||||
const { i18n } = await setupI18nSSR();
|
||||
|
||||
const blogPosts = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.date);
|
||||
|
||||
@ -19,10 +19,10 @@ export const TEAM_MEMBERS = [
|
||||
},
|
||||
{
|
||||
name: 'Ephraim Atta-Duncan',
|
||||
role: 'Software Engineer - Intern',
|
||||
salary: 15_000,
|
||||
role: 'Software Engineer - I',
|
||||
salary: 60_000,
|
||||
location: 'Ghana',
|
||||
engagement: msg`Part-Time`,
|
||||
engagement: msg`Full-Time`,
|
||||
joinDate: 'June 6th, 2023',
|
||||
},
|
||||
{
|
||||
|
||||
@ -131,7 +131,7 @@ const fetchEarlyAdopters = async () => {
|
||||
};
|
||||
|
||||
export default async function OpenPage() {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ const fontCaveat = Caveat({
|
||||
});
|
||||
|
||||
export default async function IndexPage() {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const starCount = await fetch('https://api.github.com/repos/documenso/documenso', {
|
||||
headers: {
|
||||
|
||||
@ -30,8 +30,8 @@ export type PricingPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export default function PricingPage() {
|
||||
setupI18nSSR();
|
||||
export default async function PricingPage() {
|
||||
await setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div className="mt-6 sm:mt-12">
|
||||
|
||||
@ -163,6 +163,7 @@ export const SinglePlayerClient = () => {
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
readStatus: 'OPENED',
|
||||
rejectionReason: null,
|
||||
documentDeletedAt: null,
|
||||
signingStatus: 'NOT_SIGNED',
|
||||
sendStatus: 'NOT_SENT',
|
||||
|
||||
@ -14,8 +14,8 @@ export const dynamic = 'force-dynamic';
|
||||
// !: This entire file is a hack to get around failed prerendering of
|
||||
// !: the Single Player Mode page. This regression was introduced during
|
||||
// !: the upgrade of Next.js to v13.5.x.
|
||||
export default function SingleplayerPage() {
|
||||
setupI18nSSR();
|
||||
export default async function SingleplayerPage() {
|
||||
await setupI18nSSR();
|
||||
|
||||
return <SinglePlayerClient />;
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export function generateMetadata() {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
const { lang, locales, i18n } = setupI18nSSR();
|
||||
const { lang, locales, i18n } = await setupI18nSSR();
|
||||
|
||||
return (
|
||||
<html
|
||||
|
||||
@ -108,14 +108,21 @@ export const Carousel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap());
|
||||
const newIndex = emblaApi.selectedScrollSnap();
|
||||
|
||||
setSelectedIndex(newIndex);
|
||||
emblaThumbsApi.scrollTo(newIndex);
|
||||
|
||||
resetProgress();
|
||||
|
||||
const currentVideo = videoRefs.current[newIndex];
|
||||
if (currentVideo) {
|
||||
currentVideo.currentTime = 0;
|
||||
}
|
||||
|
||||
// moduleResolution: bundler breaks this type
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const autoplay = emblaApi.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
const autoplay = emblaApi?.plugins()?.autoplay as unknown as AutoplayType | undefined;
|
||||
|
||||
if (autoplay) {
|
||||
autoplay.reset();
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.7.1-rc.3",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"build": "npm run translate:extract --prefix ../../ && turbo run translate:compile && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"e2e:prepare": "next build && next start",
|
||||
@ -28,6 +28,7 @@
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"@tanstack/react-query": "^4.29.5",
|
||||
"colord": "^2.9.3",
|
||||
"cookie-es": "^1.0.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^10.12.8",
|
||||
@ -37,7 +38,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"next": "14.2.6",
|
||||
"next-auth": "4.24.5",
|
||||
"next-axiom": "^1.1.1",
|
||||
"next-axiom": "^1.5.1",
|
||||
"next-plausible": "^3.10.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"papaparse": "^5.4.1",
|
||||
@ -53,7 +54,7 @@
|
||||
"react-icons": "^4.11.0",
|
||||
"react-rnd": "^10.4.1",
|
||||
"recharts": "^2.7.2",
|
||||
"remeda": "^1.27.1",
|
||||
"remeda": "^2.17.3",
|
||||
"sharp": "0.32.6",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
|
||||
@ -24,7 +24,7 @@ type AdminDocumentDetailsPageProps = {
|
||||
};
|
||||
|
||||
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
|
||||
const { i18n } = setupI18nSSR();
|
||||
const { i18n } = await setupI18nSSR();
|
||||
|
||||
const document = await getEntireDocument({ id: Number(params.id) });
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
import { AdminDocumentResults } from './document-results';
|
||||
|
||||
export default function AdminDocumentsPage() {
|
||||
setupI18nSSR();
|
||||
export default async function AdminDocumentsPage() {
|
||||
await setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -13,7 +13,7 @@ export type AdminSectionLayoutProps = {
|
||||
};
|
||||
|
||||
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
|
||||
|
||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
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 { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
export type SigningVolume = {
|
||||
id: number;
|
||||
name: string;
|
||||
signingVolume: number;
|
||||
createdAt: Date;
|
||||
planId: string;
|
||||
};
|
||||
|
||||
type LeaderboardTableProps = {
|
||||
signingVolume: SigningVolume[];
|
||||
totalPages: number;
|
||||
perPage: number;
|
||||
page: number;
|
||||
sortBy: 'name' | 'createdAt' | 'signingVolume';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export const LeaderboardTable = ({
|
||||
signingVolume,
|
||||
totalPages,
|
||||
perPage,
|
||||
page,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
}: LeaderboardTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
const [searchString, setSearchString] = useState('');
|
||||
const debouncedSearchString = useDebouncedValue(searchString, 1000);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: () => (
|
||||
<div
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={() => handleColumnSort('name')}
|
||||
>
|
||||
{_(msg`Name`)}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
</div>
|
||||
),
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={`https://dashboard.stripe.com/subscriptions/${row.original.planId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{row.getValue('name')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<div
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={() => handleColumnSort('signingVolume')}
|
||||
>
|
||||
{_(msg`Signing Volume`)}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
</div>
|
||||
),
|
||||
accessorKey: 'signingVolume',
|
||||
cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>,
|
||||
},
|
||||
{
|
||||
header: () => {
|
||||
return (
|
||||
<div
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={() => handleColumnSort('createdAt')}
|
||||
>
|
||||
{_(msg`Created`)}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
] satisfies DataTableColumnDef<SigningVolume>[];
|
||||
}, [sortOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
startTransition(() => {
|
||||
updateSearchParams({
|
||||
search: debouncedSearchString,
|
||||
page: 1,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedSearchString]);
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
startTransition(() => {
|
||||
updateSearchParams({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchString(e.target.value);
|
||||
};
|
||||
|
||||
const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
|
||||
startTransition(() => {
|
||||
updateSearchParams({
|
||||
sortBy: column,
|
||||
sortOrder: sortOrder === 'asc' ? 'desc' : 'asc',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="my-6 flex flex-row gap-4"
|
||||
type="text"
|
||||
placeholder={_(msg`Search by name or email`)}
|
||||
value={searchString}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={signingVolume}
|
||||
perPage={perPage}
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
onPaginationChange={onPaginationChange}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
|
||||
{isPending && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
'use server';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||
import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
|
||||
|
||||
type SearchOptions = {
|
||||
search: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sortBy: 'name' | 'createdAt' | 'signingVolume';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export async function search({ search, page, perPage, sortBy, sortOrder }: SearchOptions) {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
if (!isAdmin(user)) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const results = await getSigningVolume({ search, page, perPage, sortBy, sortOrder });
|
||||
|
||||
return results;
|
||||
}
|
||||
60
apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||
|
||||
import { LeaderboardTable } from './data-table-leaderboard';
|
||||
import { search } from './fetch-leaderboard.actions';
|
||||
|
||||
type AdminLeaderboardProps = {
|
||||
searchParams?: {
|
||||
search?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortBy?: 'name' | 'createdAt' | 'signingVolume';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Leaderboard({ searchParams = {} }: AdminLeaderboardProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
if (!isAdmin(user)) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 10;
|
||||
const searchString = searchParams.search || '';
|
||||
const sortBy = searchParams.sortBy || 'signingVolume';
|
||||
const sortOrder = searchParams.sortOrder || 'desc';
|
||||
|
||||
const { leaderboard: signingVolume, totalPages } = await search({
|
||||
search: searchString,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-4xl font-semibold">
|
||||
<Trans>Signing Volume</Trans>
|
||||
</h2>
|
||||
<div className="mt-8">
|
||||
<LeaderboardTable
|
||||
signingVolume={signingVolume}
|
||||
totalPages={totalPages}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -6,7 +6,7 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { BarChart3, FileStack, Settings, Users, Wallet2 } from 'lucide-react';
|
||||
import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -80,6 +80,20 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/leaderboard') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link href="/admin/leaderboard">
|
||||
<Trophy className="mr-2 h-5 w-5" />
|
||||
<Trans>Leaderboard</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
|
||||
@ -12,7 +12,7 @@ import { BannerForm } from './banner-form';
|
||||
// import { BannerForm } from './banner-form';
|
||||
|
||||
export default async function AdminBannerPage() {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ import { SignerConversionChart } from './signer-conversion-chart';
|
||||
import { UserWithDocumentChart } from './user-with-document';
|
||||
|
||||
export default async function AdminStatsPage() {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
export default async function Subscriptions() {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const subscriptions = await findSubscriptions();
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ type AdminManageUsersProps = {
|
||||
};
|
||||
|
||||
export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 10;
|
||||
|
||||
@ -33,6 +33,8 @@ import {
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||
|
||||
import { ResendDocumentActionItem } from '../_action-items/resend-document';
|
||||
import { DeleteDocumentDialog } from '../delete-document-dialog';
|
||||
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
|
||||
@ -62,6 +64,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
||||
|
||||
const isOwner = document.User.id === session.user.id;
|
||||
const isDraft = document.status === DocumentStatus.DRAFT;
|
||||
const isPending = document.status === DocumentStatus.PENDING;
|
||||
const isDeleted = document.deletedAt !== null;
|
||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||
@ -145,6 +148,21 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
||||
<Trans>Share</Trans>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
{canManageDocument && (
|
||||
<DocumentRecipientLinkCopyDialog
|
||||
recipients={document.Recipient}
|
||||
trigger={
|
||||
<DropdownMenuItem
|
||||
disabled={!isPending || isDeleted}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Trans>Signing Links</Trans>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ResendDocumentActionItem
|
||||
document={document}
|
||||
recipients={nonSignedRecipients}
|
||||
|
||||
@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
||||
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -133,6 +133,11 @@ export const DocumentPageViewRecentActivity = ({
|
||||
<CheckIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
))
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => (
|
||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
||||
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
))
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
|
||||
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
||||
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
||||
@ -143,17 +148,11 @@ export const DocumentPageViewRecentActivity = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Todo: Translations. */}
|
||||
<p
|
||||
className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5"
|
||||
title={`${formatDocumentAuditLogAction(auditLog, userId).prefix} ${
|
||||
formatDocumentAuditLogAction(auditLog, userId).description
|
||||
}`}
|
||||
title={formatDocumentAuditLogAction(_, auditLog, userId).description}
|
||||
>
|
||||
<span className="text-foreground font-medium">
|
||||
{formatDocumentAuditLogAction(auditLog, userId).prefix}
|
||||
</span>{' '}
|
||||
{formatDocumentAuditLogAction(auditLog, userId).description}
|
||||
{formatDocumentAuditLogAction(_, auditLog, userId).description}
|
||||
</p>
|
||||
|
||||
<time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5">
|
||||
|
||||
@ -1,16 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon, Clock, MailIcon, MailOpenIcon, PenIcon, PlusIcon } from 'lucide-react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckIcon,
|
||||
Clock,
|
||||
MailIcon,
|
||||
MailOpenIcon,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
} from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DocumentPageViewRecipientsProps = {
|
||||
document: Document & {
|
||||
@ -24,6 +38,7 @@ export const DocumentPageViewRecipients = ({
|
||||
documentRootPath,
|
||||
}: DocumentPageViewRecipientsProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const recipients = document.Recipient;
|
||||
|
||||
@ -68,53 +83,89 @@ export const DocumentPageViewRecipients = ({
|
||||
}
|
||||
/>
|
||||
|
||||
{document.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.SIGNED && (
|
||||
<Badge variant="default">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Approved</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.CC, () =>
|
||||
document.status === DocumentStatus.COMPLETED ? (
|
||||
<>
|
||||
<MailIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Sent</Trans>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-row items-center">
|
||||
{document.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.SIGNED && (
|
||||
<Badge variant="default">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Ready</Trans>
|
||||
<Trans>Approved</Trans>
|
||||
</>
|
||||
),
|
||||
)
|
||||
))
|
||||
.with(RecipientRole.CC, () =>
|
||||
document.status === DocumentStatus.COMPLETED ? (
|
||||
<>
|
||||
<MailIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Sent</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Ready</Trans>
|
||||
</>
|
||||
),
|
||||
)
|
||||
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<>
|
||||
<MailOpenIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Viewed</Trans>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Badge>
|
||||
)}
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<>
|
||||
<MailOpenIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Viewed</Trans>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{document.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||
<Badge variant="secondary">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
<Trans>Pending</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
{document.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||
<Badge variant="secondary">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
<Trans>Pending</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{document.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.REJECTED && (
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Badge variant="destructive">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
<Trans>Rejected</Trans>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<p className="text-sm">
|
||||
<Trans>Reason for rejection: </Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{recipient.rejectionReason}
|
||||
</p>
|
||||
</PopoverHover>
|
||||
)}
|
||||
|
||||
{document.status === DocumentStatus.PENDING &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||
recipient.role !== RecipientRole.CC && (
|
||||
<CopyTextButton
|
||||
value={formatSigningLink(recipient.token)}
|
||||
onCopySuccess={() => {
|
||||
toast({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -26,6 +26,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||
import {
|
||||
DocumentStatus as DocumentStatusComponent,
|
||||
FRIENDLY_STATUS_MAP,
|
||||
@ -73,7 +74,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
|
||||
let canAccessDocument = true;
|
||||
|
||||
if (team && !isRecipient) {
|
||||
if (team && !isRecipient && document?.userId !== user.id) {
|
||||
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
@ -134,6 +135,10 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
|
||||
return (
|
||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||
{document.status === DocumentStatus.PENDING && (
|
||||
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
||||
)}
|
||||
|
||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||
<Trans>Documents</Trans>
|
||||
@ -141,7 +146,10 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
|
||||
<div className="flex flex-row justify-between truncate">
|
||||
<div>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
|
||||
@ -7,10 +7,12 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -112,6 +114,24 @@ export const EditDocumentForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateTypedSignature } =
|
||||
trpc.document.updateTypedSignatureSettings.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({
|
||||
...(oldData || initialDocument),
|
||||
...newData,
|
||||
id: Number(newData.id),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newRecipients) => {
|
||||
@ -158,8 +178,8 @@ export const EditDocumentForm = ({
|
||||
stepIndex: 3,
|
||||
},
|
||||
subject: {
|
||||
title: msg`Add Subject`,
|
||||
description: msg`Add the subject and message you wish to send to signers.`,
|
||||
title: msg`Distribute Document`,
|
||||
description: msg`Choose how the document will reach recipients`,
|
||||
stepIndex: 4,
|
||||
},
|
||||
};
|
||||
@ -183,7 +203,7 @@ export const EditDocumentForm = ({
|
||||
|
||||
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
|
||||
try {
|
||||
const { timezone, dateFormat, redirectUrl } = data.meta;
|
||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||
|
||||
await setSettingsForDocument({
|
||||
documentId: document.id,
|
||||
@ -199,6 +219,7 @@ export const EditDocumentForm = ({
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
language: isValidLanguageCode(language) ? language : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@ -258,6 +279,11 @@ export const EditDocumentForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateTypedSignature({
|
||||
documentId: document.id,
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
});
|
||||
|
||||
// Clear all field data from localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
@ -282,7 +308,7 @@ export const EditDocumentForm = ({
|
||||
};
|
||||
|
||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||
const { subject, message } = data.meta;
|
||||
const { subject, message, distributionMethod, emailSettings } = data.meta;
|
||||
|
||||
try {
|
||||
await sendDocument({
|
||||
@ -291,16 +317,31 @@ export const EditDocumentForm = ({
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
distributionMethod,
|
||||
emailSettings,
|
||||
},
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Document sent`),
|
||||
description: _(msg`Your document has been sent successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
if (distributionMethod === DocumentDistributionMethod.EMAIL) {
|
||||
toast({
|
||||
title: _(msg`Document sent`),
|
||||
description: _(msg`Your document has been sent successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.push(documentRootPath);
|
||||
router.push(documentRootPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
toast({
|
||||
title: _(msg`Links Generated`),
|
||||
description: _(msg`Signing links have been generated for this document.`),
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
router.push(`${documentRootPath}/${document.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -387,6 +428,7 @@ export const EditDocumentForm = ({
|
||||
fields={fields}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
teamId={team?.id}
|
||||
/>
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
||||
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
|
||||
let canAccessDocument = true;
|
||||
|
||||
if (!isRecipient) {
|
||||
if (!isRecipient && document?.userId !== user.id) {
|
||||
canAccessDocument = match([documentVisibility, currentTeamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
@ -109,7 +109,10 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
||||
<Trans>Documents</Trans>
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@ export type DocumentPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export default function DocumentEditPage({ params }: DocumentPageProps) {
|
||||
setupI18nSSR();
|
||||
export default async function DocumentEditPage({ params }: DocumentPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
return <DocumentEditPageView params={params} />;
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ import { ChevronLeft, Loader } from 'lucide-react';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
setupI18nSSR();
|
||||
export default async function Loading() {
|
||||
await setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||
|
||||
@ -58,10 +58,6 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
});
|
||||
};
|
||||
|
||||
const uppercaseFistLetter = (text: string) => {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
const results = data ?? {
|
||||
data: [],
|
||||
perPage: 10,
|
||||
@ -103,9 +99,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
{
|
||||
header: _(msg`Action`),
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => (
|
||||
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
|
||||
),
|
||||
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
|
||||
},
|
||||
{
|
||||
header: 'IP Address',
|
||||
|
||||
@ -121,7 +121,10 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
|
||||
<div className="flex flex-col justify-between truncate sm:flex-row">
|
||||
<div>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
@ -139,6 +142,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
className="mr-2"
|
||||
documentId={document.id}
|
||||
documentStatus={document.status}
|
||||
teamId={team?.id}
|
||||
/>
|
||||
|
||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
||||
|
||||
@ -14,12 +14,14 @@ export type DownloadCertificateButtonProps = {
|
||||
className?: string;
|
||||
documentId: number;
|
||||
documentStatus: DocumentStatus;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const DownloadCertificateButton = ({
|
||||
className,
|
||||
documentId,
|
||||
documentStatus,
|
||||
teamId,
|
||||
}: DownloadCertificateButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
@ -29,7 +31,7 @@ export const DownloadCertificateButton = ({
|
||||
|
||||
const onDownloadCertificatesClick = async () => {
|
||||
try {
|
||||
const { url } = await downloadCertificate({ documentId });
|
||||
const { url } = await downloadCertificate({ documentId, teamId });
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
|
||||
@ -8,8 +8,8 @@ export type DocumentsLogsPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export default function DocumentsLogsPage({ params }: DocumentsLogsPageProps) {
|
||||
setupI18nSSR();
|
||||
export default async function DocumentsLogsPage({ params }: DocumentsLogsPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
return <DocumentLogsPageView params={params} />;
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ export type DocumentPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export default function DocumentPage({ params }: DocumentPageProps) {
|
||||
setupI18nSSR();
|
||||
export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
return <DocumentPageView params={params} />;
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
|
||||
export default function DocumentSentPage() {
|
||||
setupI18nSSR();
|
||||
export default async function DocumentSentPage() {
|
||||
await setupI18nSSR();
|
||||
|
||||
return (
|
||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||
|
||||
@ -37,6 +37,8 @@ import {
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||
|
||||
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
||||
import { DeleteDocumentDialog } from './delete-document-dialog';
|
||||
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
||||
@ -69,7 +71,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
const isOwner = row.User.id === session.user.id;
|
||||
// const isRecipient = !!recipient;
|
||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||
// const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
||||
@ -191,6 +193,20 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
<Trans>Share</Trans>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
{canManageDocument && (
|
||||
<DocumentRecipientLinkCopyDialog
|
||||
recipients={row.Recipient}
|
||||
trigger={
|
||||
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Trans>Signing Links</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ResendDocumentActionItem document={row} recipients={nonSignedRecipients} team={team} />
|
||||
|
||||
<DocumentShareButton
|
||||
|
||||
@ -78,7 +78,7 @@ export const DocumentsDataTable = ({
|
||||
{
|
||||
header: _(msg`Status`),
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => <DocumentStatus status={row.getValue('status')} />,
|
||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
|
||||
@ -47,6 +47,8 @@ export const DeleteDocumentDialog = ({
|
||||
const { refreshLimits } = useLimits();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const deleteMessage = msg`delete`;
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
|
||||
@ -87,7 +89,7 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
setIsDeleteEnabled(event.target.value === 'delete');
|
||||
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -181,7 +183,7 @@ export const DeleteDocumentDialog = ({
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
placeholder={_(msg`Type 'delete' to confirm`)}
|
||||
placeholder={_(msg`Please type ${`'${_(deleteMessage)}'`} to confirm`)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
|
||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
@ -25,16 +26,17 @@ import { DataTableSenderFilter } from './data-table-sender-filter';
|
||||
import { EmptyDocumentState } from './empty-state';
|
||||
import { UploadDocument } from './upload-document';
|
||||
|
||||
export type DocumentsPageViewProps = {
|
||||
export interface DocumentsPageViewProps {
|
||||
searchParams?: {
|
||||
status?: ExtendedDocumentStatus;
|
||||
period?: PeriodSelectorValue;
|
||||
page?: string;
|
||||
perPage?: string;
|
||||
senderIds?: string;
|
||||
search?: string;
|
||||
};
|
||||
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
||||
};
|
||||
}
|
||||
|
||||
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
@ -44,6 +46,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 20;
|
||||
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
||||
const search = searchParams.search || '';
|
||||
const currentTeam = team
|
||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
||||
: undefined;
|
||||
@ -52,6 +55,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
const getStatOptions: GetStatsInput = {
|
||||
user,
|
||||
period,
|
||||
search,
|
||||
};
|
||||
|
||||
if (team) {
|
||||
@ -79,6 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
perPage,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
});
|
||||
|
||||
const getTabHref = (value: typeof status) => {
|
||||
@ -135,10 +140,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
<DocumentStatus status={value} />
|
||||
|
||||
{value !== ExtendedDocumentStatus.ALL && (
|
||||
<span className="ml-1 inline-block opacity-50">
|
||||
{Math.min(stats[value], 99)}
|
||||
{stats[value] > 99 && '+'}
|
||||
</span>
|
||||
<span className="ml-1 inline-block opacity-50">{stats[value]}</span>
|
||||
)}
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
@ -151,6 +153,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||
<PeriodSelector />
|
||||
</div>
|
||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||
<DocumentSearch initialValue={search} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -117,10 +117,10 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
|
||||
{isLoading ? 'Moving...' : 'Move'}
|
||||
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -16,7 +16,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export type AuthenticatedDashboardLayoutProps = {
|
||||
export default async function AuthenticatedDashboardLayout({
|
||||
children,
|
||||
}: AuthenticatedDashboardLayoutProps) {
|
||||
setupI18nSSR();
|
||||
await setupI18nSSR();
|
||||
|
||||
const session = await getServerSession(NEXT_AUTH_OPTIONS);
|
||||
|
||||
|
||||
@ -44,11 +44,11 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const [interval, setInterval] = useState<Interval>('month');
|
||||
const [isFetchingCheckoutSession, setIsFetchingCheckoutSession] = useState(false);
|
||||
const [checkoutSessionPriceId, setCheckoutSessionPriceId] = useState<string | null>(null);
|
||||
|
||||
const onSubscribeClick = async (priceId: string) => {
|
||||
try {
|
||||
setIsFetchingCheckoutSession(true);
|
||||
setCheckoutSessionPriceId(priceId);
|
||||
|
||||
const url = await createCheckout({ priceId });
|
||||
|
||||
@ -64,7 +64,7 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsFetchingCheckoutSession(false);
|
||||
setCheckoutSessionPriceId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -122,7 +122,8 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
|
||||
|
||||
<Button
|
||||
className="mt-4"
|
||||
loading={isFetchingCheckoutSession}
|
||||
disabled={checkoutSessionPriceId !== null}
|
||||
loading={checkoutSessionPriceId === price.id}
|
||||
onClick={() => void onSubscribeClick(price.id)}
|
||||
>
|
||||
<Trans>Subscribe</Trans>
|
||||
|
||||