Compare commits
3 Commits
v1.9.0-rc.
...
v1.9.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98b2da5018 | ||
|
|
fc1f76b543 | ||
|
|
22c9fb777b |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/marketing",
|
"name": "@documenso/marketing",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/loader": "^4.11.3",
|
"@lingui/loader": "^4.11.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "^0.1.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
|
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -54,6 +55,7 @@ export const CheckboxField = ({
|
|||||||
...item,
|
...item,
|
||||||
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [checkedValues, setCheckedValues] = useState(
|
const [checkedValues, setCheckedValues] = useState(
|
||||||
values
|
values
|
||||||
?.map((item) =>
|
?.map((item) =>
|
||||||
@@ -97,7 +99,7 @@ export const CheckboxField = ({
|
|||||||
const payload: TSignFieldWithTokenMutationSchema = {
|
const payload: TSignFieldWithTokenMutationSchema = {
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: checkedValues.join(','),
|
value: toCheckboxValue(checkedValues),
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
authOptions,
|
authOptions,
|
||||||
};
|
};
|
||||||
@@ -191,7 +193,7 @@ export const CheckboxField = ({
|
|||||||
await signFieldWithToken({
|
await signFieldWithToken({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: updatedValues.join(','),
|
value: toCheckboxValue(checkedValues),
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,6 +230,11 @@ export const CheckboxField = ({
|
|||||||
}
|
}
|
||||||
}, [checkedValues, isLengthConditionMet, field.inserted]);
|
}, [checkedValues, isLengthConditionMet, field.inserted]);
|
||||||
|
|
||||||
|
const parsedCheckedValues = useMemo(
|
||||||
|
() => fromCheckboxValue(field.customText),
|
||||||
|
[field.customText],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
@@ -277,9 +284,7 @@ export const CheckboxField = ({
|
|||||||
className="h-3 w-3"
|
className="h-3 w-3"
|
||||||
checkClassName="text-white"
|
checkClassName="text-white"
|
||||||
id={`checkbox-${index}`}
|
id={`checkbox-${index}`}
|
||||||
checked={field.customText
|
checked={parsedCheckedValues.includes(itemValue)}
|
||||||
.split(',')
|
|
||||||
.some((customValue) => customValue === itemValue)}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
74
package-lock.json
generated
74
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -17,8 +17,10 @@
|
|||||||
"@lingui/core": "^4.11.3",
|
"@lingui/core": "^4.11.3",
|
||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
|
"mupdf": "^1.0.0",
|
||||||
"next-runtime-env": "^3.2.0",
|
"next-runtime-env": "^3.2.0",
|
||||||
"react": "^18"
|
"react": "^18",
|
||||||
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"apps/marketing": {
|
"apps/marketing": {
|
||||||
"name": "@documenso/marketing",
|
"name": "@documenso/marketing",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
@@ -115,7 +117,7 @@
|
|||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/loader": "^4.11.3",
|
"@lingui/loader": "^4.11.3",
|
||||||
@@ -492,7 +494,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -540,7 +542,7 @@
|
|||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "^0.1.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
@@ -10723,15 +10725,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trigger.dev/cli/node_modules/zod": {
|
|
||||||
"version": "3.22.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
|
||||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@trigger.dev/core": {
|
"node_modules/@trigger.dev/core": {
|
||||||
"version": "2.3.18",
|
"version": "2.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/@trigger.dev/core/-/core-2.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@trigger.dev/core/-/core-2.3.18.tgz",
|
||||||
@@ -10753,14 +10746,6 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trigger.dev/core/node_modules/zod": {
|
|
||||||
"version": "3.22.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
|
||||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@trigger.dev/nextjs": {
|
"node_modules/@trigger.dev/nextjs": {
|
||||||
"version": "2.3.18",
|
"version": "2.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/@trigger.dev/nextjs/-/nextjs-2.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@trigger.dev/nextjs/-/nextjs-2.3.18.tgz",
|
||||||
@@ -10824,14 +10809,6 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trigger.dev/sdk/node_modules/zod": {
|
|
||||||
"version": "3.22.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
|
||||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@trigger.dev/yalt": {
|
"node_modules/@trigger.dev/yalt": {
|
||||||
"version": "2.3.18",
|
"version": "2.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/@trigger.dev/yalt/-/yalt-2.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@trigger.dev/yalt/-/yalt-2.3.18.tgz",
|
||||||
@@ -10848,15 +10825,6 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trigger.dev/yalt/node_modules/zod": {
|
|
||||||
"version": "3.22.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
|
||||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
||||||
@@ -20103,14 +20071,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inngest/node_modules/zod": {
|
|
||||||
"version": "3.22.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz",
|
|
||||||
"integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/input-otp": {
|
"node_modules/input-otp": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz",
|
||||||
@@ -24284,6 +24244,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/mupdf": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mupdf/-/mupdf-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-AWT27abYSX5gQmWs7+jDEtmGJpWyZrqdxROpYf5BDAJBA+iYqlNztk2EMlKvuRLBzajj00kf4PtFiergDSKDTg==",
|
||||||
|
"license": "AGPL-3.0-or-later"
|
||||||
|
},
|
||||||
"node_modules/mute-stream": {
|
"node_modules/mute-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||||
@@ -35762,7 +35728,7 @@
|
|||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"swagger-ui-react": "^5.11.0",
|
"swagger-ui-react": "^5.11.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/api/node_modules/@ts-rest/next": {
|
"packages/api/node_modules/@ts-rest/next": {
|
||||||
@@ -35827,7 +35793,7 @@
|
|||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/email": {
|
"packages/email": {
|
||||||
@@ -37036,7 +37002,7 @@
|
|||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/browser-chromium": "1.43.0",
|
"@playwright/browser-chromium": "1.43.0",
|
||||||
@@ -37174,7 +37140,7 @@
|
|||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/trpc/node_modules/@ts-rest/next": {
|
"packages/trpc/node_modules/@ts-rest/next": {
|
||||||
@@ -37254,7 +37220,7 @@
|
|||||||
"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",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.0-rc.4",
|
"version": "1.9.0-rc.5",
|
||||||
"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",
|
||||||
@@ -68,11 +68,14 @@
|
|||||||
"@lingui/core": "^4.11.3",
|
"@lingui/core": "^4.11.3",
|
||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
|
"mupdf": "^1.0.0",
|
||||||
"next-runtime-env": "^3.2.0",
|
"next-runtime-env": "^3.2.0",
|
||||||
"react": "^18"
|
"react": "^18",
|
||||||
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"next": "14.2.6"
|
"next": "14.2.6",
|
||||||
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
|
|||||||
@@ -25,6 +25,6 @@
|
|||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"swagger-ui-react": "^5.11.0",
|
"swagger-ui-react": "^5.11.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
"next-auth": "4.24.5",
|
"next-auth": "4.24.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { sendConfirmationToken } from '../../../server-only/user/send-confirmation-token';
|
||||||
|
import type { TSendConfirmationEmailJobDefinition } from './send-confirmation-email';
|
||||||
|
|
||||||
|
export const run = async ({ payload }: { payload: TSendConfirmationEmailJobDefinition }) => {
|
||||||
|
await sendConfirmationToken({
|
||||||
|
email: payload.email,
|
||||||
|
force: payload.force,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { sendConfirmationToken } from '../../../server-only/user/send-confirmation-token';
|
|
||||||
import type { JobDefinition } from '../../client/_internal/job';
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID = 'send.signup.confirmation.email';
|
const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID = 'send.signup.confirmation.email';
|
||||||
@@ -10,6 +9,10 @@ const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
force: z.boolean().optional(),
|
force: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendConfirmationEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
|
export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
|
||||||
id: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
|
id: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
|
||||||
name: 'Send Confirmation Email',
|
name: 'Send Confirmation Email',
|
||||||
@@ -19,12 +22,11 @@ export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
|
|||||||
schema: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA,
|
schema: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload }) => {
|
handler: async ({ payload }) => {
|
||||||
await sendConfirmationToken({
|
const handler = await import('./send-confirmation-email.handler');
|
||||||
email: payload.email,
|
|
||||||
force: payload.force,
|
await handler.run({ payload });
|
||||||
});
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
|
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA>
|
TSendConfirmationEmailJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||||
|
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||||
|
import { formatDocumentsPath } from '../../../utils/teams';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendSigningRejectionEmailsJobDefinition } from './send-rejection-emails';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSendSigningRejectionEmailsJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const { documentId, recipientId } = payload;
|
||||||
|
|
||||||
|
const [document, recipient] = await Promise.all([
|
||||||
|
prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
|
documentMeta: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
teamEmail: true,
|
||||||
|
name: true,
|
||||||
|
url: true,
|
||||||
|
teamGlobalSettings: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
signingStatus: SigningStatus.REJECTED,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { documentMeta, team, User: documentOwner } = document;
|
||||||
|
|
||||||
|
const isEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
if (!isEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = await getI18nInstance(documentMeta?.language);
|
||||||
|
|
||||||
|
// Send confirmation email to the recipient who rejected
|
||||||
|
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||||
|
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||||
|
recipientName: recipient.name,
|
||||||
|
documentName: document.title,
|
||||||
|
documentOwnerName: document.User.name || document.User.email,
|
||||||
|
reason: recipient.rejectionReason || '',
|
||||||
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = document.team?.teamGlobalSettings
|
||||||
|
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(recipientTemplate, { lang: documentMeta?.language, branding }),
|
||||||
|
renderEmailWithI18N(recipientTemplate, {
|
||||||
|
lang: documentMeta?.language,
|
||||||
|
branding,
|
||||||
|
plainText: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
name: recipient.name,
|
||||||
|
address: recipient.email,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send notification email to document owner
|
||||||
|
await io.runTask('send-owner-notification-email', async () => {
|
||||||
|
const ownerTemplate = createElement(DocumentRejectedEmail, {
|
||||||
|
recipientName: recipient.name,
|
||||||
|
documentName: document.title,
|
||||||
|
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
||||||
|
document.id
|
||||||
|
}`,
|
||||||
|
rejectionReason: recipient.rejectionReason || '',
|
||||||
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = document.team?.teamGlobalSettings
|
||||||
|
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(ownerTemplate, { lang: documentMeta?.language, branding }),
|
||||||
|
renderEmailWithI18N(ownerTemplate, {
|
||||||
|
lang: documentMeta?.language,
|
||||||
|
branding,
|
||||||
|
plainText: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
name: documentOwner.name || '',
|
||||||
|
address: documentOwner.email,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('update-recipient', async () => {
|
||||||
|
await prisma.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: recipient.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
|
||||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
|
||||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
|
||||||
import { formatDocumentsPath } from '../../../utils/teams';
|
|
||||||
import { type JobDefinition } from '../../client/_internal/job';
|
import { type JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID = 'send.signing.rejected.emails';
|
const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID = 'send.signing.rejected.emails';
|
||||||
@@ -25,6 +9,10 @@ const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
recipientId: z.number(),
|
recipientId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendSigningRejectionEmailsJobDefinition = z.infer<
|
||||||
|
typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION = {
|
export const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION = {
|
||||||
id: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
|
id: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
|
||||||
name: 'Send Rejection Emails',
|
name: 'Send Rejection Emails',
|
||||||
@@ -34,136 +22,11 @@ export const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION = {
|
|||||||
schema: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA,
|
schema: SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const { documentId, recipientId } = payload;
|
const handler = await import('./send-rejection-emails.handler');
|
||||||
|
|
||||||
const [document, recipient] = await Promise.all([
|
await handler.run({ payload, io });
|
||||||
prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
User: true,
|
|
||||||
documentMeta: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
teamEmail: true,
|
|
||||||
name: true,
|
|
||||||
url: true,
|
|
||||||
teamGlobalSettings: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.recipient.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
signingStatus: SigningStatus.REJECTED,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { documentMeta, team, User: documentOwner } = document;
|
|
||||||
|
|
||||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
||||||
document.documentMeta,
|
|
||||||
).recipientSigningRequest;
|
|
||||||
|
|
||||||
if (!isEmailEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(documentMeta?.language);
|
|
||||||
|
|
||||||
// Send confirmation email to the recipient who rejected
|
|
||||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
|
||||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
|
||||||
recipientName: recipient.name,
|
|
||||||
documentName: document.title,
|
|
||||||
documentOwnerName: document.User.name || document.User.email,
|
|
||||||
reason: recipient.rejectionReason || '',
|
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = document.team?.teamGlobalSettings
|
|
||||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(recipientTemplate, { lang: documentMeta?.language, branding }),
|
|
||||||
renderEmailWithI18N(recipientTemplate, {
|
|
||||||
lang: documentMeta?.language,
|
|
||||||
branding,
|
|
||||||
plainText: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
name: recipient.name,
|
|
||||||
address: recipient.email,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send notification email to document owner
|
|
||||||
await io.runTask('send-owner-notification-email', async () => {
|
|
||||||
const ownerTemplate = createElement(DocumentRejectedEmail, {
|
|
||||||
recipientName: recipient.name,
|
|
||||||
documentName: document.title,
|
|
||||||
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
|
||||||
document.id
|
|
||||||
}`,
|
|
||||||
rejectionReason: recipient.rejectionReason || '',
|
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = document.team?.teamGlobalSettings
|
|
||||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(ownerTemplate, { lang: documentMeta?.language, branding }),
|
|
||||||
renderEmailWithI18N(ownerTemplate, {
|
|
||||||
lang: documentMeta?.language,
|
|
||||||
branding,
|
|
||||||
plainText: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
name: documentOwner.name || '',
|
|
||||||
address: documentOwner.email,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await io.runTask('update-recipient', async () => {
|
|
||||||
await prisma.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: recipient.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
sendStatus: SendStatus.SENT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
|
typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_SCHEMA>
|
TSendSigningRejectionEmailsJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import {
|
||||||
|
DocumentSource,
|
||||||
|
DocumentStatus,
|
||||||
|
RecipientRole,
|
||||||
|
SendStatus,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||||
|
import {
|
||||||
|
RECIPIENT_ROLES_DESCRIPTION,
|
||||||
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
|
} from '../../../constants/recipient-roles';
|
||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
|
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||||
|
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendSigningEmailJobDefinition } from './send-signing-email';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSendSigningEmailJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const { userId, documentId, recipientId, requestMetadata } = payload;
|
||||||
|
|
||||||
|
const [user, document, recipient] = await Promise.all([
|
||||||
|
prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
teamEmail: true,
|
||||||
|
name: true,
|
||||||
|
teamGlobalSettings: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { documentMeta, team } = document;
|
||||||
|
|
||||||
|
if (recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
if (!isRecipientSigningRequestEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customEmail = document?.documentMeta;
|
||||||
|
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||||
|
const isTeamDocument = document.teamId !== null;
|
||||||
|
|
||||||
|
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||||
|
|
||||||
|
const { email, name } = recipient;
|
||||||
|
const selfSigner = email === user.email;
|
||||||
|
|
||||||
|
const i18n = await getI18nInstance(documentMeta?.language);
|
||||||
|
|
||||||
|
const recipientActionVerb = i18n
|
||||||
|
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
let emailMessage = customEmail?.message || '';
|
||||||
|
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
|
||||||
|
|
||||||
|
if (selfSigner) {
|
||||||
|
emailMessage = i18n._(
|
||||||
|
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||||
|
);
|
||||||
|
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectTemplate) {
|
||||||
|
emailMessage = i18n._(
|
||||||
|
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
|
||||||
|
);
|
||||||
|
emailSubject = i18n._(
|
||||||
|
msg`Please ${recipientActionVerb} this document created by your direct template`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTeamDocument && team) {
|
||||||
|
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
|
||||||
|
emailMessage = customEmail?.message ?? '';
|
||||||
|
|
||||||
|
if (!emailMessage) {
|
||||||
|
emailMessage = i18n._(
|
||||||
|
team.teamGlobalSettings?.includeSenderDetails
|
||||||
|
? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
||||||
|
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customEmailTemplate = {
|
||||||
|
'signer.name': name,
|
||||||
|
'signer.email': email,
|
||||||
|
'document.name': document.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||||
|
|
||||||
|
const template = createElement(DocumentInviteEmailTemplate, {
|
||||||
|
documentName: document.title,
|
||||||
|
inviterName: user.name || undefined,
|
||||||
|
inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
|
||||||
|
assetBaseUrl,
|
||||||
|
signDocumentLink,
|
||||||
|
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||||
|
role: recipient.role,
|
||||||
|
selfSigner,
|
||||||
|
isTeamInvite: isTeamDocument,
|
||||||
|
teamName: team?.name,
|
||||||
|
teamEmail: team?.teamEmail?.email,
|
||||||
|
includeSenderDetails: team?.teamGlobalSettings?.includeSenderDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('send-signing-email', async () => {
|
||||||
|
const branding = document.team?.teamGlobalSettings
|
||||||
|
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
|
||||||
|
renderEmailWithI18N(template, {
|
||||||
|
lang: documentMeta?.language,
|
||||||
|
branding,
|
||||||
|
plainText: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: {
|
||||||
|
name: recipient.name,
|
||||||
|
address: recipient.email,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: renderCustomEmailTemplate(
|
||||||
|
documentMeta?.subject || emailSubject,
|
||||||
|
customEmailTemplate,
|
||||||
|
),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('update-recipient', async () => {
|
||||||
|
await prisma.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: recipient.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('store-audit-log', async () => {
|
||||||
|
await prisma.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
|
documentId: document.id,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
emailType: recipientEmailType,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isResending: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,32 +1,6 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import {
|
|
||||||
DocumentSource,
|
|
||||||
DocumentStatus,
|
|
||||||
RecipientRole,
|
|
||||||
SendStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
|
||||||
import {
|
|
||||||
RECIPIENT_ROLES_DESCRIPTION,
|
|
||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
|
||||||
} from '../../../constants/recipient-roles';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
|
||||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
|
||||||
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
|
||||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
|
||||||
import { type JobDefinition } from '../../client/_internal/job';
|
import { type JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
|
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
|
||||||
@@ -38,6 +12,10 @@ const SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendSigningEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
|
export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
|
||||||
id: SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
|
id: SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
|
||||||
name: 'Send Signing Email',
|
name: 'Send Signing Email',
|
||||||
@@ -47,185 +25,11 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
|
|||||||
schema: SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA,
|
schema: SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const { userId, documentId, recipientId, requestMetadata } = payload;
|
const handler = await import('./send-signing-email.handler');
|
||||||
|
|
||||||
const [user, document, recipient] = await Promise.all([
|
await handler.run({ payload, io });
|
||||||
prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
status: DocumentStatus.PENDING,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
teamEmail: true,
|
|
||||||
name: true,
|
|
||||||
teamGlobalSettings: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.recipient.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { documentMeta, team } = document;
|
|
||||||
|
|
||||||
if (recipient.role === RecipientRole.CC) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
||||||
document.documentMeta,
|
|
||||||
).recipientSigningRequest;
|
|
||||||
|
|
||||||
if (!isRecipientSigningRequestEmailEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customEmail = document?.documentMeta;
|
|
||||||
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
|
||||||
const isTeamDocument = document.teamId !== null;
|
|
||||||
|
|
||||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
|
||||||
|
|
||||||
const { email, name } = recipient;
|
|
||||||
const selfSigner = email === user.email;
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(documentMeta?.language);
|
|
||||||
|
|
||||||
const recipientActionVerb = i18n
|
|
||||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
let emailMessage = customEmail?.message || '';
|
|
||||||
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
|
|
||||||
|
|
||||||
if (selfSigner) {
|
|
||||||
emailMessage = i18n._(
|
|
||||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
|
||||||
);
|
|
||||||
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectTemplate) {
|
|
||||||
emailMessage = i18n._(
|
|
||||||
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
|
|
||||||
);
|
|
||||||
emailSubject = i18n._(
|
|
||||||
msg`Please ${recipientActionVerb} this document created by your direct template`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTeamDocument && team) {
|
|
||||||
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
|
|
||||||
emailMessage = customEmail?.message ?? '';
|
|
||||||
|
|
||||||
if (!emailMessage) {
|
|
||||||
emailMessage = i18n._(
|
|
||||||
team.teamGlobalSettings?.includeSenderDetails
|
|
||||||
? msg`${user.name} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
|
||||||
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const customEmailTemplate = {
|
|
||||||
'signer.name': name,
|
|
||||||
'signer.email': email,
|
|
||||||
'document.name': document.title,
|
|
||||||
};
|
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
|
||||||
|
|
||||||
const template = createElement(DocumentInviteEmailTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
inviterName: user.name || undefined,
|
|
||||||
inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
|
|
||||||
assetBaseUrl,
|
|
||||||
signDocumentLink,
|
|
||||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
|
||||||
role: recipient.role,
|
|
||||||
selfSigner,
|
|
||||||
isTeamInvite: isTeamDocument,
|
|
||||||
teamName: team?.name,
|
|
||||||
teamEmail: team?.teamEmail?.email,
|
|
||||||
includeSenderDetails: team?.teamGlobalSettings?.includeSenderDetails,
|
|
||||||
});
|
|
||||||
|
|
||||||
await io.runTask('send-signing-email', async () => {
|
|
||||||
const branding = document.team?.teamGlobalSettings
|
|
||||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
|
|
||||||
renderEmailWithI18N(template, {
|
|
||||||
lang: documentMeta?.language,
|
|
||||||
branding,
|
|
||||||
plainText: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
name: recipient.name,
|
|
||||||
address: recipient.email,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: renderCustomEmailTemplate(
|
|
||||||
documentMeta?.subject || emailSubject,
|
|
||||||
customEmailTemplate,
|
|
||||||
),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await io.runTask('update-recipient', async () => {
|
|
||||||
await prisma.recipient.update({
|
|
||||||
where: {
|
|
||||||
id: recipient.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
sendStatus: SendStatus.SENT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await io.runTask('store-audit-log', async () => {
|
|
||||||
await prisma.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
|
||||||
documentId: document.id,
|
|
||||||
user,
|
|
||||||
requestMetadata,
|
|
||||||
data: {
|
|
||||||
emailType: recipientEmailType,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
recipientName: recipient.name,
|
|
||||||
recipientEmail: recipient.email,
|
|
||||||
recipientRole: recipient.role,
|
|
||||||
isResending: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
|
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA>
|
TSendSigningEmailJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendTeamDeletedEmailJobDefinition } from './send-team-deleted-email';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSendTeamDeletedEmailJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const { team, members } = payload;
|
||||||
|
|
||||||
|
for (const member of members) {
|
||||||
|
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
|
||||||
|
await sendTeamDeleteEmail({
|
||||||
|
email: member.email,
|
||||||
|
team,
|
||||||
|
isOwner: member.id === team.ownerUserId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
|
|
||||||
import type { JobDefinition } from '../../client/_internal/job';
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
|
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
|
||||||
@@ -37,6 +36,10 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendTeamDeletedEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
|
export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
|
||||||
id: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
|
id: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
|
||||||
name: 'Send Team Deleted Email',
|
name: 'Send Team Deleted Email',
|
||||||
@@ -46,19 +49,11 @@ export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
|
|||||||
schema: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA,
|
schema: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const { team, members } = payload;
|
const handler = await import('./send-team-deleted-email.handler');
|
||||||
|
|
||||||
for (const member of members) {
|
await handler.run({ payload, io });
|
||||||
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
|
|
||||||
await sendTeamDeleteEmail({
|
|
||||||
email: member.email,
|
|
||||||
team,
|
|
||||||
isOwner: member.id === team.ownerUserId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
|
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA>
|
TSendTeamDeletedEmailJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||||
|
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendTeamMemberJoinedEmailJobDefinition } from './send-team-member-joined-email';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSendTeamMemberJoinedEmailJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const team = await prisma.team.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: payload.teamId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
role: {
|
||||||
|
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teamGlobalSettings: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitedMember = await prisma.teamMember.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: payload.memberId,
|
||||||
|
teamId: payload.teamId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const member of team.members) {
|
||||||
|
if (member.id === invitedMember.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await io.runTask(
|
||||||
|
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||||
|
async () => {
|
||||||
|
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||||
|
assetBaseUrl: WEBAPP_BASE_URL,
|
||||||
|
baseUrl: WEBAPP_BASE_URL,
|
||||||
|
memberName: invitedMember.user.name || '',
|
||||||
|
memberEmail: invitedMember.user.email,
|
||||||
|
teamName: team.name,
|
||||||
|
teamUrl: team.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = team.teamGlobalSettings
|
||||||
|
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||||
|
|
||||||
|
// !: Replace with the actual language of the recipient later
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(emailContent, {
|
||||||
|
lang,
|
||||||
|
branding,
|
||||||
|
}),
|
||||||
|
renderEmailWithI18N(emailContent, {
|
||||||
|
lang,
|
||||||
|
branding,
|
||||||
|
plainText: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const i18n = await getI18nInstance(lang);
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: member.user.email,
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: i18n._(msg`A new member has joined your team`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
|
||||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
|
||||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
|
||||||
import type { JobDefinition } from '../../client/_internal/job';
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
|
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
|
||||||
@@ -22,6 +9,10 @@ const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
memberId: z.number(),
|
memberId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendTeamMemberJoinedEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
||||||
id: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
id: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||||
name: 'Send Team Member Joined Email',
|
name: 'Send Team Member Joined Email',
|
||||||
@@ -31,88 +22,11 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
|||||||
schema: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
schema: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const team = await prisma.team.findFirstOrThrow({
|
const handler = await import('./send-team-member-joined-email.handler');
|
||||||
where: {
|
|
||||||
id: payload.teamId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
members: {
|
|
||||||
where: {
|
|
||||||
role: {
|
|
||||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teamGlobalSettings: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const invitedMember = await prisma.teamMember.findFirstOrThrow({
|
await handler.run({ payload, io });
|
||||||
where: {
|
|
||||||
id: payload.memberId,
|
|
||||||
teamId: payload.teamId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const member of team.members) {
|
|
||||||
if (member.id === invitedMember.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await io.runTask(
|
|
||||||
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
|
|
||||||
async () => {
|
|
||||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
|
||||||
assetBaseUrl: WEBAPP_BASE_URL,
|
|
||||||
baseUrl: WEBAPP_BASE_URL,
|
|
||||||
memberName: invitedMember.user.name || '',
|
|
||||||
memberEmail: invitedMember.user.email,
|
|
||||||
teamName: team.name,
|
|
||||||
teamUrl: team.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = team.teamGlobalSettings
|
|
||||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
|
||||||
|
|
||||||
// !: Replace with the actual language of the recipient later
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(emailContent, {
|
|
||||||
lang,
|
|
||||||
branding,
|
|
||||||
}),
|
|
||||||
renderEmailWithI18N(emailContent, {
|
|
||||||
lang,
|
|
||||||
branding,
|
|
||||||
plainText: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(lang);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: member.user.email,
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`A new member has joined your team`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA>
|
TSendTeamMemberJoinedEmailJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { mailer } from '@documenso/email/mailer';
|
||||||
|
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||||
|
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
||||||
|
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendTeamMemberLeftEmailJobDefinition } from './send-team-member-left-email';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSendTeamMemberLeftEmailJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const team = await prisma.team.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: payload.teamId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
|
role: {
|
||||||
|
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teamGlobalSettings: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldMember = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: payload.memberUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const member of team.members) {
|
||||||
|
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
|
||||||
|
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||||
|
assetBaseUrl: WEBAPP_BASE_URL,
|
||||||
|
baseUrl: WEBAPP_BASE_URL,
|
||||||
|
memberName: oldMember.name || '',
|
||||||
|
memberEmail: oldMember.email,
|
||||||
|
teamName: team.name,
|
||||||
|
teamUrl: team.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = team.teamGlobalSettings
|
||||||
|
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||||
|
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(emailContent, {
|
||||||
|
lang,
|
||||||
|
branding,
|
||||||
|
}),
|
||||||
|
renderEmailWithI18N(emailContent, {
|
||||||
|
lang,
|
||||||
|
branding,
|
||||||
|
plainText: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const i18n = await getI18nInstance(lang);
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: member.user.email,
|
||||||
|
from: {
|
||||||
|
name: FROM_NAME,
|
||||||
|
address: FROM_ADDRESS,
|
||||||
|
},
|
||||||
|
subject: i18n._(msg`A team member has left ${team.name}`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
|
||||||
import { WEBAPP_BASE_URL } from '../../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
|
||||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
|
||||||
import type { JobDefinition } from '../../client/_internal/job';
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
|
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
|
||||||
@@ -22,6 +9,10 @@ const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
memberUserId: z.number(),
|
memberUserId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSendTeamMemberLeftEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
||||||
id: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
id: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||||
name: 'Send Team Member Left Email',
|
name: 'Send Team Member Left Email',
|
||||||
@@ -31,76 +22,11 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
|||||||
schema: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
schema: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const team = await prisma.team.findFirstOrThrow({
|
const handler = await import('./send-team-member-left-email.handler');
|
||||||
where: {
|
|
||||||
id: payload.teamId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
members: {
|
|
||||||
where: {
|
|
||||||
role: {
|
|
||||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teamGlobalSettings: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const oldMember = await prisma.user.findFirstOrThrow({
|
await handler.run({ payload, io });
|
||||||
where: {
|
|
||||||
id: payload.memberUserId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const member of team.members) {
|
|
||||||
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
|
|
||||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
|
||||||
assetBaseUrl: WEBAPP_BASE_URL,
|
|
||||||
baseUrl: WEBAPP_BASE_URL,
|
|
||||||
memberName: oldMember.name || '',
|
|
||||||
memberEmail: oldMember.email,
|
|
||||||
teamName: team.name,
|
|
||||||
teamUrl: team.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = team.teamGlobalSettings
|
|
||||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(emailContent, {
|
|
||||||
lang,
|
|
||||||
branding,
|
|
||||||
}),
|
|
||||||
renderEmailWithI18N(emailContent, {
|
|
||||||
lang,
|
|
||||||
branding,
|
|
||||||
plainText: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(lang);
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: member.user.email,
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: i18n._(msg`A team member has left ${team.name}`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA>
|
TSendTeamMemberLeftEmailJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
253
packages/lib/jobs/definitions/internal/seal-document.handler.ts
Normal file
253
packages/lib/jobs/definitions/internal/seal-document.handler.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import {
|
||||||
|
DocumentStatus,
|
||||||
|
RecipientRole,
|
||||||
|
SigningStatus,
|
||||||
|
WebhookTriggerEvents,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
|
import { signPdf } from '@documenso/signing';
|
||||||
|
|
||||||
|
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||||
|
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||||
|
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||||
|
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||||
|
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||||
|
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
||||||
|
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
|
||||||
|
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||||
|
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
|
||||||
|
import { getFile } from '../../../universal/upload/get-file';
|
||||||
|
import { putPdfFile } from '../../../universal/upload/put-file';
|
||||||
|
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSealDocumentJobDefinition } from './seal-document';
|
||||||
|
|
||||||
|
export const run = async ({
|
||||||
|
payload,
|
||||||
|
io,
|
||||||
|
}: {
|
||||||
|
payload: TSealDocumentJobDefinition;
|
||||||
|
io: JobRunIO;
|
||||||
|
}) => {
|
||||||
|
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
Recipient: {
|
||||||
|
every: {
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
Recipient: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
teamGlobalSettings: {
|
||||||
|
select: {
|
||||||
|
includeSigningCertificate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seems silly but we need to do this in case the job is re-ran
|
||||||
|
// after it has already run through the update task further below.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
const documentStatus = await io.runTask('get-document-status', async () => {
|
||||||
|
return document.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the same case as above.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
const documentDataId = await io.runTask('get-document-data-id', async () => {
|
||||||
|
return document.documentDataId;
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentData = await prisma.documentData.findFirst({
|
||||||
|
where: {
|
||||||
|
id: documentDataId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentData) {
|
||||||
|
throw new Error(`Document ${document.id} has no document data`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
|
||||||
|
throw new Error(`Document ${document.id} has unsigned recipients`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Signature: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fields.some((field) => !field.inserted)) {
|
||||||
|
throw new Error(`Document ${document.id} has unsigned fields`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResealing) {
|
||||||
|
// If we're resealing we want to use the initial data for the document
|
||||||
|
// so we aren't placing fields on top of eachother.
|
||||||
|
documentData.data = documentData.initialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfData = await getFile(documentData);
|
||||||
|
|
||||||
|
const certificateData =
|
||||||
|
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||||
|
? await getCertificatePdf({
|
||||||
|
documentId,
|
||||||
|
language: document.documentMeta?.language,
|
||||||
|
}).catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||||
|
const pdfDoc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
|
// Normalize and flatten layers that could cause issues with the signature
|
||||||
|
normalizeSignatureAppearances(pdfDoc);
|
||||||
|
flattenForm(pdfDoc);
|
||||||
|
flattenAnnotations(pdfDoc);
|
||||||
|
|
||||||
|
if (certificateData) {
|
||||||
|
const certificateDoc = await PDFDocument.load(certificateData);
|
||||||
|
|
||||||
|
const certificatePages = await pdfDoc.copyPages(
|
||||||
|
certificateDoc,
|
||||||
|
certificateDoc.getPageIndices(),
|
||||||
|
);
|
||||||
|
|
||||||
|
certificatePages.forEach((page) => {
|
||||||
|
pdfDoc.addPage(page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
await insertFieldInPDF(pdfDoc, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-flatten the form to handle our checkbox and radio fields that
|
||||||
|
// create native arcoFields
|
||||||
|
flattenForm(pdfDoc);
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||||
|
|
||||||
|
const { name } = path.parse(document.title);
|
||||||
|
|
||||||
|
const documentData = await putPdfFile({
|
||||||
|
name: `${name}_signed.pdf`,
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||||
|
});
|
||||||
|
|
||||||
|
return documentData.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHog = PostHogServerClient();
|
||||||
|
|
||||||
|
if (postHog) {
|
||||||
|
postHog.capture({
|
||||||
|
distinctId: nanoid(),
|
||||||
|
event: 'App: Document Sealed',
|
||||||
|
properties: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await io.runTask('update-document', async () => {
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
const newData = await tx.documentData.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: newDataId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: DocumentStatus.COMPLETED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentData.update({
|
||||||
|
where: {
|
||||||
|
id: documentData.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
data: newData.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||||
|
documentId: document.id,
|
||||||
|
requestMetadata,
|
||||||
|
user: null,
|
||||||
|
data: {
|
||||||
|
transactionId: nanoid(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await io.runTask('send-completed-email', async () => {
|
||||||
|
let shouldSendCompletedEmail = sendEmail && !isResealing;
|
||||||
|
|
||||||
|
if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
|
||||||
|
shouldSendCompletedEmail = sendEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSendCompletedEmail) {
|
||||||
|
await sendCompletedEmail({ documentId, requestMetadata });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
|
Recipient: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||||
|
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||||
|
userId: updatedDocument.userId,
|
||||||
|
teamId: updatedDocument.teamId ?? undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,31 +1,6 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { PDFDocument } from 'pdf-lib';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import {
|
|
||||||
DocumentStatus,
|
|
||||||
RecipientRole,
|
|
||||||
SigningStatus,
|
|
||||||
WebhookTriggerEvents,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { signPdf } from '@documenso/signing';
|
|
||||||
|
|
||||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
|
||||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
|
||||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
|
||||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
|
||||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
|
||||||
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
|
||||||
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
|
|
||||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
|
||||||
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
|
|
||||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../../universal/upload/get-file';
|
|
||||||
import { putPdfFile } from '../../../universal/upload/put-file';
|
|
||||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
|
||||||
import { type JobDefinition } from '../../client/_internal/job';
|
import { type JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
const SEAL_DOCUMENT_JOB_DEFINITION_ID = 'internal.seal-document';
|
const SEAL_DOCUMENT_JOB_DEFINITION_ID = 'internal.seal-document';
|
||||||
@@ -37,6 +12,8 @@ const SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TSealDocumentJobDefinition = z.infer<typeof SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA>;
|
||||||
|
|
||||||
export const SEAL_DOCUMENT_JOB_DEFINITION = {
|
export const SEAL_DOCUMENT_JOB_DEFINITION = {
|
||||||
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||||
name: 'Seal Document',
|
name: 'Seal Document',
|
||||||
@@ -46,223 +23,11 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
|
|||||||
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
|
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
|
||||||
},
|
},
|
||||||
handler: async ({ payload, io }) => {
|
handler: async ({ payload, io }) => {
|
||||||
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
|
const handler = await import('./seal-document.handler');
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
await handler.run({ payload, io });
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
Recipient: {
|
|
||||||
every: {
|
|
||||||
signingStatus: SigningStatus.SIGNED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
Recipient: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
teamGlobalSettings: {
|
|
||||||
select: {
|
|
||||||
includeSigningCertificate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Seems silly but we need to do this in case the job is re-ran
|
|
||||||
// after it has already run through the update task further below.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
|
||||||
const documentStatus = await io.runTask('get-document-status', async () => {
|
|
||||||
return document.status;
|
|
||||||
});
|
|
||||||
|
|
||||||
// This is the same case as above.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
|
||||||
const documentDataId = await io.runTask('get-document-data-id', async () => {
|
|
||||||
return document.documentDataId;
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentData = await prisma.documentData.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentDataId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
throw new Error(`Document ${document.id} has no document data`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = await prisma.recipient.findMany({
|
|
||||||
where: {
|
|
||||||
documentId: document.id,
|
|
||||||
role: {
|
|
||||||
not: RecipientRole.CC,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
|
|
||||||
throw new Error(`Document ${document.id} has unsigned recipients`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = await prisma.field.findMany({
|
|
||||||
where: {
|
|
||||||
documentId: document.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Signature: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fields.some((field) => !field.inserted)) {
|
|
||||||
throw new Error(`Document ${document.id} has unsigned fields`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResealing) {
|
|
||||||
// If we're resealing we want to use the initial data for the document
|
|
||||||
// so we aren't placing fields on top of eachother.
|
|
||||||
documentData.data = documentData.initialData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfData = await getFile(documentData);
|
|
||||||
const certificateData =
|
|
||||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
|
||||||
? await getCertificatePdf({
|
|
||||||
documentId,
|
|
||||||
language: document.documentMeta?.language,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
|
||||||
const pdfDoc = await PDFDocument.load(pdfData);
|
|
||||||
|
|
||||||
// Normalize and flatten layers that could cause issues with the signature
|
|
||||||
normalizeSignatureAppearances(pdfDoc);
|
|
||||||
flattenForm(pdfDoc);
|
|
||||||
flattenAnnotations(pdfDoc);
|
|
||||||
|
|
||||||
if (certificateData) {
|
|
||||||
const certificateDoc = await PDFDocument.load(certificateData);
|
|
||||||
|
|
||||||
const certificatePages = await pdfDoc.copyPages(
|
|
||||||
certificateDoc,
|
|
||||||
certificateDoc.getPageIndices(),
|
|
||||||
);
|
|
||||||
|
|
||||||
certificatePages.forEach((page) => {
|
|
||||||
pdfDoc.addPage(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const field of fields) {
|
|
||||||
await insertFieldInPDF(pdfDoc, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-flatten the form to handle our checkbox and radio fields that
|
|
||||||
// create native arcoFields
|
|
||||||
flattenForm(pdfDoc);
|
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
|
||||||
|
|
||||||
const { name } = path.parse(document.title);
|
|
||||||
|
|
||||||
const documentData = await putPdfFile({
|
|
||||||
name: `${name}_signed.pdf`,
|
|
||||||
type: 'application/pdf',
|
|
||||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
|
||||||
});
|
|
||||||
|
|
||||||
return documentData.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const postHog = PostHogServerClient();
|
|
||||||
|
|
||||||
if (postHog) {
|
|
||||||
postHog.capture({
|
|
||||||
distinctId: nanoid(),
|
|
||||||
event: 'App: Document Sealed',
|
|
||||||
properties: {
|
|
||||||
documentId: document.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await io.runTask('update-document', async () => {
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
const newData = await tx.documentData.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: newDataId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.document.update({
|
|
||||||
where: {
|
|
||||||
id: document.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: DocumentStatus.COMPLETED,
|
|
||||||
completedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentData.update({
|
|
||||||
where: {
|
|
||||||
id: documentData.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
data: newData.data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
|
||||||
documentId: document.id,
|
|
||||||
requestMetadata,
|
|
||||||
user: null,
|
|
||||||
data: {
|
|
||||||
transactionId: nanoid(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await io.runTask('send-completed-email', async () => {
|
|
||||||
let shouldSendCompletedEmail = sendEmail && !isResealing;
|
|
||||||
|
|
||||||
if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
|
|
||||||
shouldSendCompletedEmail = sendEmail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSendCompletedEmail) {
|
|
||||||
await sendCompletedEmail({ documentId, requestMetadata });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: document.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
documentMeta: true,
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await triggerWebhook({
|
|
||||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
|
||||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
|
||||||
userId: updatedDocument.userId,
|
|
||||||
teamId: updatedDocument.teamId ?? undefined,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
} as const satisfies JobDefinition<
|
} as const satisfies JobDefinition<
|
||||||
typeof SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
typeof SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||||
z.infer<typeof SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA>
|
TSealDocumentJobDefinition
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -56,11 +56,11 @@
|
|||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/browser-chromium": "1.43.0",
|
"@playwright/browser-chromium": "1.43.0",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/pg": "^8.11.4"
|
"@types/pg": "^8.11.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import { validateDropdownField } from '@documenso/lib/advanced-fields-validation
|
|||||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||||
|
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
@@ -119,7 +120,8 @@ export const signFieldWithToken = async ({
|
|||||||
|
|
||||||
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
|
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
|
||||||
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
const checkboxFieldValues = value.split(',');
|
const checkboxFieldValues: string[] = fromCheckboxValue(value);
|
||||||
|
|
||||||
const errors = validateCheckboxField(checkboxFieldValues, checkboxFieldParsedMeta, true);
|
const errors = validateCheckboxField(checkboxFieldValues, checkboxFieldParsedMeta, true);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
MIN_HANDWRITING_FONT_SIZE,
|
MIN_HANDWRITING_FONT_SIZE,
|
||||||
MIN_STANDARD_FONT_SIZE,
|
MIN_STANDARD_FONT_SIZE,
|
||||||
} from '@documenso/lib/constants/pdf';
|
} from '@documenso/lib/constants/pdf';
|
||||||
|
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
@@ -194,7 +195,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selected = field.customText.split(',');
|
const selected: string[] = fromCheckboxValue(field.customText);
|
||||||
|
|
||||||
for (const [index, item] of (values ?? []).entries()) {
|
for (const [index, item] of (values ?? []).entries()) {
|
||||||
const offsetY = index * 16;
|
const offsetY = index * 16;
|
||||||
|
|||||||
21
packages/lib/universal/field-checkbox.ts
Normal file
21
packages/lib/universal/field-checkbox.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const fromCheckboxValue = (customText: string): string[] => {
|
||||||
|
if (!customText) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(customText);
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
throw new Error('Parsed checkbox values are not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return customText.split(',').filter(Boolean);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toCheckboxValue = (values: string[]): string => {
|
||||||
|
return JSON.stringify(values);
|
||||||
|
};
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +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",
|
||||||
"zod": "^3.23.8"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user