Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5388f202b5 | ||
|
|
f1c91c4951 | ||
|
|
a5ef1d23e6 | ||
|
|
d91414697d | ||
|
|
e222a872d2 | ||
|
|
e3b0087be6 | ||
|
|
da89ce7c9a | ||
|
|
b762561f11 | ||
|
|
3bcd023b00 | ||
|
|
f6f66272b2 | ||
|
|
776cd3aa8b |
@@ -22,6 +22,15 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
|
||||
- `document.completed`
|
||||
- `document.rejected`
|
||||
- `document.cancelled`
|
||||
- `document.viewed`
|
||||
- `document.recipient.completed`
|
||||
- `document.downloaded`
|
||||
- `document.reminder.sent`
|
||||
- `template.created`
|
||||
- `template.updated`
|
||||
- `template.deleted`
|
||||
- `template.used`
|
||||
- `recipient.authentication.failed`
|
||||
|
||||
## Create a webhook subscription
|
||||
|
||||
@@ -38,7 +47,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
|
||||
To create a new webhook subscription, you need to provide the following information:
|
||||
|
||||
- Enter the webhook URL that will receive the event payload.
|
||||
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`.
|
||||
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`, `document.viewed`, `document.recipient.completed`, `document.downloaded`, `document.reminder.sent`, `template.created`, `template.updated`, `template.deleted`, `template.used`, `recipient.authentication.failed`.
|
||||
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
|
||||
|
||||

|
||||
@@ -634,6 +643,591 @@ Example payload for the `document.cancelled` event:
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `document.viewed` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_VIEWED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:50:26.174Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `document.recipient.completed` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_RECIPIENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:51:10.055Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 50,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer1@documenso.com",
|
||||
"name": "Signer 1",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:51:10.055Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer2@documenso.com",
|
||||
"name": "Signer 2",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 2,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:51:10.577Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `document.downloaded` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_DOWNLOADED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "COMPLETED",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:52:05.708Z",
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 51,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "Signer",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:53:18.577Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `document.reminder.sent` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_REMINDER_SENT",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "Signer",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T12:00:00.000Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `template.created` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_CREATED",
|
||||
"payload": {
|
||||
"id": 5,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "employment_contract.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:44:43.341Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": 2,
|
||||
"templateId": 5,
|
||||
"source": "TEMPLATE",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_456",
|
||||
"subject": "Employment Contract",
|
||||
"message": "Please review and sign your employment contract.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 25,
|
||||
"documentId": null,
|
||||
"templateId": 5,
|
||||
"email": "employee@company.com",
|
||||
"name": "Employee",
|
||||
"token": "TemplateToken123",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:44:44.779Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `template.updated` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_UPDATED",
|
||||
"payload": {
|
||||
"id": 5,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "employment_contract_v2.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T12:30:00.000Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": 2,
|
||||
"templateId": 5,
|
||||
"source": "TEMPLATE",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_456",
|
||||
"subject": "Employment Contract - Updated",
|
||||
"message": "Please review and sign your employment contract.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 25,
|
||||
"documentId": null,
|
||||
"templateId": 5,
|
||||
"email": "employee@company.com",
|
||||
"name": "Employee",
|
||||
"token": "TemplateToken123",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T12:30:01.000Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `template.deleted` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_DELETED",
|
||||
"payload": {
|
||||
"id": 5,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "employment_contract.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:44:43.341Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": 2,
|
||||
"templateId": 5,
|
||||
"source": "TEMPLATE",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_456",
|
||||
"subject": "Employment Contract",
|
||||
"message": "Please review and sign your employment contract.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 25,
|
||||
"documentId": null,
|
||||
"templateId": 5,
|
||||
"email": "employee@company.com",
|
||||
"name": "Employee",
|
||||
"token": "TemplateToken123",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T13:00:00.000Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `template.used` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_USED",
|
||||
"payload": {
|
||||
"id": 15,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "employment_contract.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "new_doc_data_123",
|
||||
"createdAt": "2024-04-22T14:00:00.000Z",
|
||||
"updatedAt": "2024-04-22T14:00:00.000Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": 2,
|
||||
"templateId": 5,
|
||||
"source": "TEMPLATE",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_789",
|
||||
"subject": "Employment Contract",
|
||||
"message": "Please review and sign your employment contract.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 60,
|
||||
"documentId": 15,
|
||||
"templateId": 5,
|
||||
"email": "newemployee@company.com",
|
||||
"name": "New Employee",
|
||||
"token": "DocToken456",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T14:00:01.000Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
Example payload for the `recipient.authentication.failed` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "RECIPIENT_AUTHENTICATION_FAILED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "documenso.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@documenso.com",
|
||||
"name": "Signer",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": {
|
||||
"accessAuth": "TWO_FACTOR_AUTH",
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:49:00.000Z",
|
||||
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook events testing
|
||||
|
||||
You can trigger test webhook events to test the webhook functionality. To do so, navigate to the webhook subscription details page and click the "Test" button.
|
||||
|
||||
@@ -149,7 +149,12 @@ export const EnvelopesBulkDeleteDialog = ({
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" disabled={isPending}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -40,21 +40,13 @@ type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
|
||||
export type FolderCreateDialogProps = {
|
||||
type: FolderType;
|
||||
trigger?: React.ReactNode;
|
||||
parentFolderId?: string | null;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const FolderCreateDialog = ({
|
||||
type,
|
||||
trigger,
|
||||
parentFolderId,
|
||||
...props
|
||||
}: FolderCreateDialogProps) => {
|
||||
export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDialogProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { folderId } = useParams();
|
||||
|
||||
const parentId = parentFolderId ?? folderId;
|
||||
|
||||
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation();
|
||||
@@ -70,7 +62,7 @@ export const FolderCreateDialog = ({
|
||||
try {
|
||||
await createFolder({
|
||||
name: data.name,
|
||||
parentId,
|
||||
parentId: folderId,
|
||||
type,
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export const OrganisationGroupDeleteDialog = ({
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
<Trans context="Removing group from organisation">
|
||||
You are about to remove the following group from{' '}
|
||||
<span className="font-semibold">{organisation.name}</span>.
|
||||
</Trans>
|
||||
|
||||
@@ -81,7 +81,7 @@ export const TeamGroupDeleteDialog = ({
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
<Trans context="Removing group from team">
|
||||
You are about to remove the following group from{' '}
|
||||
<span className="font-semibold">{team.name}</span>.
|
||||
</Trans>
|
||||
|
||||
@@ -53,10 +53,6 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
||||
const rootPath =
|
||||
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
|
||||
|
||||
if (parentId) {
|
||||
return `${rootPath}/folders?parentId=${parentId}`;
|
||||
}
|
||||
|
||||
return `${rootPath}/folders`;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useLingui as useLinguiMacro } from '@lingui/react/macro';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import type { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@documenso/ui/primitives/hover-card';
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
type AdminUserTeamsTableProps = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const AdminUserTeamsTable = ({ userId }: AdminUserTeamsTableProps) => {
|
||||
const { i18n } = useLingui();
|
||||
const { t } = useLinguiMacro();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.admin.user.findTeams.useQuery({
|
||||
userId,
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
});
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
updateSearchParams({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
};
|
||||
|
||||
const results = data ?? {
|
||||
data: [],
|
||||
perPage: 10,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: t`Team`,
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="cursor-default underline decoration-dotted underline-offset-4">
|
||||
{row.original.name}
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-auto font-mono text-xs text-muted-foreground"
|
||||
align="start"
|
||||
>
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
||||
<dt>id</dt>
|
||||
<dd>{row.original.id}</dd>
|
||||
<dt>url</dt>
|
||||
<dd>{row.original.url}</dd>
|
||||
</dl>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t`Organisation`,
|
||||
accessorKey: 'organisation',
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={`/admin/organisations/${row.original.organisation.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{row.original.organisation.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t`Role`,
|
||||
accessorKey: 'teamRole',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="neutral">
|
||||
{i18n._(TEAM_MEMBER_ROLE_MAP[row.original.teamRole as TeamMemberRole])}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t`Created At`,
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={results.data}
|
||||
perPage={results.perPage}
|
||||
currentPage={results.currentPage}
|
||||
totalPages={results.totalPages}
|
||||
onPaginationChange={onPaginationChange}
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
}}
|
||||
skeleton={{
|
||||
enable: isLoading,
|
||||
rows: 3,
|
||||
component: (
|
||||
<>
|
||||
<TableCell className="py-4 pr-4">
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-12 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
</TableCell>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(table) =>
|
||||
table.getPageCount() > 1 ? (
|
||||
<DataTablePagination additionalInformation="VisibleCount" table={table} />
|
||||
) : null
|
||||
}
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export const EnvelopesTableBulkActionBar = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-x-4 rounded-lg border border-border bg-widget px-4 py-3 shadow-lg">
|
||||
<div className="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-x-4 rounded-lg border border-border bg-background px-4 py-3 shadow-lg">
|
||||
<span className="text-sm font-medium">
|
||||
<Trans>{selectedCount} selected</Trans>
|
||||
</span>
|
||||
@@ -36,13 +36,7 @@ export const EnvelopesTableBulkActionBar = ({
|
||||
<Trans>Move to Folder</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onDeleteClick}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Button type="button" variant="destructive" size="sm" onClick={onDeleteClick}>
|
||||
<Trash2Icon className="mr-2 h-4 w-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -10,6 +10,12 @@ import type { z } from 'zod';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||
import { ZUpdateUserRequestSchema } from '@documenso/trpc/server/admin-router/update-user.types';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@documenso/ui/primitives/accordion';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -30,6 +36,7 @@ import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-di
|
||||
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
|
||||
import { AdminUserTeamsTable } from '~/components/tables/admin-user-teams-table';
|
||||
|
||||
import { MultiSelectRoleCombobox } from '../../../components/general/multiselect-role-combobox';
|
||||
|
||||
@@ -197,7 +204,7 @@ const AdminUserPage = ({ user }: { user: TGetUserResponse }) => {
|
||||
<h3 className="text-lg font-semibold leading-none tracking-tight">
|
||||
<Trans>User Organisations</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1.5 text-sm">
|
||||
<p className="mt-1.5 text-sm text-muted-foreground">
|
||||
<Trans>Organisations that the user is a member of.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -219,6 +226,28 @@ const AdminUserPage = ({ user }: { user: TGetUserResponse }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="team-memberships" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-semibold leading-none tracking-tight">
|
||||
<Trans>Team Memberships</Trans>
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm font-normal text-muted-foreground">
|
||||
<Trans>Teams that this user is a member of and their roles.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mt-4">
|
||||
<AdminUserTeamsTable userId={user.id} />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="mt-16 flex flex-col gap-4">
|
||||
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
|
||||
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useParams, useSearchParams } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useSessionStorage } from '@documenso/lib/client-only/hooks/use-session-storage';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
||||
@@ -58,7 +59,10 @@ export default function DocumentsPage() {
|
||||
const [isMovingDocument, setIsMovingDocument] = useState(false);
|
||||
const [documentToMove, setDocumentToMove] = useState<number | null>(null);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = useSessionStorage<RowSelectionState>(
|
||||
'documents-bulk-selection',
|
||||
{},
|
||||
);
|
||||
const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
|
||||
@@ -121,11 +125,6 @@ export default function DocumentsPage() {
|
||||
}
|
||||
}, [data?.stats]);
|
||||
|
||||
// Clear selection when navigation or filters change
|
||||
useEffect(() => {
|
||||
setRowSelection({});
|
||||
}, [folderId, findDocumentSearchParams]);
|
||||
|
||||
return (
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.DOCUMENT}>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { FolderIcon, HomeIcon, Loader2, SearchIcon } from 'lucide-react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
||||
@@ -25,10 +26,8 @@ export function meta() {
|
||||
export default function DocumentsFoldersPage() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useCurrentTeam();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const parentId = searchParams.get('parentId');
|
||||
|
||||
const [isMovingFolder, setIsMovingFolder] = useState(false);
|
||||
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
|
||||
@@ -40,51 +39,44 @@ export default function DocumentsFoldersPage() {
|
||||
|
||||
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
|
||||
type: FolderType.DOCUMENT,
|
||||
parentId: parentId,
|
||||
parentId: null,
|
||||
});
|
||||
|
||||
const navigateToFolder = (folderId?: string | null) => {
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
if (folderId) {
|
||||
void navigate(`${documentsPath}/f/${folderId}`);
|
||||
} else {
|
||||
void navigate(documentsPath);
|
||||
}
|
||||
};
|
||||
|
||||
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
|
||||
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const formatBreadCrumbPath = (folderId: string) => {
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
return `${documentsPath}/f/${folderId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-1 items-center text-sm font-medium text-muted-foreground">
|
||||
<Link
|
||||
to={formatDocumentsPath(team.url)}
|
||||
className="flex items-center hover:text-muted-foreground/80"
|
||||
<div className="flex flex-1 items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
|
||||
onClick={() => navigateToFolder(null)}
|
||||
>
|
||||
<HomeIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Home</Trans>
|
||||
</Link>
|
||||
|
||||
{foldersData?.breadcrumbs.map((folder) => (
|
||||
<div key={folder.id} className="flex items-center">
|
||||
<span className="px-3">/</span>
|
||||
<Link
|
||||
to={formatBreadCrumbPath(folder.id)}
|
||||
className="flex items-center hover:text-muted-foreground/80"
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
<span>{folder.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<HomeIcon className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
|
||||
<FolderCreateDialog type={FolderType.DOCUMENT} parentFolderId={parentId} />
|
||||
<FolderCreateDialog type={FolderType.DOCUMENT} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md py-6">
|
||||
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
|
||||
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
|
||||
<Input
|
||||
placeholder={t`Search folders...`}
|
||||
value={searchTerm}
|
||||
@@ -99,7 +91,7 @@ export default function DocumentsFoldersPage() {
|
||||
|
||||
{isFoldersLoading ? (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -134,14 +126,14 @@ export default function DocumentsFoldersPage() {
|
||||
|
||||
<div>
|
||||
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
|
||||
<div className="mt-6 text-center text-muted-foreground">
|
||||
<div className="text-muted-foreground mt-6 text-center">
|
||||
<Trans>No folders found matching "{searchTerm}"</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{foldersData?.folders
|
||||
.filter((folder) => !folder.pinned && isFolderMatchingSearch(folder))
|
||||
.filter((folder) => !folder.pinned)
|
||||
.map((folder) => (
|
||||
<FolderCard
|
||||
key={folder.id}
|
||||
|
||||
@@ -207,7 +207,9 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
|
||||
directTemplates={enabledPrivateDirectTemplates}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
<Trans>Link template</Trans>
|
||||
<Trans context="Action button to link template to public profile">
|
||||
Link template
|
||||
</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Bird } from 'lucide-react';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
|
||||
import { useSessionStorage } from '@documenso/lib/client-only/hooks/use-session-storage';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
@@ -34,7 +35,10 @@ export default function TemplatesPage() {
|
||||
const page = Number(searchParams.get('page')) || 1;
|
||||
const perPage = Number(searchParams.get('perPage')) || 10;
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [rowSelection, setRowSelection] = useSessionStorage<RowSelectionState>(
|
||||
'templates-bulk-selection',
|
||||
{},
|
||||
);
|
||||
const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
|
||||
@@ -51,11 +55,6 @@ export default function TemplatesPage() {
|
||||
folderId,
|
||||
});
|
||||
|
||||
// Clear selection when navigation or filters change
|
||||
useEffect(() => {
|
||||
setRowSelection({});
|
||||
}, [folderId, page, perPage]);
|
||||
|
||||
return (
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.TEMPLATE}>
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { FolderIcon, HomeIcon, Loader2, SearchIcon } from 'lucide-react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
||||
@@ -25,10 +26,8 @@ export function meta() {
|
||||
export default function TemplatesFoldersPage() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useCurrentTeam();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const parentId = searchParams.get('parentId');
|
||||
|
||||
const [isMovingFolder, setIsMovingFolder] = useState(false);
|
||||
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
|
||||
@@ -40,51 +39,44 @@ export default function TemplatesFoldersPage() {
|
||||
|
||||
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
|
||||
type: FolderType.TEMPLATE,
|
||||
parentId: parentId,
|
||||
parentId: null,
|
||||
});
|
||||
|
||||
const navigateToFolder = (folderId?: string | null) => {
|
||||
const templatesPath = formatTemplatesPath(team.url);
|
||||
|
||||
if (folderId) {
|
||||
void navigate(`${templatesPath}/f/${folderId}`);
|
||||
} else {
|
||||
void navigate(templatesPath);
|
||||
}
|
||||
};
|
||||
|
||||
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
|
||||
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const formatBreadCrumbPath = (folderId: string) => {
|
||||
const templatesPath = formatTemplatesPath(team.url);
|
||||
|
||||
return `${templatesPath}/f/${folderId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-1 items-center text-sm font-medium text-muted-foreground">
|
||||
<Link
|
||||
to={formatTemplatesPath(team.url)}
|
||||
className="flex items-center hover:text-muted-foreground/80"
|
||||
<div className="flex flex-1 items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
|
||||
onClick={() => navigateToFolder(null)}
|
||||
>
|
||||
<HomeIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Home</Trans>
|
||||
</Link>
|
||||
|
||||
{foldersData?.breadcrumbs.map((folder) => (
|
||||
<div key={folder.id} className="flex items-center">
|
||||
<span className="px-3">/</span>
|
||||
<Link
|
||||
to={formatBreadCrumbPath(folder.id)}
|
||||
className="flex items-center hover:text-muted-foreground/80"
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
<span>{folder.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<HomeIcon className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
|
||||
<FolderCreateDialog type={FolderType.TEMPLATE} parentFolderId={parentId} />
|
||||
<FolderCreateDialog type={FolderType.TEMPLATE} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md py-6">
|
||||
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
|
||||
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
|
||||
<Input
|
||||
placeholder={t`Search folders...`}
|
||||
value={searchTerm}
|
||||
@@ -98,8 +90,8 @@ export default function TemplatesFoldersPage() {
|
||||
</h1>
|
||||
|
||||
{isFoldersLoading ? (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<div className="mt- flex justify-center">
|
||||
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -134,7 +126,7 @@ export default function TemplatesFoldersPage() {
|
||||
|
||||
<div>
|
||||
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
|
||||
<div className="mt-6 text-center text-muted-foreground">
|
||||
<div className="text-muted-foreground mt-6 text-center">
|
||||
<Trans>No folders found matching "{searchTerm}"</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
function dispatchStorageEvent(key: string, newValue: string | null) {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key, newValue }));
|
||||
}
|
||||
|
||||
const setSessionStorageItem = <T>(key: string, value: T) => {
|
||||
const stringifiedValue = JSON.stringify(value);
|
||||
window.sessionStorage.setItem(key, stringifiedValue);
|
||||
dispatchStorageEvent(key, stringifiedValue);
|
||||
};
|
||||
|
||||
const removeSessionStorageItem = (key: string) => {
|
||||
window.sessionStorage.removeItem(key);
|
||||
dispatchStorageEvent(key, null);
|
||||
};
|
||||
|
||||
const getSessionStorageItem = (key: string) => {
|
||||
return window.sessionStorage.getItem(key);
|
||||
};
|
||||
|
||||
const useSessionStorageSubscribe = (callback: (event: StorageEvent) => void) => {
|
||||
window.addEventListener('storage', callback);
|
||||
return () => window.removeEventListener('storage', callback);
|
||||
};
|
||||
|
||||
export function useSessionStorage<T>(
|
||||
key: string,
|
||||
initialValue: T,
|
||||
): [T, Dispatch<SetStateAction<T>>] {
|
||||
const serializedInitialValue = JSON.stringify(initialValue);
|
||||
|
||||
const getSnapshot = () => getSessionStorageItem(key);
|
||||
const getServerSnapshot = () => serializedInitialValue;
|
||||
|
||||
const store = React.useSyncExternalStore(
|
||||
useSessionStorageSubscribe,
|
||||
getSnapshot,
|
||||
getServerSnapshot,
|
||||
);
|
||||
|
||||
const setState: Dispatch<SetStateAction<T>> = React.useCallback(
|
||||
(v) => {
|
||||
try {
|
||||
const prevValue = store ? JSON.parse(store) : initialValue;
|
||||
// @ts-expect-error - SetStateAction function check is safe at runtime
|
||||
const nextState = typeof v === 'function' ? v(prevValue) : v;
|
||||
|
||||
if (nextState === undefined || nextState === null) {
|
||||
removeSessionStorageItem(key);
|
||||
} else {
|
||||
setSessionStorageItem(key, nextState);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
[key, store, initialValue],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (getSessionStorageItem(key) === null && typeof initialValue !== 'undefined') {
|
||||
setSessionStorageItem(key, initialValue);
|
||||
}
|
||||
}, [key, initialValue]);
|
||||
|
||||
return [store ? JSON.parse(store) : initialValue, setState];
|
||||
}
|
||||
@@ -106,7 +106,9 @@ export const completeDocumentWithToken = async ({
|
||||
}
|
||||
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({
|
||||
token: recipient.token,
|
||||
});
|
||||
|
||||
if (!isRecipientsTurn) {
|
||||
throw new Error(
|
||||
@@ -195,6 +197,18 @@ export const completeDocumentWithToken = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
const envelopeForFailure = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: envelope.id },
|
||||
include: { documentMeta: true, recipients: true },
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeForFailure)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.TWO_FACTOR_AUTH_FAILED, {
|
||||
message: 'Invalid 2FA authentication',
|
||||
});
|
||||
@@ -283,6 +297,18 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const envelopeWithRelations = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: envelope.id },
|
||||
include: { documentMeta: true, recipients: true },
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.recipient.signed.email',
|
||||
payload: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
@@ -24,12 +25,17 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
@@ -232,5 +238,12 @@ export const resendDocument = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
return envelope;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
@@ -56,6 +55,13 @@ export const viewedDocument = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_VIEWED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
// Early return if already opened.
|
||||
if (recipient.readStatus === ReadStatus.OPENED) {
|
||||
return;
|
||||
|
||||
@@ -616,6 +616,13 @@ export const createEnvelope = async ({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
} else if (type === EnvelopeType.TEMPLATE) {
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdEnvelope;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
|
||||
import { EnvelopeType, FolderType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, FolderType, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@@ -12,9 +11,14 @@ import { prisma } from '@documenso/prisma';
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getEnvelopeWhereInput } from './get-envelope-by-id';
|
||||
|
||||
export type UpdateEnvelopeOptions = {
|
||||
@@ -339,6 +343,22 @@ export const updateEnvelope = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
const envelopeWithRelations = await tx.envelope.findUniqueOrThrow({
|
||||
where: { id: updatedEnvelope.id },
|
||||
include: { documentMeta: true, recipients: true },
|
||||
});
|
||||
|
||||
void triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_UPDATED,
|
||||
data: ZWebhookDocumentSchema.parse(
|
||||
mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations),
|
||||
),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return updatedEnvelope;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -764,6 +764,13 @@ export const createDocumentFromTemplate = async ({
|
||||
teamId,
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_USED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteTemplateOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
@@ -19,6 +24,18 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio
|
||||
teamId,
|
||||
});
|
||||
|
||||
const templateToDelete = await prisma.envelope.findUniqueOrThrow({
|
||||
where: envelopeWhereInput,
|
||||
include: { documentMeta: true, recipients: true },
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_DELETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(templateToDelete)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.envelope.delete({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,12 @@ export const submitSupportTicket = async ({
|
||||
organisationId,
|
||||
teamId,
|
||||
}: SubmitSupportTicketOptions) => {
|
||||
if (!plainClient) {
|
||||
throw new AppError(AppErrorCode.NOT_SETUP, {
|
||||
message: 'Support ticket system is not configured',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
@@ -52,6 +58,29 @@ export const submitSupportTicket = async ({
|
||||
})
|
||||
: null;
|
||||
|
||||
// Ensure the customer exists in Plain before creating a thread
|
||||
const plainCustomer = await plainClient.upsertCustomer({
|
||||
identifier: {
|
||||
emailAddress: user.email,
|
||||
},
|
||||
onCreate: {
|
||||
// If the user doesn't have a name, default to their email
|
||||
fullName: user.name || user.email,
|
||||
email: {
|
||||
email: user.email,
|
||||
isVerified: !!user.emailVerified,
|
||||
},
|
||||
},
|
||||
// No need to update the customer if it already exists
|
||||
onUpdate: {},
|
||||
});
|
||||
|
||||
if (plainCustomer.error) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: `Failed to create customer in support system: ${plainCustomer.error.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
const customMessage = `
|
||||
Organisation: ${organisation.name} (${organisation.id})
|
||||
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
|
||||
@@ -60,12 +89,14 @@ ${message}`;
|
||||
|
||||
const res = await plainClient.createThread({
|
||||
title: subject,
|
||||
customerIdentifier: { emailAddress: user.email },
|
||||
customerIdentifier: { customerId: plainCustomer.data.customer.id },
|
||||
components: [{ componentText: { text: customMessage } }],
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: `Failed to create support ticket: ${res.error.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@@ -480,5 +480,198 @@ export const generateSampleWebhookPayload = (
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_VIEWED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: now,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: now,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_DOWNLOADED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
completedAt: now,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: now,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: now,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_REMINDER_SENT) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
sendStatus: SendStatus.SENT,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
sendStatus: SendStatus.SENT,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.TEMPLATE_CREATED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
title: 'My Template',
|
||||
status: DocumentStatus.DRAFT,
|
||||
templateId: 10,
|
||||
source: DocumentSource.TEMPLATE,
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.TEMPLATE_UPDATED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
title: 'My Updated Template',
|
||||
status: DocumentStatus.DRAFT,
|
||||
templateId: 10,
|
||||
source: DocumentSource.TEMPLATE,
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.TEMPLATE_DELETED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
title: 'Deleted Template',
|
||||
status: DocumentStatus.DRAFT,
|
||||
templateId: 10,
|
||||
source: DocumentSource.TEMPLATE,
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.TEMPLATE_USED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
title: 'Document from Template',
|
||||
status: DocumentStatus.DRAFT,
|
||||
templateId: 10,
|
||||
source: DocumentSource.TEMPLATE,
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported event type: ${event}`);
|
||||
};
|
||||
|
||||
+485
-183
File diff suppressed because it is too large
Load Diff
+477
-175
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+485
-183
File diff suppressed because it is too large
Load Diff
@@ -294,8 +294,10 @@ export const formatDocumentAuditLogAction = (
|
||||
auditLog: TDocumentAuditLog,
|
||||
userId?: number,
|
||||
) => {
|
||||
const prefix =
|
||||
userId === auditLog.userId ? i18n._(msg`You`) : auditLog.name || auditLog.email || '';
|
||||
const isCurrentUser = userId === auditLog.userId;
|
||||
const user = auditLog.name || auditLog.email || '';
|
||||
|
||||
const prefix = isCurrentUser ? i18n._(msg`You`) : user || '';
|
||||
|
||||
const description = match(auditLog)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({
|
||||
@@ -303,245 +305,295 @@ export const formatDocumentAuditLogAction = (
|
||||
message: `A field was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a field`,
|
||||
you: msg`You added a field`,
|
||||
user: msg`${user} added a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A field was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a field`,
|
||||
you: msg`You removed a field`,
|
||||
user: msg`${user} removed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A field was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a field`,
|
||||
you: msg`You updated a field`,
|
||||
user: msg`${user} updated a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a recipient`,
|
||||
you: msg`You added a recipient`,
|
||||
user: msg`${user} added a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a recipient`,
|
||||
you: msg`You removed a recipient`,
|
||||
user: msg`${user} removed a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a recipient`,
|
||||
you: msg`You updated a recipient`,
|
||||
user: msg`${user} updated a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document created`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} created the document`,
|
||||
you: msg`You created the document`,
|
||||
user: msg`${user} created the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document deleted`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} deleted the document`,
|
||||
you: msg`You deleted the document`,
|
||||
user: msg`${user} deleted the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELDS_AUTO_INSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `System auto inserted fields`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`System auto inserted fields`,
|
||||
you: msg({
|
||||
message: `System auto inserted fields`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
user: msg({
|
||||
message: `System auto inserted fields`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field signed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} signed a field`,
|
||||
you: msg`You signed a field`,
|
||||
user: msg`${user} signed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field unsigned`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} unsigned a field`,
|
||||
you: msg`You unsigned a field`,
|
||||
user: msg`${user} unsigned a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field prefilled by assistant`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} prefilled a field`,
|
||||
you: msg`You prefilled a field`,
|
||||
user: msg`${user} prefilled a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document visibility updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
you: msg`You updated the document visibility`,
|
||||
user: msg`${user} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document access auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
you: msg`You updated the document access auth requirements`,
|
||||
user: msg`${user} updated the document access auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document signing auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document signing auth requirements`,
|
||||
you: msg`You updated the document signing auth requirements`,
|
||||
user: msg`${user} updated the document signing auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document`,
|
||||
you: msg`You updated the document`,
|
||||
user: msg`${user} updated the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document opened`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} opened the document`,
|
||||
you: msg`You opened the document`,
|
||||
user: msg`${user} opened the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document viewed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} viewed the document`,
|
||||
you: msg`You viewed the document`,
|
||||
user: msg`${user} viewed the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document title updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document title`,
|
||||
you: msg`You updated the document title`,
|
||||
user: msg`${user} updated the document title`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document external ID updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document external ID`,
|
||||
you: msg`You updated the document external ID`,
|
||||
user: msg`${user} updated the document external ID`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document sent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} sent the document`,
|
||||
you: msg`You sent the document`,
|
||||
user: msg`${user} sent the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document moved to team`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} moved the document to team`,
|
||||
you: msg`You moved the document to team`,
|
||||
user: msg`${user} moved the document to team`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => {
|
||||
const userName = prefix || i18n._(msg`Recipient`);
|
||||
|
||||
const result = match(data.recipientRole)
|
||||
.with(RecipientRole.SIGNER, () => msg`${userName} signed the document`)
|
||||
.with(RecipientRole.VIEWER, () => msg`${userName} viewed the document`)
|
||||
.with(RecipientRole.APPROVER, () => msg`${userName} approved the document`)
|
||||
.with(RecipientRole.CC, () => msg`${userName} CC'd the document`)
|
||||
.otherwise(() => msg`${userName} completed their task`);
|
||||
|
||||
return {
|
||||
anonymous: result,
|
||||
identified: result,
|
||||
};
|
||||
return match(data.recipientRole)
|
||||
.with(RecipientRole.SIGNER, () => ({
|
||||
anonymous: msg`Recipient signed the document`,
|
||||
you: msg`You signed the document`,
|
||||
user: msg`${user} signed the document`,
|
||||
}))
|
||||
.with(RecipientRole.VIEWER, () => ({
|
||||
anonymous: msg`Recipient viewed the document`,
|
||||
you: msg`You viewed the document`,
|
||||
user: msg`${user} viewed the document`,
|
||||
}))
|
||||
.with(RecipientRole.APPROVER, () => ({
|
||||
anonymous: msg`Recipient approved the document`,
|
||||
you: msg`You approved the document`,
|
||||
user: msg`${user} approved the document`,
|
||||
}))
|
||||
.with(RecipientRole.CC, () => ({
|
||||
anonymous: msg`Recipient CC'd the document`,
|
||||
you: msg`You CC'd the document`,
|
||||
user: msg`${user} CC'd the document`,
|
||||
}))
|
||||
.otherwise(() => ({
|
||||
anonymous: msg`Recipient completed their task`,
|
||||
you: msg`You completed your task`,
|
||||
user: msg`${user} completed their task`,
|
||||
}));
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => {
|
||||
const userName = prefix || i18n._(msg`Recipient`);
|
||||
|
||||
const result = msg`${userName} rejected the document`;
|
||||
|
||||
return {
|
||||
anonymous: result,
|
||||
identified: result,
|
||||
};
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_REQUESTED }, ({ data }) => {
|
||||
const userName = prefix || i18n._(msg`Recipient`);
|
||||
|
||||
const result = msg`${userName} requested a 2FA token for the document`;
|
||||
|
||||
return {
|
||||
anonymous: result,
|
||||
identified: result,
|
||||
};
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_VALIDATED }, ({ data }) => {
|
||||
const userName = prefix || i18n._(msg`Recipient`);
|
||||
|
||||
const result = msg`${userName} validated a 2FA token for the document`;
|
||||
|
||||
return {
|
||||
anonymous: result,
|
||||
identified: result,
|
||||
};
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_FAILED }, ({ data }) => {
|
||||
const userName = prefix || i18n._(msg`Recipient`);
|
||||
|
||||
const result = msg`${userName} failed to validate a 2FA token for the document`;
|
||||
|
||||
return {
|
||||
anonymous: result,
|
||||
identified: result,
|
||||
};
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
||||
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
|
||||
identified: data.isResending
|
||||
? msg`${prefix} resent an email to ${data.recipientEmail}`
|
||||
: msg`${prefix} sent an email to ${data.recipientEmail}`,
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, () => ({
|
||||
anonymous: msg`Recipient rejected the document`,
|
||||
you: msg`You rejected the document`,
|
||||
user: msg`${user} rejected the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_REQUESTED }, () => ({
|
||||
anonymous: msg`Recipient requested a 2FA token for the document`,
|
||||
you: msg`You requested a 2FA token for the document`,
|
||||
user: msg`${user} requested a 2FA token for the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_VALIDATED }, () => ({
|
||||
anonymous: msg`Recipient validated a 2FA token for the document`,
|
||||
you: msg`You validated a 2FA token for the document`,
|
||||
user: msg`${user} validated a 2FA token for the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_FAILED }, () => ({
|
||||
anonymous: msg`Recipient failed to validate a 2FA token for the document`,
|
||||
you: msg`You failed to validate a 2FA token for the document`,
|
||||
user: msg`${user} failed to validate a 2FA token for the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => {
|
||||
if (data.isResending) {
|
||||
return {
|
||||
anonymous: msg({
|
||||
message: `Email resent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You resent an email to ${data.recipientEmail}`,
|
||||
user: msg`${user} resent an email to ${data.recipientEmail}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
anonymous: msg({
|
||||
message: `Email sent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You sent an email to ${data.recipientEmail}`,
|
||||
user: msg`${user} sent an email to ${data.recipientEmail}`,
|
||||
};
|
||||
})
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg({ message: `Document completed`, context: `Audit log format` }),
|
||||
you: msg({ message: `Document completed`, context: `Audit log format` }),
|
||||
user: msg({ message: `Document completed`, context: `Audit log format` }),
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED }, ({ data }) => ({
|
||||
anonymous: msg`Envelope item created`,
|
||||
identified: msg`${prefix} created an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED }, ({ data }) => ({
|
||||
anonymous: msg`Envelope item deleted`,
|
||||
identified: msg`${prefix} deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED }, ({ data }) => ({
|
||||
anonymous: msg({
|
||||
message: `Document ownership delegated`,
|
||||
message: `Envelope item created`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`The document ownership was delegated to ${data.delegatedOwnerName || data.delegatedOwnerEmail} on behalf of ${data.teamName}`,
|
||||
you: msg`You created an envelope item with title ${data.envelopeItemTitle}`,
|
||||
user: msg`${user} created an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED }, ({ data }) => ({
|
||||
anonymous: msg({
|
||||
message: `Envelope item deleted`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
user: msg`${user} deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED }, ({ data }) => {
|
||||
const message = msg({
|
||||
message: `The document ownership was delegated to ${data.delegatedOwnerName || data.delegatedOwnerEmail} on behalf of ${data.teamName}`,
|
||||
context: `Audit log format`,
|
||||
});
|
||||
return {
|
||||
anonymous: message,
|
||||
you: message,
|
||||
user: message,
|
||||
};
|
||||
})
|
||||
.exhaustive();
|
||||
|
||||
let selectedDescription = description.anonymous;
|
||||
|
||||
if (isCurrentUser) {
|
||||
selectedDescription = description.you;
|
||||
} else if (user) {
|
||||
selectedDescription = description.user;
|
||||
}
|
||||
|
||||
return {
|
||||
prefix,
|
||||
description: i18n._(prefix ? description.identified : description.anonymous),
|
||||
description: i18n._(selectedDescription),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_VIEWED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_RECIPIENT_COMPLETED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_DOWNLOADED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_REMINDER_SENT';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_CREATED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_UPDATED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_DELETED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_USED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'RECIPIENT_AUTHENTICATION_FAILED';
|
||||
@@ -172,6 +172,15 @@ enum WebhookTriggerEvents {
|
||||
DOCUMENT_COMPLETED
|
||||
DOCUMENT_REJECTED
|
||||
DOCUMENT_CANCELLED
|
||||
DOCUMENT_VIEWED
|
||||
DOCUMENT_RECIPIENT_COMPLETED
|
||||
DOCUMENT_DOWNLOADED
|
||||
DOCUMENT_REMINDER_SENT
|
||||
TEMPLATE_CREATED
|
||||
TEMPLATE_UPDATED
|
||||
TEMPLATE_DELETED
|
||||
TEMPLATE_USED
|
||||
RECIPIENT_AUTHENTICATION_FAILED
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZFindUserTeamsRequestSchema, ZFindUserTeamsResponseSchema } from './find-user-teams.types';
|
||||
|
||||
export const findUserTeamsRoute = adminProcedure
|
||||
.input(ZFindUserTeamsRequestSchema)
|
||||
.output(ZFindUserTeamsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { userId, query, page, perPage } = input;
|
||||
|
||||
return await findUserTeams({
|
||||
userId,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
type FindUserTeamsOptions = {
|
||||
userId: number;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
const findUserTeams = async ({ userId, query, page = 1, perPage = 10 }: FindUserTeamsOptions) => {
|
||||
const whereClause: Prisma.TeamWhereInput = {
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (query && query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.team.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.team.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedData = data.map((team) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
url: team.url,
|
||||
createdAt: team.createdAt,
|
||||
teamRole: getHighestTeamRoleInGroup(team.teamGroups),
|
||||
organisation: team.organisation,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { TeamMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/TeamMemberRoleSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
export const ZFindUserTeamsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export const ZFindUserTeamsResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
})
|
||||
.extend({
|
||||
teamRole: TeamMemberRoleSchema,
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindUserTeamsRequest = z.infer<typeof ZFindUserTeamsRequestSchema>;
|
||||
export type TFindUserTeamsResponse = z.infer<typeof ZFindUserTeamsResponseSchema>;
|
||||
@@ -12,6 +12,7 @@ import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
|
||||
import { findDocumentJobsRoute } from './find-document-jobs';
|
||||
import { findDocumentsRoute } from './find-documents';
|
||||
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
||||
import { findUserTeamsRoute } from './find-user-teams';
|
||||
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
||||
import { getUserRoute } from './get-user';
|
||||
import { promoteMemberToOwnerRoute } from './promote-member-to-owner';
|
||||
@@ -55,6 +56,7 @@ export const adminRouter = router({
|
||||
enable: enableUserRoute,
|
||||
disable: disableUserRoute,
|
||||
resetTwoFactor: resetTwoFactorRoute,
|
||||
findTeams: findUserTeamsRoute,
|
||||
},
|
||||
document: {
|
||||
find: findDocumentsRoute,
|
||||
|
||||
Reference in New Issue
Block a user