Compare commits
9 Commits
power-sign
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9182d014b4 | ||
|
|
5f602d897b | ||
|
|
0084a94bb1 | ||
|
|
a4f1a138d0 | ||
|
|
6a9ae132c7 | ||
|
|
afb156f073 | ||
|
|
f6a24224fe | ||
|
|
080bb405f0 | ||
|
|
8b1b0de935 |
@@ -1,4 +1,4 @@
|
|||||||
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="https://documen.so/sign-everywhere">The Platform Plan</a>!
|
> 🚨 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>
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,5 @@
|
|||||||
"solid": "Solid Integration",
|
"solid": "Solid Integration",
|
||||||
"preact": "Preact Integration",
|
"preact": "Preact Integration",
|
||||||
"angular": "Angular Integration",
|
"angular": "Angular Integration",
|
||||||
"css-variables": "CSS Variables",
|
"css-variables": "CSS Variables"
|
||||||
"web-components": "Web Components"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,15 +73,14 @@ These customization options are available for both Direct Templates and Signing
|
|||||||
|
|
||||||
We support embedding across a range of popular JavaScript frameworks, including:
|
We support embedding across a range of popular JavaScript frameworks, including:
|
||||||
|
|
||||||
| Framework | Package |
|
| Framework | Package |
|
||||||
| --------- | ---------------------------------------------------------------------------------- |
|
| --------- | ---------------------------------------------------------------------------------- |
|
||||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||||
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
||||||
| Web Components | [@documenso/embed-webcomponent](https://www.npmjs.com/package/@documenso/embed-webcomponent) |
|
|
||||||
|
|
||||||
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
||||||
|
|
||||||
@@ -167,7 +166,6 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
|||||||
- [Svelte](/developers/embedding/svelte)
|
- [Svelte](/developers/embedding/svelte)
|
||||||
- [Solid](/developers/embedding/solid)
|
- [Solid](/developers/embedding/solid)
|
||||||
- [Angular](/developers/embedding/angular)
|
- [Angular](/developers/embedding/angular)
|
||||||
- [Web Components](/developers/embedding/web-components)
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -179,5 +177,4 @@ If you're using **web components**, the integration process is slightly differen
|
|||||||
- [Solid Integration](/developers/embedding/solid)
|
- [Solid Integration](/developers/embedding/solid)
|
||||||
- [Preact Integration](/developers/embedding/preact)
|
- [Preact Integration](/developers/embedding/preact)
|
||||||
- [Angular Integration](/developers/embedding/angular)
|
- [Angular Integration](/developers/embedding/angular)
|
||||||
- [Web Components](/developers/embedding/web-components)
|
|
||||||
- [CSS Variables](/developers/embedding/css-variables)
|
- [CSS Variables](/developers/embedding/css-variables)
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
---
|
|
||||||
title: Web Components Integration
|
|
||||||
description: Learn how to use our embedding SDK via Web Components on a framework-less web application.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Web Components Integration
|
|
||||||
|
|
||||||
Our Web Components SDK provides a simple way to embed a signing experience within your framework-less web application. It supports both direct link templates and signing tokens.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install the SDK, run the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @documenso/embed-webcomponent
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in your html file, add the following to add the script, replacing the path with the proper path to the web component script.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="YOUR_PATH_HERE"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
|
|
||||||
|
|
||||||
### Direct Link Template
|
|
||||||
|
|
||||||
If you have a direct link template, you can simply provide the token for the template to the `documenso-embed-direct-template` tag.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<documenso-embed-direct-template
|
|
||||||
token="YOUR_TOKEN_HERE"
|
|
||||||
</documenso-embed-direct-template>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
|
||||||
| token | string | The token for the document you want to embed |
|
|
||||||
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
|
|
||||||
| name | string (optional) | The name the signer that will be used by default for signing |
|
|
||||||
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
|
|
||||||
| 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 |
|
|
||||||
| 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 |
|
|
||||||
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
|
|
||||||
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
|
|
||||||
|
|
||||||
### Signing Token
|
|
||||||
|
|
||||||
If you have a signing token, you can provide it to the `documenso-embed-sign-document` tag.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<documenso-embed-sign-document
|
|
||||||
token="YOUR_TOKEN_HERE"
|
|
||||||
</documenso-embed-sign-document>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
|
||||||
| token | string | The token for the document you want to embed |
|
|
||||||
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
|
|
||||||
| name | string (optional) | The name the signer that will be used by default for signing |
|
|
||||||
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Creating via JavaScript
|
|
||||||
|
|
||||||
You can also create the tag element using javascript, for dynamic generation of either modes. For example, this would add the sign document embed to the DOM.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
document.getElementById('my-wrapper-here').innerHTML = '';
|
|
||||||
|
|
||||||
const tag = document.createElement('documenso-embed-sign-document');
|
|
||||||
tag.setAttribute('token', data.token);
|
|
||||||
tag.style.width = '100%';
|
|
||||||
tag.style.height = '100%';
|
|
||||||
|
|
||||||
document.getElementById('my-wrapper-here').appendChild(tag);
|
|
||||||
```
|
|
||||||
@@ -21,25 +21,14 @@ Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) f
|
|||||||
|
|
||||||
## API V2 - Beta
|
## API V2 - Beta
|
||||||
|
|
||||||
<Callout type="warning">API V2 is currently beta, and will be subject to breaking changes</Callout>
|
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
|
||||||
|
|
||||||
Check out the [API V2 documentation](https://documen.so/api-v2-docs) for details about the API endpoints, request parameters, response formats, and authentication methods.
|
<Callout type="warning">
|
||||||
|
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
|
||||||
Our new API V2 supports the following typed SDKs:
|
|
||||||
|
|
||||||
- [TypeScript](https://github.com/documenso/sdk-typescript)
|
|
||||||
- [Python](https://github.com/documenso/sdk-python)
|
|
||||||
- [Go](https://github.com/documenso/sdk-go)
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
For the staging API, please use the following base URL:
|
|
||||||
`https://stg-app.documenso.dev/api/v2-beta/`
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
||||||
|
|
||||||
📖 [Documentation](https://documen.so/api-v2-docs)
|
|
||||||
|
|
||||||
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
||||||
|
|
||||||
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
|||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
SplitButton,
|
||||||
|
SplitButtonAction,
|
||||||
|
SplitButtonDropdown,
|
||||||
|
SplitButtonDropdownItem,
|
||||||
|
} from '@documenso/ui/primitives/split-button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentPageViewButtonProps = {
|
export type DocumentPageViewButtonProps = {
|
||||||
@@ -25,7 +31,9 @@ export type DocumentPageViewButtonProps = {
|
|||||||
team?: Pick<Team, 'id' | 'url'>;
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
export const DocumentPageViewButton = ({
|
||||||
|
document: activeDocument,
|
||||||
|
}: DocumentPageViewButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@@ -34,25 +42,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
const recipient = activeDocument.recipients.find(
|
||||||
|
(recipient) => recipient.email === session.user.email,
|
||||||
|
);
|
||||||
|
|
||||||
const isRecipient = !!recipient;
|
const isRecipient = !!recipient;
|
||||||
const isPending = document.status === DocumentStatus.PENDING;
|
const isPending = activeDocument.status === DocumentStatus.PENDING;
|
||||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
const isComplete = activeDocument.status === DocumentStatus.COMPLETED;
|
||||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
const role = recipient?.role;
|
const role = recipient?.role;
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(document.team?.url);
|
const documentsPath = formatDocumentsPath(activeDocument.team?.url);
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||||
{
|
{
|
||||||
documentId: document.id,
|
documentId: activeDocument.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
teamId: document.team?.id?.toString(),
|
teamId: activeDocument.team?.id?.toString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -63,7 +73,10 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
throw new Error('No document available');
|
throw new Error('No document available');
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: documentWithData.title });
|
await downloadPDF({
|
||||||
|
documentData,
|
||||||
|
fileName: documentWithData.title,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@@ -73,6 +86,100 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDownloadAuditLogClick = async () => {
|
||||||
|
try {
|
||||||
|
const { url } = await trpcClient.document.downloadAuditLogs.mutate({
|
||||||
|
documentId: activeDocument.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
|
src: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(iframe.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '0',
|
||||||
|
height: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLoaded = () => {
|
||||||
|
if (iframe.contentDocument?.readyState === 'complete') {
|
||||||
|
iframe.contentWindow?.print();
|
||||||
|
|
||||||
|
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||||
|
iframe.addEventListener('load', onLoaded);
|
||||||
|
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
onLoaded();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Something went wrong`),
|
||||||
|
description: _(
|
||||||
|
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownloadSigningCertificateClick = async () => {
|
||||||
|
try {
|
||||||
|
const { url } = await trpcClient.document.downloadCertificate.mutate({
|
||||||
|
documentId: activeDocument.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframe = Object.assign(document.createElement('iframe'), {
|
||||||
|
src: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(iframe.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '0',
|
||||||
|
height: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLoaded = () => {
|
||||||
|
if (iframe.contentDocument?.readyState === 'complete') {
|
||||||
|
iframe.contentWindow?.print();
|
||||||
|
|
||||||
|
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||||
|
iframe.addEventListener('load', onLoaded);
|
||||||
|
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
onLoaded();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Something went wrong`),
|
||||||
|
description: _(
|
||||||
|
msg`Sorry, we were unable to download the certificate. Please try again later.`,
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
isRecipient,
|
isRecipient,
|
||||||
isPending,
|
isPending,
|
||||||
@@ -106,16 +213,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
))
|
))
|
||||||
.with({ isComplete: false }, () => (
|
.with({ isComplete: false }, () => (
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
<Link href={`${documentsPath}/${document.id}/edit`}>
|
<Link href={`${documentsPath}/${activeDocument.id}/edit`}>
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<Button className="w-full" onClick={onDownloadClick}>
|
<SplitButton className="flex w-full">
|
||||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
<SplitButtonAction className="w-full" onClick={() => void onDownloadClick()}>
|
||||||
<Trans>Download</Trans>
|
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||||
</Button>
|
<Trans>Download</Trans>
|
||||||
|
</SplitButtonAction>
|
||||||
|
<SplitButtonDropdown>
|
||||||
|
<SplitButtonDropdownItem onClick={() => void onDownloadAuditLogClick()}>
|
||||||
|
<Trans>Only Audit Log</Trans>
|
||||||
|
</SplitButtonDropdownItem>
|
||||||
|
|
||||||
|
<SplitButtonDropdownItem onClick={() => void onDownloadSigningCertificateClick()}>
|
||||||
|
<Trans>Only Signing Certificate</Trans>
|
||||||
|
</SplitButtonDropdownItem>
|
||||||
|
</SplitButtonDropdown>
|
||||||
|
</SplitButton>
|
||||||
))
|
))
|
||||||
.otherwise(() => null);
|
.otherwise(() => null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -187,6 +187,8 @@ export const EditDocumentForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
|
includeSigningCertificate: data.includeSigningCertificate,
|
||||||
|
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
field.fieldMeta ?? {
|
|
||||||
type: 'checkbox',
|
|
||||||
values: [{ id: 1, checked: false, value: '' }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = parsedFieldMeta.values?.map((item) => ({
|
const values = parsedFieldMeta.values?.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { notFound } from 'next/navigation';
|
|||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { CheckCircle2, Clock8 } from 'lucide-react';
|
import { CheckCircle2, Clock8 } from 'lucide-react';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -15,12 +16,10 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
|||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
import { getNextInboxDocument } from '@documenso/lib/server-only/user/get-next-inbox-document';
|
|
||||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { NextInboxItemButton } from '@documenso/ui/components/document/next-inbox-item-button';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
@@ -62,10 +61,9 @@ export default async function CompletedSigningPage({
|
|||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
const [fields, recipient, nextInboxDocument] = await Promise.all([
|
const [fields, recipient] = await Promise.all([
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
getNextInboxDocument({ email: user?.email }).catch(() => null),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
@@ -93,7 +91,8 @@ export default async function CompletedSigningPage({
|
|||||||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
recipient.email;
|
recipient.email;
|
||||||
|
|
||||||
const isLoggedIn = !!user;
|
const sessionData = await getServerSession();
|
||||||
|
const isLoggedIn = !!sessionData?.user;
|
||||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -183,16 +182,12 @@ export default async function CompletedSigningPage({
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div
|
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||||
className={cn('mt-8 flex w-full items-center justify-center gap-4', {
|
|
||||||
'max-w-sm': !nextInboxDocument,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||||
|
|
||||||
{document.status === DocumentStatus.COMPLETED ? (
|
{document.status === DocumentStatus.COMPLETED ? (
|
||||||
<DocumentDownloadButton
|
<DocumentDownloadButton
|
||||||
className="flex-1 text-xs"
|
className="flex-1"
|
||||||
fileName={document.title}
|
fileName={document.title}
|
||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||||
@@ -204,15 +199,6 @@ export default async function CompletedSigningPage({
|
|||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoggedIn && nextInboxDocument && (
|
|
||||||
<NextInboxItemButton
|
|
||||||
className="text-xs"
|
|
||||||
userEmail={user?.email}
|
|
||||||
documentData={documentData}
|
|
||||||
nextInboxDocument={nextInboxDocument}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -234,7 +220,7 @@ export default async function CompletedSigningPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-4">
|
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-2">
|
||||||
<Trans>Go Back Home</Trans>
|
<Trans>Go Back Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -182,23 +182,6 @@ export const SigningFieldContainer = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
|
|
||||||
field.fieldMeta?.label && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
|
|
||||||
{
|
|
||||||
'bg-foreground/5 border-border border': !field.inserted,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'bg-documenso-200 border-primary border': field.inserted,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.fieldMeta.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</FieldRootContainer>
|
</FieldRootContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
|
|||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
typedSignatureEnabled: z.boolean(),
|
typedSignatureEnabled: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
|
includeAuditTrailLog: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
|
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
|
||||||
@@ -72,6 +73,7 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
||||||
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
|
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
|
||||||
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
||||||
|
includeAuditTrailLog: settings?.includeAuditTrailLog ?? false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
||||||
});
|
});
|
||||||
@@ -86,6 +88,7 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
|
includeAuditTrailLog,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
await updateTeamDocumentPreferences({
|
await updateTeamDocumentPreferences({
|
||||||
@@ -96,6 +99,7 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
|
includeAuditTrailLog,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,6 +304,37 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="includeAuditTrailLog"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Include the Audit Trail Log in the Document</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormControl className="block">
|
||||||
|
<Switch
|
||||||
|
ref={field.ref}
|
||||||
|
name={field.name}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans>
|
||||||
|
Controls whether the audit trail log will be included in the document when it is
|
||||||
|
downloaded. The audit trail log can still be downloaded from the logs page
|
||||||
|
separately.
|
||||||
|
</Trans>
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end space-x-4">
|
<div className="flex flex-row justify-end space-x-4">
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
<Trans>Save</Trans>
|
<Trans>Save</Trans>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type EmbedDirectTemplateClientPageProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhiteLabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDirectTemplateClientPage = ({
|
export const EmbedDirectTemplateClientPage = ({
|
||||||
@@ -60,7 +60,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhiteLabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
}: EmbedDirectTemplateClientPageProps) => {
|
}: EmbedDirectTemplateClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -288,7 +288,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhiteLabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@@ -349,7 +349,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
||||||
@@ -360,34 +360,19 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isExpanded ? (
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
<Button
|
{isExpanded ? (
|
||||||
variant="outline"
|
<LucideChevronDown
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
|
) : (
|
||||||
</Button>
|
<LucideChevronUp
|
||||||
) : pendingFields.length > 0 ? (
|
className="text-muted-foreground h-5 w-5"
|
||||||
<Button
|
onClick={() => setIsExpanded(true)}
|
||||||
variant="outline"
|
/>
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
)}
|
||||||
onClick={() => setIsExpanded(true)}
|
</Button>
|
||||||
>
|
|
||||||
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="md:hidden"
|
|
||||||
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={() => throttledOnCompleteClick()}
|
|
||||||
>
|
|
||||||
<Trans>Complete</Trans>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@@ -56,16 +55,12 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
documentAuth: template.authOptions,
|
documentAuth: template.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(template),
|
isDocumentPlatform(template),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: template.userId,
|
userId: template.userId,
|
||||||
teamId: template.teamId ?? undefined,
|
teamId: template.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: template.userId,
|
|
||||||
teamId: template.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||||
@@ -110,10 +105,8 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={template.templateMeta}
|
metadata={template.templateMeta}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
/>
|
/>
|
||||||
</RecipientProvider>
|
</RecipientProvider>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export type EmbedSignDocumentClientPageProps = {
|
|||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhitelabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhitelabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
}: EmbedSignDocumentClientPageProps) => {
|
}: EmbedSignDocumentClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@@ -212,7 +212,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhitelabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@@ -288,7 +288,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
@@ -303,36 +303,19 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isExpanded ? (
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
<Button
|
{isExpanded ? (
|
||||||
variant="outline"
|
<LucideChevronDown
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
) : (
|
||||||
</Button>
|
<LucideChevronUp
|
||||||
) : pendingFields.length > 0 ? (
|
className="text-muted-foreground h-5 w-5"
|
||||||
<Button
|
onClick={() => setIsExpanded(true)}
|
||||||
variant="outline"
|
/>
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
)}
|
||||||
onClick={() => setIsExpanded(true)}
|
</Button>
|
||||||
>
|
|
||||||
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="md:hidden"
|
|
||||||
disabled={
|
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
|
||||||
}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={() => throttledOnCompleteClick()}
|
|
||||||
>
|
|
||||||
<Trans>Complete</Trans>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@@ -63,16 +62,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
return <EmbedPaywall />;
|
return <EmbedPaywall />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(document),
|
isDocumentPlatform(document),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: document.userId,
|
userId: document.userId,
|
||||||
teamId: document.teamId ?? undefined,
|
teamId: document.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: document.userId,
|
|
||||||
teamId: document.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
@@ -131,10 +126,8 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={document.documentMeta}
|
metadata={document.documentMeta}
|
||||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@@ -363,6 +363,40 @@ export const DocumentHistorySheet = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
.with(
|
||||||
|
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED },
|
||||||
|
({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Old',
|
||||||
|
value: data.from,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'New',
|
||||||
|
value: data.to,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with(
|
||||||
|
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED },
|
||||||
|
({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Old',
|
||||||
|
value: data.from,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'New',
|
||||||
|
value: data.to,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
|
|
||||||
{isUserDetailsVisible && (
|
{isUserDetailsVisible && (
|
||||||
|
|||||||
11790
package-lock.json
generated
11790
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.1-rc.7",
|
"version": "1.9.1-rc.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { stripe } from '@documenso/lib/server-only/stripe';
|
|||||||
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
||||||
|
|
||||||
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
||||||
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
|
const planTypes = typeof plan === 'string' ? [plan] : plan;
|
||||||
|
|
||||||
const prices = await stripe.prices.list({
|
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||||
|
|
||||||
|
const { data: prices } = await stripe.prices.search({
|
||||||
|
query,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prices.data.filter(
|
return prices.filter((price) => price.type === 'recurring');
|
||||||
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import type { Subscription } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';
|
|
||||||
|
|
||||||
export type IsCommunityPlanOptions = {
|
|
||||||
userId: number;
|
|
||||||
teamId?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user or team is on the community plan.
|
|
||||||
*/
|
|
||||||
export const isCommunityPlan = async ({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
}: IsCommunityPlanOptions): Promise<boolean> => {
|
|
||||||
let subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
if (teamId) {
|
|
||||||
subscriptions = await prisma.team
|
|
||||||
.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: teamId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
owner: {
|
|
||||||
include: {
|
|
||||||
subscriptions: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((team) => team.owner.subscriptions);
|
|
||||||
} else {
|
|
||||||
subscriptions = await prisma.user
|
|
||||||
.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
subscriptions: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((user) => user.subscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriptions.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const communityPlanPriceIds = await getCommunityPlanPriceIds();
|
|
||||||
|
|
||||||
return subscriptionsContainsActivePlan(subscriptions, communityPlanPriceIds);
|
|
||||||
};
|
|
||||||
@@ -17,6 +17,7 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
documentLanguage: z.string(),
|
documentLanguage: z.string(),
|
||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
|
includeAuditTrailLog: z.boolean(),
|
||||||
brandingEnabled: z.boolean(),
|
brandingEnabled: z.boolean(),
|
||||||
brandingLogo: z.string(),
|
brandingLogo: z.string(),
|
||||||
brandingUrl: z.string(),
|
brandingUrl: z.string(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { signPdf } from '@documenso/signing';
|
|||||||
|
|
||||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||||
|
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||||
@@ -57,6 +58,7 @@ export const run = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
|
includeAuditTrailLog: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -121,13 +123,36 @@ export const run = async ({
|
|||||||
|
|
||||||
const pdfData = await getFile(documentData);
|
const pdfData = await getFile(documentData);
|
||||||
|
|
||||||
const certificateData =
|
let includeSigningCertificate;
|
||||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
|
||||||
? await getCertificatePdf({
|
if (document.teamId) {
|
||||||
documentId,
|
includeSigningCertificate =
|
||||||
language: document.documentMeta?.language,
|
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
||||||
}).catch(() => null)
|
} else {
|
||||||
: null;
|
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateData = includeSigningCertificate
|
||||||
|
? await getCertificatePdf({
|
||||||
|
documentId,
|
||||||
|
language: document.documentMeta?.language,
|
||||||
|
}).catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let includeAuditTrailLog;
|
||||||
|
|
||||||
|
if (document.teamId) {
|
||||||
|
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
||||||
|
} else {
|
||||||
|
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditLogData = includeAuditTrailLog
|
||||||
|
? await getAuditLogsPdf({
|
||||||
|
documentId,
|
||||||
|
language: document.documentMeta?.language,
|
||||||
|
}).catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||||
const pdfDoc = await PDFDocument.load(pdfData);
|
const pdfDoc = await PDFDocument.load(pdfData);
|
||||||
@@ -150,6 +175,16 @@ export const run = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auditLogData) {
|
||||||
|
const auditLog = await PDFDocument.load(auditLogData);
|
||||||
|
|
||||||
|
const auditLogPages = await pdfDoc.copyPages(auditLog, auditLog.getPageIndices());
|
||||||
|
|
||||||
|
auditLogPages.forEach((page) => {
|
||||||
|
pdfDoc.addPage(page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
await insertFieldInPDF(pdfDoc, field);
|
await insertFieldInPDF(pdfDoc, field);
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export const createDocument = async ({
|
|||||||
team?.teamGlobalSettings?.documentVisibility,
|
team?.teamGlobalSettings?.documentVisibility,
|
||||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||||
),
|
),
|
||||||
|
includeSigningCertificate: team?.teamGlobalSettings?.includeSigningCertificate ?? true,
|
||||||
|
includeAuditTrailLog: team?.teamGlobalSettings?.includeAuditTrailLog ?? true,
|
||||||
formValues,
|
formValues,
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
documentMeta: {
|
documentMeta: {
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export const findDocuments = async ({
|
|||||||
const searchFilter: Prisma.DocumentWhereInput = {
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
OR: [
|
OR: [
|
||||||
{ title: { contains: query, mode: 'insensitive' } },
|
{ title: { contains: query, mode: 'insensitive' } },
|
||||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
|
||||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
|||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putPdfFile } from '../../universal/upload/put-file';
|
import { putPdfFile } from '../../universal/upload/put-file';
|
||||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||||
|
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||||
import { flattenForm } from '../pdf/flatten-form';
|
import { flattenForm } from '../pdf/flatten-form';
|
||||||
@@ -61,6 +62,7 @@ export const sealDocument = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
|
includeAuditTrailLog: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -109,13 +111,36 @@ export const sealDocument = async ({
|
|||||||
// !: Need to write the fields onto the document as a hard copy
|
// !: Need to write the fields onto the document as a hard copy
|
||||||
const pdfData = await getFile(documentData);
|
const pdfData = await getFile(documentData);
|
||||||
|
|
||||||
const certificateData =
|
let includeSigningCertificate;
|
||||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
|
||||||
? await getCertificatePdf({
|
if (document.teamId) {
|
||||||
documentId,
|
includeSigningCertificate =
|
||||||
language: document.documentMeta?.language,
|
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
||||||
}).catch(() => null)
|
} else {
|
||||||
: null;
|
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateData = includeSigningCertificate
|
||||||
|
? await getCertificatePdf({
|
||||||
|
documentId,
|
||||||
|
language: document.documentMeta?.language,
|
||||||
|
}).catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let includeAuditTrailLog;
|
||||||
|
|
||||||
|
if (document.teamId) {
|
||||||
|
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
||||||
|
} else {
|
||||||
|
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditLogData = includeAuditTrailLog
|
||||||
|
? await getAuditLogsPdf({
|
||||||
|
documentId,
|
||||||
|
language: document.documentMeta?.language,
|
||||||
|
}).catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const doc = await PDFDocument.load(pdfData);
|
const doc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
@@ -134,6 +159,16 @@ export const sealDocument = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auditLogData) {
|
||||||
|
const auditLog = await PDFDocument.load(auditLogData);
|
||||||
|
|
||||||
|
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
||||||
|
|
||||||
|
auditLogPages.forEach((page) => {
|
||||||
|
doc.addPage(page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
await insertFieldInPDF(doc, field);
|
await insertFieldInPDF(doc, field);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
userId: userId,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
recipients: {
|
recipients: {
|
||||||
some: {
|
some: {
|
||||||
@@ -96,23 +88,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
},
|
},
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
teamId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export type UpdateDocumentOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility | null;
|
visibility?: DocumentVisibility | null;
|
||||||
|
includeSigningCertificate?: boolean;
|
||||||
|
includeAuditTrailLog?: boolean;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||||
};
|
};
|
||||||
@@ -156,6 +158,12 @@ export const updateDocument = async ({
|
|||||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||||
const isDocumentVisibilitySame =
|
const isDocumentVisibilitySame =
|
||||||
data.visibility === undefined || data.visibility === document.visibility;
|
data.visibility === undefined || data.visibility === document.visibility;
|
||||||
|
const isIncludeSigningCertificateSame =
|
||||||
|
data.includeSigningCertificate === undefined ||
|
||||||
|
data.includeSigningCertificate === document.includeSigningCertificate;
|
||||||
|
const isIncludeAuditTrailLogSame =
|
||||||
|
data.includeAuditTrailLog === undefined ||
|
||||||
|
data.includeAuditTrailLog === document.includeAuditTrailLog;
|
||||||
|
|
||||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||||
|
|
||||||
@@ -235,6 +243,34 @@ export const updateDocument = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isIncludeSigningCertificateSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED,
|
||||||
|
documentId,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: String(document.includeSigningCertificate),
|
||||||
|
to: String(data.includeSigningCertificate || false),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIncludeAuditTrailLogSame) {
|
||||||
|
auditLogs.push(
|
||||||
|
createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED,
|
||||||
|
documentId,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
data: {
|
||||||
|
from: String(document.includeAuditTrailLog),
|
||||||
|
to: String(data.includeAuditTrailLog || false),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Early return if nothing is required.
|
// Early return if nothing is required.
|
||||||
if (auditLogs.length === 0) {
|
if (auditLogs.length === 0) {
|
||||||
return document;
|
return document;
|
||||||
@@ -254,6 +290,8 @@ export const updateDocument = async ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId,
|
externalId: data.externalId,
|
||||||
visibility: data.visibility as DocumentVisibility,
|
visibility: data.visibility as DocumentVisibility,
|
||||||
|
includeSigningCertificate: data.includeSigningCertificate,
|
||||||
|
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||||
authOptions,
|
authOptions,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
74
packages/lib/server-only/htmltopdf/get-audit-logs-pdf.ts
Normal file
74
packages/lib/server-only/htmltopdf/get-audit-logs-pdf.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import type { Browser } from 'playwright';
|
||||||
|
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||||
|
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||||
|
|
||||||
|
export type GetAuditLogsPdfParams = {
|
||||||
|
documentId: number;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
language?: SupportedLanguageCodes | (string & {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfParams) => {
|
||||||
|
const { chromium } = await import('playwright');
|
||||||
|
|
||||||
|
const encryptedId = encryptSecondaryData({
|
||||||
|
data: documentId.toString(),
|
||||||
|
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
|
||||||
|
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
|
||||||
|
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
||||||
|
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
||||||
|
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
|
||||||
|
} else {
|
||||||
|
browser = await chromium.launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!browser) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserContext = await browser.newContext();
|
||||||
|
|
||||||
|
const page = await browserContext.newPage();
|
||||||
|
|
||||||
|
const lang = isValidLanguageCode(language) ? language : 'en';
|
||||||
|
|
||||||
|
await page.context().addCookies([
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
value: lang,
|
||||||
|
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await page.pdf({
|
||||||
|
format: 'A4',
|
||||||
|
});
|
||||||
|
|
||||||
|
await browserContext.close();
|
||||||
|
|
||||||
|
void browser.close();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await browserContext.close();
|
||||||
|
|
||||||
|
void browser.close();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ export type UpdateTeamDocumentSettingsOptions = {
|
|||||||
includeSenderDetails: boolean;
|
includeSenderDetails: boolean;
|
||||||
typedSignatureEnabled: boolean;
|
typedSignatureEnabled: boolean;
|
||||||
includeSigningCertificate: boolean;
|
includeSigningCertificate: boolean;
|
||||||
|
includeAuditTrailLog: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
|
includeAuditTrailLog,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
|
includeAuditTrailLog,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
documentVisibility,
|
documentVisibility,
|
||||||
@@ -68,6 +71,7 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
|
includeAuditTrailLog,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
type GetNextInboxDocumentOptions = {
|
|
||||||
email: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNextInboxDocument = async ({ email }: GetNextInboxDocumentOptions) => {
|
|
||||||
if (!email) {
|
|
||||||
throw new Error('User is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.document.findMany({
|
|
||||||
where: {
|
|
||||||
recipients: {
|
|
||||||
some: {
|
|
||||||
email,
|
|
||||||
signingStatus: SigningStatus.NOT_SIGNED,
|
|
||||||
role: {
|
|
||||||
not: RecipientRole.CC,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: { not: DocumentStatus.DRAFT },
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
title: true,
|
|
||||||
status: true,
|
|
||||||
recipients: {
|
|
||||||
where: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
token: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
documentMeta: true,
|
|
||||||
},
|
|
||||||
orderBy: [{ createdAt: 'asc' }],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -29,7 +29,9 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
|||||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
||||||
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
||||||
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
||||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated.
|
||||||
|
'DOCUMENT_SIGNING_CERTIFICATE_UPDATED', // When the include signing certificate is updated.
|
||||||
|
'DOCUMENT_AUDIT_TRAIL_UPDATED', // When the include audit trail is updated.
|
||||||
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
||||||
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
||||||
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
||||||
@@ -397,6 +399,16 @@ export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
|||||||
data: ZGenericFromToSchema,
|
data: ZGenericFromToSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED),
|
||||||
|
data: ZGenericFromToSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED),
|
||||||
|
data: ZGenericFromToSchema,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event: Document global authentication access updated.
|
* Event: Document global authentication access updated.
|
||||||
*/
|
*/
|
||||||
@@ -574,6 +586,8 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
|||||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||||
|
ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema,
|
||||||
|
ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { ZRecipientLiteSchema } from './recipient';
|
|||||||
*/
|
*/
|
||||||
export const ZDocumentSchema = DocumentSchema.pick({
|
export const ZDocumentSchema = DocumentSchema.pick({
|
||||||
visibility: true,
|
visibility: true,
|
||||||
|
includeSigningCertificate: true,
|
||||||
|
includeAuditTrailLog: true,
|
||||||
status: true,
|
status: true,
|
||||||
source: true,
|
source: true,
|
||||||
id: true,
|
id: true,
|
||||||
@@ -82,6 +84,8 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
|
|||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
|
includeSigningCertificate: true,
|
||||||
|
includeAuditTrailLog: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +108,8 @@ export const ZDocumentManySchema = DocumentSchema.pick({
|
|||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
|
includeSigningCertificate: true,
|
||||||
|
includeAuditTrailLog: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
user: UserSchema.pick({
|
user: UserSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -322,6 +322,14 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Document visibility updated`,
|
anonymous: msg`Document visibility updated`,
|
||||||
identified: msg`${prefix} updated the document visibility`,
|
identified: msg`${prefix} updated the document visibility`,
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED }, () => ({
|
||||||
|
anonymous: msg`Document signing certificate updated`,
|
||||||
|
identified: msg`${prefix} updated the document signing certificate`,
|
||||||
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED }, () => ({
|
||||||
|
anonymous: msg`Document audit trail updated`,
|
||||||
|
identified: msg`${prefix} updated the document audit trail`,
|
||||||
|
}))
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||||
anonymous: msg`Document access auth updated`,
|
anonymous: msg`Document access auth updated`,
|
||||||
identified: msg`${prefix} updated the document access auth requirements`,
|
identified: msg`${prefix} updated the document access auth requirements`,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" ADD COLUMN "includeAuditTrail" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `includeAuditTrail` on the `Document` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" DROP COLUMN "includeAuditTrail",
|
||||||
|
ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -311,30 +311,32 @@ enum DocumentVisibility {
|
|||||||
|
|
||||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||||
model Document {
|
model Document {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||||
visibility DocumentVisibility @default(EVERYONE)
|
visibility DocumentVisibility @default(EVERYONE)
|
||||||
title String
|
includeSigningCertificate Boolean @default(true)
|
||||||
status DocumentStatus @default(DRAFT)
|
includeAuditTrailLog Boolean @default(false)
|
||||||
recipients Recipient[]
|
title String
|
||||||
fields Field[]
|
status DocumentStatus @default(DRAFT)
|
||||||
shareLinks DocumentShareLink[]
|
recipients Recipient[]
|
||||||
documentDataId String
|
fields Field[]
|
||||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
shareLinks DocumentShareLink[]
|
||||||
documentMeta DocumentMeta?
|
documentDataId String
|
||||||
createdAt DateTime @default(now())
|
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
documentMeta DocumentMeta?
|
||||||
completedAt DateTime?
|
createdAt DateTime @default(now())
|
||||||
deletedAt DateTime?
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
teamId Int?
|
completedAt DateTime?
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
deletedAt DateTime?
|
||||||
templateId Int?
|
teamId Int?
|
||||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
source DocumentSource
|
templateId Int?
|
||||||
|
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||||
|
source DocumentSource
|
||||||
|
|
||||||
auditLogs DocumentAuditLog[]
|
auditLogs DocumentAuditLog[]
|
||||||
|
|
||||||
@@ -543,6 +545,7 @@ model TeamGlobalSettings {
|
|||||||
includeSenderDetails Boolean @default(true)
|
includeSenderDetails Boolean @default(true)
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
includeSigningCertificate Boolean @default(true)
|
includeSigningCertificate Boolean @default(true)
|
||||||
|
includeAuditTrailLog Boolean @default(false)
|
||||||
|
|
||||||
brandingEnabled Boolean @default(false)
|
brandingEnabled Boolean @default(false)
|
||||||
brandingLogo String @default("")
|
brandingLogo String @default("")
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const documentRouter = router({
|
|||||||
.input(ZGetDocumentByIdQuerySchema)
|
.input(ZGetDocumentByIdQuerySchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { teamId } = ctx;
|
const { teamId } = ctx;
|
||||||
const { documentId } = input;
|
const { documentId, includeCertificate, includeAuditLog } = input;
|
||||||
|
|
||||||
return await getDocumentById({
|
return await getDocumentById({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ export const ZDocumentVisibilitySchema = z
|
|||||||
.nativeEnum(DocumentVisibility)
|
.nativeEnum(DocumentVisibility)
|
||||||
.describe('The visibility of the document.');
|
.describe('The visibility of the document.');
|
||||||
|
|
||||||
|
export const ZDocumentIncludeSigningCertificateSchema = z
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.describe('Whether to include a signing certificate in the document.');
|
||||||
|
|
||||||
|
export const ZDocumentIncludeAuditTrailSchema = z
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.describe('Whether to include an audit trail in the document.');
|
||||||
|
|
||||||
export const ZDocumentMetaTimezoneSchema = z
|
export const ZDocumentMetaTimezoneSchema = z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -141,6 +151,8 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
|
|||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
|
includeCertificate: z.boolean().default(true).optional(),
|
||||||
|
includeAuditLog: z.boolean().default(true).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDuplicateDocumentRequestSchema = z.object({
|
export const ZDuplicateDocumentRequestSchema = z.object({
|
||||||
@@ -235,6 +247,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
|
includeSigningCertificate: ZDocumentIncludeSigningCertificateSchema.optional(),
|
||||||
|
includeAuditTrailLog: ZDocumentIncludeAuditTrailSchema.optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
|
|||||||
includeSenderDetails: z.boolean().optional().default(false),
|
includeSenderDetails: z.boolean().optional().default(false),
|
||||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||||
includeSigningCertificate: z.boolean().optional().default(true),
|
includeSigningCertificate: z.boolean().optional().default(true),
|
||||||
|
includeAuditTrailLog: z.boolean().optional().default(true),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
|
||||||
|
|
||||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
|
||||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
type FriendlyStatus = {
|
|
||||||
label: MessageDescriptor;
|
|
||||||
labelExtended: MessageDescriptor;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
|
||||||
PENDING: {
|
|
||||||
label: msg`Pending`,
|
|
||||||
labelExtended: msg`Document pending`,
|
|
||||||
icon: Clock,
|
|
||||||
color: 'text-blue-600 dark:text-blue-300',
|
|
||||||
},
|
|
||||||
COMPLETED: {
|
|
||||||
label: msg`Completed`,
|
|
||||||
labelExtended: msg`Document completed`,
|
|
||||||
icon: CheckCircle2,
|
|
||||||
color: 'text-green-500 dark:text-green-300',
|
|
||||||
},
|
|
||||||
DRAFT: {
|
|
||||||
label: msg`Draft`,
|
|
||||||
labelExtended: msg`Document draft`,
|
|
||||||
icon: File,
|
|
||||||
color: 'text-yellow-500 dark:text-yellow-200',
|
|
||||||
},
|
|
||||||
INBOX: {
|
|
||||||
label: msg`Inbox`,
|
|
||||||
labelExtended: msg`Document inbox`,
|
|
||||||
icon: SignatureIcon,
|
|
||||||
color: 'text-muted-foreground',
|
|
||||||
},
|
|
||||||
ALL: {
|
|
||||||
label: msg`All`,
|
|
||||||
labelExtended: msg`Document All`,
|
|
||||||
color: 'text-muted-foreground',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
|
||||||
status: ExtendedDocumentStatus;
|
|
||||||
inheritColor?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DocumentStatus = ({
|
|
||||||
className,
|
|
||||||
status,
|
|
||||||
inheritColor,
|
|
||||||
...props
|
|
||||||
}: DocumentStatusProps) => {
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={cn('flex items-center', className)} {...props}>
|
|
||||||
{Icon && (
|
|
||||||
<Icon
|
|
||||||
className={cn('mr-2 inline-block h-4 w-4', {
|
|
||||||
[color]: !inheritColor,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{_(label)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { CheckCircle, EyeIcon, Pencil } from 'lucide-react';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { type DocumentData, type Prisma, RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from '@documenso/ui/primitives/sheet';
|
|
||||||
|
|
||||||
import { DocumentStatus } from './document-status';
|
|
||||||
|
|
||||||
type GetNextInboxDocumentResult =
|
|
||||||
| Prisma.DocumentGetPayload<{
|
|
||||||
select: {
|
|
||||||
id: true;
|
|
||||||
createdAt: true;
|
|
||||||
title: true;
|
|
||||||
status: true;
|
|
||||||
recipients: {
|
|
||||||
select: {
|
|
||||||
token: true;
|
|
||||||
role: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
documentMeta: true;
|
|
||||||
};
|
|
||||||
}>[]
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export type NextInboxItemButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
|
||||||
disabled?: boolean;
|
|
||||||
documentData?: DocumentData;
|
|
||||||
userEmail: string | undefined;
|
|
||||||
nextInboxDocument: GetNextInboxDocumentResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NextInboxItemButton = ({
|
|
||||||
className,
|
|
||||||
documentData,
|
|
||||||
nextInboxDocument,
|
|
||||||
userEmail,
|
|
||||||
disabled,
|
|
||||||
...props
|
|
||||||
}: NextInboxItemButtonProps) => {
|
|
||||||
return (
|
|
||||||
<Sheet>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className={className}
|
|
||||||
disabled={disabled || !documentData || !userEmail}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SignatureIcon className="mr-2 h-5 w-5" />
|
|
||||||
<Trans>Sign Next Document</Trans>
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent>
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle className="text-2xl">Inbox</SheetTitle>
|
|
||||||
<SheetDescription>Documents awaiting your signature or review</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
|
|
||||||
<div className="mt-8 space-y-6">
|
|
||||||
{nextInboxDocument?.map((document) => {
|
|
||||||
const recipient = document.recipients[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={document.id} className="flex items-center justify-between space-y-1">
|
|
||||||
<div>
|
|
||||||
<p className="text-foreground text-lg font-semibold">{document.title}</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<DocumentStatus status={document.status} />
|
|
||||||
|
|
||||||
{document.createdAt && (
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
<Trans>
|
|
||||||
Created {DateTime.fromJSDate(document.createdAt).toFormat('LLL ‘yy')}
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button asChild className="w-28">
|
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
|
||||||
{match(recipient?.role)
|
|
||||||
.with(RecipientRole.SIGNER, () => (
|
|
||||||
<>
|
|
||||||
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
<Trans>Sign</Trans>
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
.with(RecipientRole.APPROVER, () => (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
<Trans>Approve</Trans>
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
.otherwise(() => (
|
|
||||||
<>
|
|
||||||
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
<Trans>View</Trans>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -78,7 +78,6 @@
|
|||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"vaul": "^1.0.0",
|
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
|
||||||
|
import { Checkbox } from '../checkbox';
|
||||||
import { Combobox } from '../combobox';
|
import { Combobox } from '../combobox';
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
||||||
@@ -92,6 +93,8 @@ export const AddSettingsFormPartial = ({
|
|||||||
visibility: document.visibility || '',
|
visibility: document.visibility || '',
|
||||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||||
|
includeSigningCertificate: document.includeSigningCertificate ?? true,
|
||||||
|
includeAuditTrailLog: document.includeAuditTrailLog ?? true,
|
||||||
meta: {
|
meta: {
|
||||||
timezone:
|
timezone:
|
||||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||||
@@ -259,6 +262,111 @@ export const AddSettingsFormPartial = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="globalActionAuth"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex flex-row items-center">
|
||||||
|
<Trans>Recipient action authentication</Trans>
|
||||||
|
<DocumentGlobalAuthActionTooltip />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Accordion type="multiple" className="mt-6">
|
||||||
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
|
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||||
|
<Trans>Certificates</Trans>
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
|
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="includeSigningCertificate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
className="h-5 w-5"
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="m-0 flex flex-row items-center">
|
||||||
|
<Trans>Include signing certificate</Trans>{' '}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||||
|
<Trans>
|
||||||
|
Including the signing certificate means that the certificate
|
||||||
|
will be attached to the document. You won't be able to remove
|
||||||
|
it. <br />
|
||||||
|
<br />
|
||||||
|
If you don't include it, you can download it individually.
|
||||||
|
</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="includeAuditTrailLog"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
className="h-5 w-5"
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="m-0 flex flex-row items-center">
|
||||||
|
<Trans>Include audit trail</Trans>{' '}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||||
|
<Trans>
|
||||||
|
Including the audit trail means that the log of all actions will
|
||||||
|
be attached to the document. You won't be able to remove it.{' '}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you don't include it, you can download it individually.
|
||||||
|
</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
{isDocumentEnterprise && (
|
{isDocumentEnterprise && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export const ZAddSettingsFormSchema = z.object({
|
|||||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
|
includeSigningCertificate: z.boolean().default(true).optional(),
|
||||||
|
includeAuditTrailLog: z.boolean().default(true).optional(),
|
||||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||||
ZDocumentAccessAuthTypesSchema.optional(),
|
ZDocumentAccessAuthTypesSchema.optional(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { match } from 'ts-pattern';
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { useSignerColors } from '../../lib/signer-colors';
|
import { useSignerColors } from '../../lib/signer-colors';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
@@ -186,35 +185,11 @@ export const FieldItem = ({
|
|||||||
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
||||||
[field.fieldMeta],
|
[field.fieldMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const radioHasValues = useMemo(
|
const radioHasValues = useMemo(
|
||||||
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
||||||
[field.fieldMeta],
|
[field.fieldMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
|
|
||||||
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === FieldType.RADIO) {
|
|
||||||
const parsed = ZRadioFieldMeta.parse(fieldMeta);
|
|
||||||
return parsed.values?.some((value) => value.checked) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === FieldType.CHECKBOX) {
|
|
||||||
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
|
|
||||||
return parsed.values?.some((value) => value.checked) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldHasCheckedValues = useMemo(
|
|
||||||
() => hasCheckedValues(field.fieldMeta, field.type),
|
|
||||||
[field.fieldMeta, field.type],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fixedSize = checkBoxHasValues || radioHasValues;
|
const fixedSize = checkBoxHasValues || radioHasValues;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -254,21 +229,6 @@ export const FieldItem = ({
|
|||||||
onMove?.(d.node);
|
onMove?.(d.node);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
|
|
||||||
field.fieldMeta?.label && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
|
|
||||||
{
|
|
||||||
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
|
|
||||||
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.fieldMeta.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full w-full items-center justify-center bg-white',
|
'relative flex h-full w-full items-center justify-center bg-white',
|
||||||
|
|||||||
@@ -126,18 +126,6 @@ export const CheckboxFieldAdvancedSettings = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="mb-2">
|
|
||||||
<Label>
|
|
||||||
<Trans>Label</Trans>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="label"
|
|
||||||
className="bg-background mt-2"
|
|
||||||
placeholder={_(msg`Field label`)}
|
|
||||||
value={fieldState.label}
|
|
||||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-x-4">
|
<div className="flex flex-row items-center gap-x-4">
|
||||||
<div className="flex w-2/3 flex-col">
|
<div className="flex w-2/3 flex-col">
|
||||||
<Label>
|
<Label>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||||
|
|
||||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||||
@@ -28,8 +27,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
handleFieldChange,
|
handleFieldChange,
|
||||||
handleErrors,
|
handleErrors,
|
||||||
}: RadioFieldAdvancedSettingsProps) => {
|
}: RadioFieldAdvancedSettingsProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const [showValidation, setShowValidation] = useState(false);
|
const [showValidation, setShowValidation] = useState(false);
|
||||||
const [values, setValues] = useState(
|
const [values, setValues] = useState(
|
||||||
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
||||||
@@ -105,18 +102,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
|
||||||
<Label>
|
|
||||||
<Trans>Label</Trans>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="label"
|
|
||||||
className="bg-background mt-2"
|
|
||||||
placeholder={_(msg`Field label`)}
|
|
||||||
value={fieldState.label}
|
|
||||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
|
|||||||
83
packages/ui/primitives/split-button.tsx
Normal file
83
packages/ui/primitives/split-button.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
import { Button } from './button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from './dropdown-menu';
|
||||||
|
|
||||||
|
const SplitButtonContext = React.createContext<{
|
||||||
|
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||||
|
size?: React.ComponentProps<typeof Button>['size'];
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const SplitButton = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||||
|
size?: React.ComponentProps<typeof Button>['size'];
|
||||||
|
}
|
||||||
|
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<SplitButtonContext.Provider value={{ variant, size }}>
|
||||||
|
<div ref={ref} className={cn('inline-flex', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SplitButtonContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SplitButton.displayName = 'SplitButton';
|
||||||
|
|
||||||
|
const SplitButtonAction = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { variant, size } = React.useContext(SplitButtonContext);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn('rounded-r-none border-r-0', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SplitButtonAction.displayName = 'SplitButtonAction';
|
||||||
|
|
||||||
|
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ children, ...props }, ref) => {
|
||||||
|
const { variant, size } = React.useContext(SplitButtonContext);
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className="rounded-l-none px-2 focus-visible:ring-offset-0"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More options</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" {...props} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
|
||||||
|
|
||||||
|
const SplitButtonDropdownItem = DropdownMenuItem;
|
||||||
|
|
||||||
|
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };
|
||||||
Reference in New Issue
Block a user