mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
76 Commits
v1.8.0-rc.
...
v1.9.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 98b2da5018 | |||
| fc1f76b543 | |||
| 22c9fb777b | |||
| 2da051a7f9 | |||
| 390a317bd3 | |||
| c161553d1d | |||
| c960a48b4f | |||
| 9502f4361d | |||
| 82deab41f4 | |||
| 2245812f0b | |||
| 861e9c976b | |||
| f55808199b | |||
| b4a7f1887d | |||
| f73441ee85 | |||
| d7de3b08c1 | |||
| 7d201f05d9 | |||
| a21ee2cea6 | |||
| 4ad46b81c9 | |||
| 10b8e785e0 | |||
| 5fbed783fc | |||
| c9fe134852 | |||
| f2149719e3 | |||
| 161d40cde7 | |||
| 76028771b8 | |||
| 5df1a6602e | |||
| 3d7b28a92b | |||
| ed862413b1 | |||
| 9d02ab4a5e | |||
| 34c0868d77 | |||
| fae9c0ca24 | |||
| dd162205fa | |||
| a88ae1cc1e | |||
| 904948e2bc | |||
| 3b6b96f551 | |||
| 67e49c82a3 | |||
| 9f45fe62e4 | |||
| 9e8094e34c | |||
| 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 |
@ -10,13 +10,7 @@
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"onCreateCommand": "./.devcontainer/on-create.sh",
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
54320,
|
||||
9000,
|
||||
2500,
|
||||
1100
|
||||
],
|
||||
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
@ -35,4 +29,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,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=
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="documen.so/sign-everywhere">The Platform Plan</a>!
|
||||
|
||||
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso-platform-plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
|
||||
|
||||
<p align="center" style="margin-top: 20px">
|
||||
|
||||
@ -1,36 +1 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3002](http://localhost:3002) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
# @documenso/documentation
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
9
apps/documentation/pages/developers/embedding/_meta.json
Normal file
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
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>
|
||||
```
|
||||
|
||||
@ -500,8 +500,35 @@ Now you can make a `POST` request to the `/api/v1/documents/{documentId}/fields`
|
||||
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.
|
||||
|
||||
@ -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,6 @@
|
||||
"signing-documents": "Signing Documents",
|
||||
"templates": "Templates",
|
||||
"direct-links": "Direct Signing Links",
|
||||
"document-visibility": "Document Visibility",
|
||||
"teams": "Teams",
|
||||
"-- Legal Overview": {
|
||||
"type": "separator",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"general-settings": "General Settings",
|
||||
"preferences": "Preferences",
|
||||
"document-visibility": "Document Visibility",
|
||||
"sender-details": "Email Sender Details"
|
||||
"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.
|
||||
@ -13,27 +13,29 @@ The default document visibility option allows you to control who can view and ac
|
||||
- **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 settings page](/users/teams/general-settings) 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>
|
||||
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.
|
||||
|
||||
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.
|
||||
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Everyone_", the document's visibility is set to "_EVERYONE_".
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- 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 the default document visibility ("_Admin_" or "_Managers and above_" in this case).
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Everyone_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Everyone_" or "_Managers and above_" in this case).
|
||||
- The user can change the visibility of the document to any of these options, except "_Admin_", in the document editor.
|
||||
- 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 "_Admin_".
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- If a user with the "_Admin_" role creates a document, and the default document visibility is set to "_Everyone_", "_Managers and above_", or "_Admin_", the document's visibility is set to the default document visibility.
|
||||
- The user can change the visibility of the document to any of these options in the document editor.
|
||||
|
||||
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
|
||||
Updating the default document visibility in the team's general preferences will not affect the
|
||||
visibility of existing documents. You will need to update the visibility of each document
|
||||
individually.
|
||||
</Callout>
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: General Settings
|
||||
description: Learn how to manage your team's General settings.
|
||||
---
|
||||
|
||||
# General Settings
|
||||
|
||||
You can manage your team's general settings by clicking on the **General Settings** tab in the team's settings dashboard.
|
||||
|
||||

|
||||
|
||||
The general settings 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).
|
||||
- **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).
|
||||
19
apps/documentation/pages/users/teams/preferences.mdx
Normal file
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).
|
||||
BIN
apps/documentation/public/teams/team-branding-preferences.webp
Normal file
BIN
apps/documentation/public/teams/team-branding-preferences.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
apps/documentation/public/teams/team-preferences.webp
Normal file
BIN
apps/documentation/public/teams/team-preferences.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@ -8,6 +8,59 @@ 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:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.8.0-rc.3",
|
||||
"version": "1.9.0-rc.5",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -47,7 +47,7 @@
|
||||
"recharts": "^2.7.2",
|
||||
"sharp": "0.32.6",
|
||||
"typescript": "5.2.2",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/loader": "^4.11.3",
|
||||
|
||||
BIN
apps/marketing/public/changelog/v1_8_0/reject-document.mp4
Normal file
BIN
apps/marketing/public/changelog/v1_8_0/reject-document.mp4
Normal file
Binary file not shown.
BIN
apps/marketing/public/changelog/v1_8_0/team-global-settings.jpeg
Normal file
BIN
apps/marketing/public/changelog/v1_8_0/team-global-settings.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
40
apps/openpage-api/.gitignore
vendored
Normal file
40
apps/openpage-api/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
1
apps/openpage-api/README.md
Normal file
1
apps/openpage-api/README.md
Normal file
@ -0,0 +1 @@
|
||||
# @documenso/openpage-api
|
||||
36
apps/openpage-api/app/community/route.ts
Normal file
36
apps/openpage-api/app/community/route.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
const paths = [
|
||||
{ path: '/total-prs', description: 'Total GitHub Merged PRs' },
|
||||
{ path: '/total-stars', description: 'Total GitHub Stars' },
|
||||
{ path: '/total-forks', description: 'Total GitHub Forks' },
|
||||
{ path: '/total-issues', description: 'Total GitHub Issues' },
|
||||
];
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.toString();
|
||||
const apis = paths.map(({ path, description }) => {
|
||||
return { path: url + path, description };
|
||||
});
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(apis), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/community/total-forks/route.ts
Normal file
27
apps/openpage-api/app/community/total-forks/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { transformData } from '@/lib/transform-data';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
|
||||
const data = await res.json();
|
||||
const transformedData = transformData({ data, metric: 'forks' });
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(transformedData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/community/total-issues/route.ts
Normal file
27
apps/openpage-api/app/community/total-issues/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { transformData } from '@/lib/transform-data';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
|
||||
const data = await res.json();
|
||||
const transformedData = transformData({ data, metric: 'openIssues' });
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(transformedData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/community/total-prs/route.ts
Normal file
27
apps/openpage-api/app/community/total-prs/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { transformData } from '@/lib/transform-data';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
|
||||
const data = await res.json();
|
||||
const transformedData = transformData({ data, metric: 'mergedPRs' });
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(transformedData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/community/total-stars/route.ts
Normal file
27
apps/openpage-api/app/community/total-stars/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { transformData } from '@/lib/transform-data';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
|
||||
const data = await res.json();
|
||||
const transformedData = transformData({ data, metric: 'stars' });
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(transformedData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/github/forks/route.ts
Normal file
25
apps/openpage-api/app/github/forks/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://api.github.com/repos/documenso/documenso');
|
||||
const { forks_count } = await res.json();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify({ data: forks_count }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/github/issues/route.ts
Normal file
27
apps/openpage-api/app/github/issues/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/search/issues?q=repo:documenso/documenso+type:issue+state:open&page=0&per_page=1',
|
||||
);
|
||||
const { total_count } = await res.json();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify({ data: total_count }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
27
apps/openpage-api/app/github/prs/route.ts
Normal file
27
apps/openpage-api/app/github/prs/route.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/search/issues?q=repo:documenso/documenso/+is:pr+merged:>=2010-01-01&page=0&per_page=1',
|
||||
);
|
||||
const { total_count } = await res.json();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify({ data: total_count }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
36
apps/openpage-api/app/github/route.ts
Normal file
36
apps/openpage-api/app/github/route.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
const paths = [
|
||||
{ path: '/forks', description: 'GitHub Forks' },
|
||||
{ path: '/stars', description: 'GitHub Stars' },
|
||||
{ path: '/issues', description: 'GitHub Merged Issues' },
|
||||
{ path: '/prs', description: 'GitHub Pull Requests' },
|
||||
];
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.toString();
|
||||
const apis = paths.map(({ path, description }) => {
|
||||
return { path: url + path, description };
|
||||
});
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(apis), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/github/stars/route.ts
Normal file
25
apps/openpage-api/app/github/stars/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://api.github.com/repos/documenso/documenso');
|
||||
const { stargazers_count } = await res.json();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify({ data: stargazers_count }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/growth/completed-documents/route.ts
Normal file
25
apps/openpage-api/app/growth/completed-documents/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getCompletedDocumentsMonthly } from '@/lib/growth/get-monthly-completed-document';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const completedDocuments = await getCompletedDocumentsMonthly();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(completedDocuments), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/growth/new-users/route.ts
Normal file
25
apps/openpage-api/app/growth/new-users/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const monthlyUsers = await getUserMonthlyGrowth();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(monthlyUsers), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
39
apps/openpage-api/app/growth/route.ts
Normal file
39
apps/openpage-api/app/growth/route.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
const paths = [
|
||||
{ path: '/total-customers', description: 'Total Customers' },
|
||||
{ path: '/total-users', description: 'Total Users' },
|
||||
{ path: '/new-users', description: 'New Users' },
|
||||
{ path: '/completed-documents', description: 'Completed Documents per Month' },
|
||||
{ path: '/total-completed-documents', description: 'Total Completed Documents' },
|
||||
{ path: '/signer-conversion', description: 'Signers That Signed Up' },
|
||||
{ path: '/total-signer-conversion', description: 'Total Signers That Signed Up' },
|
||||
];
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.toString();
|
||||
const apis = paths.map(({ path, description }) => {
|
||||
return { path: url + path, description };
|
||||
});
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(apis), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/growth/signer-conversion/route.ts
Normal file
25
apps/openpage-api/app/growth/signer-conversion/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getSignerConversionMonthly } from '@/lib/growth/get-signer-conversion';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const signers = await getSignerConversionMonthly();
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(signers), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getCompletedDocumentsMonthly } from '@/lib/growth/get-monthly-completed-document';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const totalCompletedDocuments = await getCompletedDocumentsMonthly('cumulative');
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(totalCompletedDocuments), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
31
apps/openpage-api/app/growth/total-customers/route.ts
Normal file
31
apps/openpage-api/app/growth/total-customers/route.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { transformData } from '@/lib/transform-data';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const res = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe');
|
||||
const EARLY_ADOPTERS_DATA = await res.json();
|
||||
|
||||
const transformedData = transformData({
|
||||
data: EARLY_ADOPTERS_DATA,
|
||||
metric: 'earlyAdopters',
|
||||
});
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(transformedData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getSignerConversionMonthly } from '@/lib/growth/get-signer-conversion';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const totalSigners = await getSignerConversionMonthly('cumulative');
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(totalSigners), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
25
apps/openpage-api/app/growth/total-users/route.ts
Normal file
25
apps/openpage-api/app/growth/total-users/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import cors from '@/lib/cors';
|
||||
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const totalUsers = await getUserMonthlyGrowth("cumulative");
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(totalUsers), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
35
apps/openpage-api/app/route.ts
Normal file
35
apps/openpage-api/app/route.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import cors from '@/lib/cors';
|
||||
|
||||
const paths = [
|
||||
{ path: 'github', description: 'GitHub Data' },
|
||||
{ path: 'community', description: 'Community Data' },
|
||||
{ path: 'growth', description: 'Growth Data' },
|
||||
];
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.toString();
|
||||
const apis = paths.map(({ path, description }) => {
|
||||
return { path: url + path, description };
|
||||
});
|
||||
|
||||
return cors(
|
||||
request,
|
||||
new Response(JSON.stringify(apis), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function OPTIONS(request: Request) {
|
||||
return cors(
|
||||
request,
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
}),
|
||||
);
|
||||
}
|
||||
135
apps/openpage-api/lib/cors.ts
Normal file
135
apps/openpage-api/lib/cors.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Multi purpose CORS lib.
|
||||
* Note: Based on the `cors` package in npm but using only web APIs.
|
||||
* Taken from: https://github.com/vercel/examples/blob/main/edge-functions/cors/lib/cors.ts
|
||||
*/
|
||||
|
||||
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
|
||||
|
||||
type OriginFn = (origin: string | undefined, req: Request) => StaticOrigin | Promise<StaticOrigin>;
|
||||
|
||||
interface CorsOptions {
|
||||
origin?: StaticOrigin | OriginFn;
|
||||
methods?: string | string[];
|
||||
allowedHeaders?: string | string[];
|
||||
exposedHeaders?: string | string[];
|
||||
credentials?: boolean;
|
||||
maxAge?: number;
|
||||
preflightContinue?: boolean;
|
||||
optionsSuccessStatus?: number;
|
||||
}
|
||||
|
||||
const defaultOptions: CorsOptions = {
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204,
|
||||
};
|
||||
|
||||
function isOriginAllowed(origin: string, allowed: StaticOrigin): boolean {
|
||||
return Array.isArray(allowed)
|
||||
? allowed.some((o) => isOriginAllowed(origin, o))
|
||||
: typeof allowed === 'string'
|
||||
? origin === allowed
|
||||
: allowed instanceof RegExp
|
||||
? allowed.test(origin)
|
||||
: !!allowed;
|
||||
}
|
||||
|
||||
function getOriginHeaders(reqOrigin: string | undefined, origin: StaticOrigin) {
|
||||
const headers = new Headers();
|
||||
|
||||
if (origin === '*') {
|
||||
headers.set('Access-Control-Allow-Origin', '*');
|
||||
} else if (typeof origin === 'string') {
|
||||
headers.set('Access-Control-Allow-Origin', origin);
|
||||
headers.append('Vary', 'Origin');
|
||||
} else {
|
||||
const allowed = isOriginAllowed(reqOrigin ?? '', origin);
|
||||
|
||||
if (allowed && reqOrigin) {
|
||||
headers.set('Access-Control-Allow-Origin', reqOrigin);
|
||||
}
|
||||
headers.append('Vary', 'Origin');
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginFn) {
|
||||
const reqOrigin = req.headers.get('Origin') || undefined;
|
||||
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
|
||||
|
||||
if (!value) return;
|
||||
return getOriginHeaders(reqOrigin, value);
|
||||
}
|
||||
|
||||
function getAllowedHeaders(req: Request, allowed?: string | string[]) {
|
||||
const headers = new Headers();
|
||||
|
||||
if (!allowed) {
|
||||
allowed = req.headers.get('Access-Control-Request-Headers')!;
|
||||
headers.append('Vary', 'Access-Control-Request-Headers');
|
||||
} else if (Array.isArray(allowed)) {
|
||||
allowed = allowed.join(',');
|
||||
}
|
||||
if (allowed) {
|
||||
headers.set('Access-Control-Allow-Headers', allowed);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export default async function cors(req: Request, res: Response, options?: CorsOptions) {
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
const { headers } = res;
|
||||
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
|
||||
const mergeHeaders = (v: string, k: string) => {
|
||||
if (k === 'Vary') headers.append(k, v);
|
||||
else headers.set(k, v);
|
||||
};
|
||||
|
||||
// If there's no origin we won't touch the response
|
||||
if (!originHeaders) return res;
|
||||
|
||||
originHeaders.forEach(mergeHeaders);
|
||||
|
||||
if (opts.credentials) {
|
||||
headers.set('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
const exposed = Array.isArray(opts.exposedHeaders)
|
||||
? opts.exposedHeaders.join(',')
|
||||
: opts.exposedHeaders;
|
||||
|
||||
if (exposed) {
|
||||
headers.set('Access-Control-Expose-Headers', exposed);
|
||||
}
|
||||
|
||||
// Handle the preflight request
|
||||
if (req.method === 'OPTIONS') {
|
||||
if (opts.methods) {
|
||||
const methods = Array.isArray(opts.methods) ? opts.methods.join(',') : opts.methods;
|
||||
|
||||
headers.set('Access-Control-Allow-Methods', methods);
|
||||
}
|
||||
|
||||
getAllowedHeaders(req, opts.allowedHeaders).forEach(mergeHeaders);
|
||||
|
||||
if (typeof opts.maxAge === 'number') {
|
||||
headers.set('Access-Control-Max-Age', String(opts.maxAge));
|
||||
}
|
||||
|
||||
if (opts.preflightContinue) return res;
|
||||
|
||||
headers.set('Content-Length', '0');
|
||||
return new Response(null, { status: opts.optionsSuccessStatus, headers });
|
||||
}
|
||||
|
||||
// If we got here, it's a normal request
|
||||
return res;
|
||||
}
|
||||
|
||||
export function initCors(options?: CorsOptions) {
|
||||
return async (req: Request, res: Response) => cors(req, res, options);
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
|
||||
const qb = kyselyPrisma.$kysely
|
||||
.selectFrom('Document')
|
||||
.select(({ fn }) => [
|
||||
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']).as('month'),
|
||||
fn.count('id').as('count'),
|
||||
fn
|
||||
.sum(fn.count('id'))
|
||||
// Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']) as any))
|
||||
.as('cume_count'),
|
||||
])
|
||||
.where(() => sql`"Document"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
|
||||
.groupBy('month')
|
||||
.orderBy('month', 'desc')
|
||||
.limit(12);
|
||||
|
||||
const result = await qb.execute();
|
||||
|
||||
const transformedData = {
|
||||
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
};
|
||||
|
||||
export type GetCompletedDocumentsMonthlyResult = Awaited<
|
||||
ReturnType<typeof getCompletedDocumentsMonthly>
|
||||
>;
|
||||
42
apps/openpage-api/lib/growth/get-signer-conversion.ts
Normal file
42
apps/openpage-api/lib/growth/get-signer-conversion.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = 'count') => {
|
||||
const qb = kyselyPrisma.$kysely
|
||||
.selectFrom('Recipient')
|
||||
.innerJoin('User', 'Recipient.email', 'User.email')
|
||||
.select(({ fn }) => [
|
||||
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'),
|
||||
fn.count('Recipient.email').distinct().as('count'),
|
||||
fn
|
||||
.sum(fn.count('Recipient.email').distinct())
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any))
|
||||
.as('cume_count'),
|
||||
])
|
||||
.where('Recipient.signedAt', 'is not', null)
|
||||
.where('Recipient.signedAt', '<', (eb) => eb.ref('User.createdAt'))
|
||||
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']))
|
||||
.orderBy('month', 'desc');
|
||||
|
||||
const result = await qb.execute();
|
||||
|
||||
const transformedData = {
|
||||
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
};
|
||||
|
||||
export type GetSignerConversionMonthlyResult = Awaited<
|
||||
ReturnType<typeof getSignerConversionMonthly>
|
||||
>;
|
||||
38
apps/openpage-api/lib/growth/get-user-monthly-growth.ts
Normal file
38
apps/openpage-api/lib/growth/get-user-monthly-growth.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => {
|
||||
const qb = kyselyPrisma.$kysely
|
||||
.selectFrom('User')
|
||||
.select(({ fn }) => [
|
||||
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'),
|
||||
fn.count('id').as('count'),
|
||||
fn
|
||||
.sum(fn.count('id'))
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any))
|
||||
.as('cume_count'),
|
||||
])
|
||||
.groupBy('month')
|
||||
.orderBy('month', 'desc')
|
||||
.limit(12);
|
||||
|
||||
const result = await qb.execute();
|
||||
|
||||
const transformedData = {
|
||||
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'New Users' : 'Total Users',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
};
|
||||
|
||||
export type GetUserMonthlyGrowthResult = Awaited<ReturnType<typeof getUserMonthlyGrowth>>;
|
||||
68
apps/openpage-api/lib/transform-data.ts
Normal file
68
apps/openpage-api/lib/transform-data.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type MetricKeys = {
|
||||
stars: number;
|
||||
forks: number;
|
||||
mergedPRs: number;
|
||||
openIssues: number;
|
||||
earlyAdopters: number;
|
||||
};
|
||||
|
||||
type DataEntry = {
|
||||
[key: string]: MetricKeys;
|
||||
};
|
||||
|
||||
type TransformData = {
|
||||
labels: string[];
|
||||
datasets: {
|
||||
label: string;
|
||||
data: number[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type MetricKey = keyof MetricKeys;
|
||||
|
||||
const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
|
||||
stars: 'Stars',
|
||||
forks: 'Forks',
|
||||
mergedPRs: 'Merged PRs',
|
||||
openIssues: 'Open Issues',
|
||||
earlyAdopters: 'Customers',
|
||||
};
|
||||
|
||||
export function transformData({
|
||||
data,
|
||||
metric,
|
||||
}: {
|
||||
data: DataEntry;
|
||||
metric: MetricKey;
|
||||
}): TransformData {
|
||||
const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => {
|
||||
const [yearA, monthA] = dateA.split('-').map(Number);
|
||||
const [yearB, monthB] = dateB.split('-').map(Number);
|
||||
|
||||
return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis();
|
||||
});
|
||||
|
||||
const labels = sortedEntries.map(([date]) => {
|
||||
const [year, month] = date.split('-');
|
||||
const dateTime = DateTime.fromObject({
|
||||
year: Number(year),
|
||||
month: Number(month),
|
||||
});
|
||||
return dateTime.toFormat('MMM yyyy');
|
||||
});
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`,
|
||||
data: sortedEntries.map(([_, stats]) => stats[metric]),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// To be on the safer side
|
||||
export const transformRepoStats = transformData;
|
||||
4
apps/openpage-api/next.config.js
Normal file
4
apps/openpage-api/next.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
module.exports = nextConfig;
|
||||
23
apps/openpage-api/package.json
Normal file
23
apps/openpage-api/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@documenso/openpage-api",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3003",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint:fix": "next lint --fix",
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "14.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.16.5",
|
||||
"@types/react": "18.3.5",
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
27
apps/openpage-api/tsconfig.json
Normal file
27
apps/openpage-api/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.8.0-rc.3",
|
||||
"version": "1.9.0-rc.5",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -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",
|
||||
@ -53,12 +54,13 @@
|
||||
"react-icons": "^4.11.0",
|
||||
"react-rnd": "^10.4.1",
|
||||
"recharts": "^2.7.2",
|
||||
"remeda": "^2.12.1",
|
||||
"remeda": "^2.17.3",
|
||||
"sharp": "0.32.6",
|
||||
"trpc-openapi": "^1.2.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uqr": "^0.1.2",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
|
||||
@ -40,7 +40,7 @@ export const AdminDocumentResults = () => {
|
||||
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
|
||||
trpc.admin.findDocuments.useQuery(
|
||||
{
|
||||
term: debouncedTerm,
|
||||
query: debouncedTerm,
|
||||
page: page || 1,
|
||||
perPage: perPage || 20,
|
||||
},
|
||||
|
||||
@ -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: sortBy === column && 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
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(
|
||||
|
||||
@ -47,7 +47,7 @@ export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButto
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||
id: document.id,
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query({
|
||||
id: document.id,
|
||||
documentId: document.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
|
||||
@ -37,10 +37,8 @@ export const DocumentPageViewRecentActivity = ({
|
||||
{
|
||||
documentId,
|
||||
filterForRecentActivity: true,
|
||||
orderBy: {
|
||||
column: 'createdAt',
|
||||
direction: 'asc',
|
||||
},
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
perPage: 10,
|
||||
},
|
||||
{
|
||||
|
||||
@ -60,7 +60,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: documentId,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
}).catch(() => null);
|
||||
@ -146,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>
|
||||
|
||||
@ -218,7 +221,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 px-4 text-sm ">
|
||||
<p className="text-muted-foreground mt-2 px-4 text-sm">
|
||||
{match(document.status)
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<Trans>This document has been signed by all recipients</Trans>
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
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';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type EditDocumentFormProps = {
|
||||
className?: string;
|
||||
initialDocument: DocumentWithDetails;
|
||||
initialDocument: TGetDocumentWithDetailsByIdResponse;
|
||||
documentRootPath: string;
|
||||
isDocumentEnterprise: boolean;
|
||||
};
|
||||
@ -63,7 +63,7 @@ export const EditDocumentForm = ({
|
||||
const { data: document, refetch: refetchDocument } =
|
||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
{
|
||||
@ -79,7 +79,7 @@ export const EditDocumentForm = ({
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||
@ -93,7 +93,7 @@ export const EditDocumentForm = ({
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||
@ -103,10 +103,10 @@ export const EditDocumentForm = ({
|
||||
|
||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newFields) => {
|
||||
onSuccess: ({ fields: newFields }) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
|
||||
@ -120,7 +120,7 @@ export const EditDocumentForm = ({
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({
|
||||
@ -134,10 +134,10 @@ export const EditDocumentForm = ({
|
||||
|
||||
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newRecipients) => {
|
||||
onSuccess: ({ recipients: newRecipients }) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
|
||||
@ -150,7 +150,7 @@ export const EditDocumentForm = ({
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
id: initialDocument.id,
|
||||
documentId: initialDocument.id,
|
||||
teamId: team?.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||
|
||||
@ -41,7 +41,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const document = await getDocumentWithDetailsById({
|
||||
id: documentId,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
}).catch(() => null);
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
@ -35,9 +35,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } =
|
||||
trpc.document.findDocumentAuditLogs.useQuery(
|
||||
|
||||
@ -47,7 +47,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
||||
|
||||
const [document, recipients] = await Promise.all([
|
||||
getDocumentById({
|
||||
id: documentId,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
}).catch(() => null),
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
|
||||
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
id: row.id,
|
||||
documentId: row.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -85,7 +85,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
id: row.id,
|
||||
documentId: row.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -9,8 +9,8 @@ import { DateTime } from 'luxon';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@ -24,13 +24,7 @@ import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||
import { DataTableTitle } from './data-table-title';
|
||||
|
||||
export type DocumentsDataTableProps = {
|
||||
results: FindResultSet<
|
||||
Document & {
|
||||
Recipient: Recipient[];
|
||||
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||
team: Pick<Team, 'id' | 'url'> | null;
|
||||
}
|
||||
>;
|
||||
results: TFindDocumentsResponse;
|
||||
showSenderColumn?: boolean;
|
||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -74,7 +76,7 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteDocument({ id, teamId });
|
||||
await deleteDocument({ documentId: id, teamId });
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -87,7 +89,7 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
setIsDeleteEnabled(event.target.value === _(msg`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`)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
perPage,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
query: search,
|
||||
});
|
||||
|
||||
const getTabHref = (value: typeof status) => {
|
||||
|
||||
@ -36,7 +36,7 @@ export const DuplicateDocumentDialog = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||
id,
|
||||
documentId: id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
@ -66,7 +66,7 @@ export const DuplicateDocumentDialog = ({
|
||||
|
||||
const onDuplicate = async () => {
|
||||
try {
|
||||
await duplicateDocument({ id, teamId: team?.id });
|
||||
await duplicateDocument({ documentId: id, teamId: team?.id });
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -92,7 +92,7 @@ export const DuplicateDocumentDialog = ({
|
||||
</h1>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll ">
|
||||
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
|
||||
<LazyPDFViewer key={document?.id} documentData={documentData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -41,7 +41,7 @@ export default async function BillingSettingsPage() {
|
||||
|
||||
const [subscriptions, prices, primaryAccountPlanPrices] = await Promise.all([
|
||||
getSubscriptionsByUserId({ userId: user.id }),
|
||||
getPricesByInterval({ plan: STRIPE_PLAN_TYPE.REGULAR }),
|
||||
getPricesByInterval({ plans: [STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.PLATFORM] }),
|
||||
getPrimaryAccountPlanPrices(),
|
||||
]);
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
|
||||
import { signOut } from 'next-auth/react';
|
||||
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -52,30 +51,20 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
||||
|
||||
return await signOut({ callbackUrl: '/' });
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description:
|
||||
err.message ??
|
||||
_(
|
||||
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
|
||||
@ -75,7 +75,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
|
||||
|
||||
const enabledPrivateDirectTemplates = useMemo(
|
||||
() =>
|
||||
(data?.templates ?? []).filter(
|
||||
(data?.data ?? []).filter(
|
||||
(template): template is DirectTemplate =>
|
||||
template.directLink?.enabled === true && template.type !== TemplateType.PUBLIC,
|
||||
),
|
||||
|
||||
@ -52,7 +52,7 @@ export const PublicTemplatesDataTable = () => {
|
||||
);
|
||||
|
||||
const { directTemplates, publicDirectTemplates, privateDirectTemplates } = useMemo(() => {
|
||||
const directTemplates = (data?.templates ?? []).filter(
|
||||
const directTemplates = (data?.data ?? []).filter(
|
||||
(template): template is DirectTemplate => template.directLink?.enabled === true,
|
||||
);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@ -33,9 +33,7 @@ export const UserSecurityActivityDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } =
|
||||
trpc.profile.findUserSecurityAuditLogs.useQuery(
|
||||
|
||||
@ -9,7 +9,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@ -27,9 +27,7 @@ export const UserPasskeysDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
|
||||
{
|
||||
|
||||
@ -59,16 +59,16 @@ export const EditTemplateForm = ({
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: template, refetch: refetchTemplate } =
|
||||
trpc.template.getTemplateWithDetailsById.useQuery(
|
||||
{
|
||||
id: initialTemplate.id,
|
||||
},
|
||||
{
|
||||
initialData: initialTemplate,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
|
||||
{
|
||||
templateId: initialTemplate.id,
|
||||
teamId: initialTemplate.teamId || undefined,
|
||||
},
|
||||
{
|
||||
initialData: initialTemplate,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
|
||||
const { Recipient: recipients, Field: fields, templateDocumentData } = template;
|
||||
|
||||
@ -92,12 +92,12 @@ export const EditTemplateForm = ({
|
||||
|
||||
const currentDocumentFlow = documentFlow[step];
|
||||
|
||||
const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplateSettings.useMutation({
|
||||
const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplate.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateWithDetailsById.setData(
|
||||
utils.template.getTemplateById.setData(
|
||||
{
|
||||
id: initialTemplate.id,
|
||||
templateId: initialTemplate.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||
);
|
||||
@ -108,9 +108,9 @@ export const EditTemplateForm = ({
|
||||
trpc.template.setSigningOrderForTemplate.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateWithDetailsById.setData(
|
||||
utils.template.getTemplateById.setData(
|
||||
{
|
||||
id: initialTemplate.id,
|
||||
templateId: initialTemplate.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||
);
|
||||
@ -120,9 +120,9 @@ export const EditTemplateForm = ({
|
||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateWithDetailsById.setData(
|
||||
utils.template.getTemplateById.setData(
|
||||
{
|
||||
id: initialTemplate.id,
|
||||
templateId: initialTemplate.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||
);
|
||||
@ -132,15 +132,32 @@ export const EditTemplateForm = ({
|
||||
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateWithDetailsById.setData(
|
||||
utils.template.getTemplateById.setData(
|
||||
{
|
||||
id: initialTemplate.id,
|
||||
templateId: initialTemplate.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateTypedSignature } =
|
||||
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.template.getTemplateById.setData(
|
||||
{
|
||||
templateId: initialTemplate.id,
|
||||
},
|
||||
(oldData) => ({
|
||||
...(oldData || initialTemplate),
|
||||
...newData,
|
||||
id: Number(newData.id),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||
try {
|
||||
await updateTemplateSettings({
|
||||
@ -211,6 +228,12 @@ export const EditTemplateForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateTypedSignature({
|
||||
templateId: template.id,
|
||||
teamId: team?.id,
|
||||
typedSignatureEnabled: data.typedSignatureEnabled,
|
||||
});
|
||||
|
||||
// Clear all field data from localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
@ -225,14 +248,13 @@ export const EditTemplateForm = ({
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
||||
router.refresh();
|
||||
|
||||
router.push(templateRootPath);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while adding signers.`),
|
||||
description: _(msg`An error occurred while adding fields.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -301,6 +323,7 @@ export const EditTemplateForm = ({
|
||||
fields={fields}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
teamId={team?.id}
|
||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||
/>
|
||||
</Stepper>
|
||||
</DocumentFlowFormContainer>
|
||||
|
||||
@ -8,7 +8,7 @@ import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
|
||||
@ -37,9 +37,10 @@ export const TemplateEditPageView = async ({ params, team }: TemplateEditPageVie
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const template = await getTemplateWithDetailsById({
|
||||
const template = await getTemplateById({
|
||||
id: templateId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!template || !template.templateDocumentData) {
|
||||
@ -63,7 +64,10 @@ export const TemplateEditPageView = async ({ params, team }: TemplateEditPageVie
|
||||
<Trans>Template</Trans>
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={template.title}
|
||||
>
|
||||
{template.title}
|
||||
</h1>
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@ -40,7 +40,7 @@ const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
|
||||
TEMPLATE_DIRECT_LINK: msg`Direct link`,
|
||||
};
|
||||
|
||||
const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
|
||||
const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({
|
||||
source: z
|
||||
.nativeEnum(DocumentSource)
|
||||
.optional()
|
||||
@ -49,10 +49,6 @@ const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
|
||||
.nativeEnum(DocumentStatusEnum)
|
||||
.optional()
|
||||
.catch(() => undefined),
|
||||
search: z.coerce
|
||||
.string()
|
||||
.optional()
|
||||
.catch(() => undefined),
|
||||
});
|
||||
|
||||
type TemplatePageViewDocumentsTableProps = {
|
||||
@ -69,7 +65,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZTemplateSearchParamsSchema.parse(
|
||||
const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
|
||||
@ -80,7 +76,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
teamId: team?.id,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
search: parsedSearchParams.search,
|
||||
query: parsedSearchParams.query,
|
||||
source: parsedSearchParams.source,
|
||||
status: parsedSearchParams.status,
|
||||
},
|
||||
|
||||
@ -26,10 +26,8 @@ export const TemplatePageViewRecentActivity = ({
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||
templateId,
|
||||
teamId,
|
||||
orderBy: {
|
||||
column: 'createdAt',
|
||||
direction: 'asc',
|
||||
},
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
perPage: 5,
|
||||
});
|
||||
|
||||
|
||||
@ -73,7 +73,6 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
||||
|
||||
const mockedDocumentMeta = templateMeta
|
||||
? {
|
||||
typedSignatureEnabled: false,
|
||||
...templateMeta,
|
||||
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
|
||||
documentId: 0,
|
||||
@ -89,7 +88,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
||||
|
||||
<div className="flex flex-row justify-between truncate">
|
||||
<div>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={template.title}
|
||||
>
|
||||
{template.title}
|
||||
</h1>
|
||||
|
||||
@ -155,7 +157,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 px-4 text-sm ">
|
||||
<p className="text-muted-foreground mt-2 px-4 text-sm">
|
||||
<Trans>Manage and view template</Trans>
|
||||
</p>
|
||||
|
||||
|
||||
@ -85,7 +85,7 @@ export const DeleteTemplateDialog = ({
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={isLoading}
|
||||
onClick={async () => deleteTemplate({ id, teamId })}
|
||||
onClick={async () => deleteTemplate({ templateId: id, teamId })}
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
|
||||
@ -4,8 +4,10 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -51,10 +53,20 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
||||
});
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (err) => {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
AppErrorCode.NOT_FOUND,
|
||||
() => msg`Template not found or already associated with a team.`,
|
||||
)
|
||||
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not a member of this team.`)
|
||||
.otherwise(() => msg`An error occurred while moving the template.`);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: error.message || _(msg`An error occurred while moving the template.`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@ -29,7 +29,7 @@ export const TemplatesPageView = async ({ searchParams = {}, team }: TemplatesPa
|
||||
const documentRootPath = formatDocumentsPath(team?.url);
|
||||
const templateRootPath = formatTemplatesPath(team?.url);
|
||||
|
||||
const { templates, totalPages } = await findTemplates({
|
||||
const { data: templates, totalPages } = await findTemplates({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
page: page,
|
||||
|
||||
@ -209,11 +209,19 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
|
||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${signature.Signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
{signature.Signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={`${signature.Signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{signature.Signature?.typedSignature && (
|
||||
<p className="font-signature text-center text-sm">
|
||||
{signature.Signature?.typedSignature}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm print:text-xs">
|
||||
|
||||
@ -9,7 +9,7 @@ import { Trans } from '@lingui/macro';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
import LogoIcon from '@documenso/assets/logo_icon.png';
|
||||
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -19,7 +19,7 @@ import { Logo } from '~/components/branding/logo';
|
||||
|
||||
type ProfileHeaderProps = {
|
||||
user?: User | null;
|
||||
teams?: GetTeamsResponse;
|
||||
teams?: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
export const ProfileHeader = ({ user, teams = [] }: ProfileHeaderProps) => {
|
||||
@ -58,7 +58,7 @@ export const ProfileHeader = ({ user, teams = [] }: ProfileHeaderProps) => {
|
||||
alt="Documenso Logo"
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-10 w-auto dark:invert sm:hidden"
|
||||
className="h-10 w-auto sm:hidden dark:invert"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirect
|
||||
export type ConfigureDirectTemplateFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
template: TemplateWithDetails;
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
directTemplateRecipient: Recipient & { Field: Field[] };
|
||||
initialEmail?: string;
|
||||
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
|
||||
|
||||
@ -28,7 +28,7 @@ import type { DirectTemplateLocalField } from './sign-direct-template';
|
||||
import { SignDirectTemplateForm } from './sign-direct-template';
|
||||
|
||||
export type TemplatesDirectPageViewProps = {
|
||||
template: TemplateWithDetails;
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
directTemplateToken: string;
|
||||
directTemplateRecipient: Recipient & { Field: Field[] };
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { truncateTitle } from '~/helpers/truncate-title';
|
||||
|
||||
import { DirectTemplatePageView } from './direct-template';
|
||||
import { DirectTemplateAuthPageView } from './signing-auth-page';
|
||||
@ -72,8 +71,11 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
|
||||
user={user}
|
||||
>
|
||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
|
||||
{truncateTitle(template.title)}
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={template.title}
|
||||
>
|
||||
{template.title}
|
||||
</h1>
|
||||
|
||||
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -56,7 +55,7 @@ export type SignDirectTemplateFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
directRecipient: Recipient;
|
||||
directRecipientFields: Field[];
|
||||
template: TemplateWithDetails;
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
|
||||
};
|
||||
|
||||
@ -72,9 +71,8 @@ export const SignDirectTemplateForm = ({
|
||||
template,
|
||||
onSubmit,
|
||||
}: SignDirectTemplateFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
||||
useRequiredSigningContext();
|
||||
|
||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
@ -102,9 +100,9 @@ export const SignDirectTemplateForm = ({
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
fieldId: 1,
|
||||
signatureImageAsBase64: value.value,
|
||||
typedSignature: null,
|
||||
};
|
||||
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
|
||||
typedSignature: value.value.startsWith('data:') ? null : value.value,
|
||||
} satisfies Signature;
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DATE) {
|
||||
@ -135,6 +133,8 @@ export const SignDirectTemplateForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
|
||||
const uninsertedFields = useMemo(() => {
|
||||
return sortFieldsByPosition(localFields.filter((field) => !field.inserted));
|
||||
}, [localFields]);
|
||||
@ -147,6 +147,10 @@ export const SignDirectTemplateForm = ({
|
||||
const handleSubmit = async () => {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
if (hasSignatureField && !signatureValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFieldsValid = validateFieldsInserted(localFields);
|
||||
|
||||
if (!isFieldsValid) {
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
|
||||
@ -23,7 +23,7 @@ export default async function RecipientLayout({ children }: RecipientLayoutProps
|
||||
|
||||
const { user, session } = await getServerComponentSession();
|
||||
|
||||
let teams: GetTeamsResponse = [];
|
||||
let teams: TGetTeamsResponse = [];
|
||||
|
||||
if (user && session) {
|
||||
teams = await getTeams({ userId: user.id });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user