Compare commits

...

14 Commits

Author SHA1 Message Date
Catalin Pit
a730acc76a chore: reverted merge 2024-09-05 16:24:40 +03:00
Catalin Pit
51afe95780 chore: merge main 2024-09-05 16:21:00 +03:00
Catalin Pit
80d2d0703a fix: select element width 2024-09-05 16:01:09 +03:00
Catalin Pit
a301dd345f feat: add colors 2024-09-05 14:32:11 +03:00
Catalin Pit
69bf404b32 feat: select signature color 2024-09-05 12:04:45 +03:00
Kaiwalya Koparkar
f4e98ae03a feat: Added Elestio as one-click deploy option (#1302)
This PR introduces new one-click deployment option Elestio
2024-09-05 15:30:46 +10:00
David Nguyen
0298e79e8c chore: add docs for translations (#1310)
Add developer documentation for translations.
2024-09-05 15:28:47 +10:00
Etrenak
8ab7464b84 fix: broken endpoint to update a field in a document (#1319)
Fixes #1318 and allows to submit a partial `fieldMeta` when updating a
field instead of replacing the current `fieldMeta`, to be consistent
with other endpoints.
2024-09-05 14:14:02 +10:00
Etrenak
ad4cff937d fix: show the existing field meta when editing a template fields (#1323)
Fixes #1259. I literally just used the exact same line of code than from
the document page so it is consistent.

The bug is extremely frustrating because if you make change to the
fields meta on the template and refresh the page, you can't see field
meta (label, default value, required), and neither do your co-workers.

Of course, I tested the change and it now displays correctly the
existing field meta on the template page.
2024-09-05 14:10:17 +10:00
Mythie
921617b905 v1.7.0-rc.4 2024-09-05 10:46:23 +10:00
Lucas Smith
a1a8a174bf feat: embed signing experience (#1322) 2024-09-04 23:13:00 +10:00
Mythie
3657050b02 fix: translation related crashes on marketing 2024-09-04 20:28:43 +10:00
Ajeet Pratap Singh
210081c520 fix: a grammer error (#1316)
## Description

<!--- Describe the changes introduced by this pull request. -->
In this PR, I've fixed a small grammar error.

## Related Issue

Fixes: #1315 

## Changes Made

It replaces `"wont"` with `"won't"` and `"Only your signing experience
is"` with `"Only your signing experience will be"`

## Checklist

- [x] I have tested these changes locally and they work as expected.
- [x] I have added/updated tests that prove the effectiveness of these
changes.
- [x] I have updated the documentation to reflect these changes, if
applicable.
- [x] I have followed the project's coding style guidelines.
- [x] I have addressed the code review feedback from the previous
submission, if applicable.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **Bug Fixes**
- Updated text in the Document Share Button for enhanced clarity and
grammatical accuracy.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: ajeet <109718740+ajeetcode@users.noreply.github.com>
2024-09-03 13:16:14 +03:00
Lucas Smith
fd7c1fea1c chore: upgrade next (#1300) 2024-09-03 09:48:54 +10:00
18 changed files with 248 additions and 214 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"workspaces": [
"apps/*",
"packages/*"
@@ -81,7 +81,7 @@
},
"apps/marketing": {
"name": "@documenso/marketing",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/assets": "*",
@@ -424,7 +424,7 @@
},
"apps/web": {
"name": "@documenso/web",
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "1.7.0-rc.3",
"version": "1.7.0-rc.2",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",

View File

@@ -21,7 +21,6 @@ import {
ZSendDocumentForSigningMutationSchema,
ZSuccessfulDeleteTemplateResponseSchema,
ZSuccessfulDocumentResponseSchema,
ZSuccessfulFieldCreationResponseSchema,
ZSuccessfulFieldResponseSchema,
ZSuccessfulGetDocumentResponseSchema,
ZSuccessfulGetTemplateResponseSchema,
@@ -237,7 +236,7 @@ export const ApiContractV1 = c.router(
path: '/api/v1/documents/:id/fields',
body: ZCreateFieldMutationSchema,
responses: {
200: ZSuccessfulFieldCreationResponseSchema,
200: ZSuccessfulFieldResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,

View File

@@ -1,5 +1,4 @@
import { createNextRoute } from '@ts-rest/next';
import { match } from 'ts-pattern';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@@ -16,6 +15,7 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field';
@@ -32,13 +32,6 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
@@ -46,8 +39,6 @@ import {
getPresignGetUrl,
getPresignPostUrl,
} from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { ApiContractV1 } from './contract';
@@ -879,167 +870,100 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
createField: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const fields = Array.isArray(args.body) ? args.body : [args.body];
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
args.body;
const document = await prisma.document.findFirst({
select: { id: true, status: true },
where: {
id: Number(documentId),
...(team?.id
? {
team: {
id: team.id,
members: { some: { userId: user.id } },
},
}
: {
userId: user.id,
teamId: null,
}),
},
if (pageNumber <= 0) {
return {
status: 400,
body: {
message: 'Invalid page number',
},
};
}
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
teamId: team?.id,
});
if (!document) {
return {
status: 404,
body: { message: 'Document not found' },
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: { message: 'Document is already completed' },
body: {
message: 'Document is already completed',
},
};
}
const recipient = await getRecipientById({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
try {
const createdFields = await prisma.$transaction(async (tx) => {
return Promise.all(
fields.map(async (fieldData) => {
const {
recipientId,
type,
pageNumber,
pageWidth,
pageHeight,
pageX,
pageY,
fieldMeta,
} = fieldData;
if (pageNumber <= 0) {
throw new Error('Invalid page number');
}
const recipient = await getRecipientById({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
throw new Error('Recipient not found');
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
throw new Error('Recipient has already signed the document');
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
type,
);
if (advancedField && !fieldMeta) {
throw new Error(
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
);
}
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
throw new Error('Field meta type does not match the field type');
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: {},
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: {},
}))
.exhaustive();
if (!result.success) {
throw new Error('Field meta parsing failed');
}
const field = await tx.field.create({
data: {
documentId: Number(documentId),
recipientId: Number(recipientId),
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
},
include: {
Recipient: true,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: 'FIELD_CREATED',
documentId: Number(documentId),
user: {
id: team?.id ?? user.id,
email: team?.name ?? user.email,
name: team ? '' : user.name,
},
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.Recipient?.email ?? '',
fieldRecipientId: recipientId,
fieldType: field.type,
},
requestMetadata: extractNextApiRequestMetadata(args.req),
}),
});
return {
id: field.id,
documentId: Number(field.documentId),
recipientId: field.recipientId ?? -1,
type: field.type,
pageNumber: field.page,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
customText: field.customText,
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
inserted: field.inserted,
};
}),
);
const field = await createField({
documentId: Number(documentId),
recipientId: Number(recipientId),
userId: user.id,
teamId: team?.id,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
fieldMeta,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
const remappedField = {
id: field.id,
documentId: field.documentId,
recipientId: field.recipientId ?? -1,
type: field.type,
pageNumber: field.page,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
customText: field.customText,
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
inserted: field.inserted,
};
return {
status: 200,
body: {
fields: createdFields,
...remappedField,
documentId: Number(documentId),
},
};

View File

@@ -293,7 +293,7 @@ export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecip
/**
* Fields
*/
const ZCreateFieldSchema = z.object({
export const ZCreateFieldMutationSchema = z.object({
recipientId: z.number(),
type: z.nativeEnum(FieldType),
pageNumber: z.number(),
@@ -301,17 +301,12 @@ const ZCreateFieldSchema = z.object({
pageY: z.number(),
pageWidth: z.number(),
pageHeight: z.number(),
fieldMeta: ZFieldMetaSchema.openapi({}),
fieldMeta: ZFieldMetaSchema,
});
export const ZCreateFieldMutationSchema = z.union([
ZCreateFieldSchema,
z.array(ZCreateFieldSchema).min(1),
]);
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
export const ZUpdateFieldMutationSchema = ZCreateFieldSchema.partial();
export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial();
export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>;
@@ -319,26 +314,6 @@ export const ZDeleteFieldMutationSchema = null;
export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
const ZSuccessfulFieldSchema = z.object({
id: z.number(),
documentId: z.number(),
recipientId: z.number(),
type: z.nativeEnum(FieldType),
pageNumber: z.number(),
pageX: z.number(),
pageY: z.number(),
pageWidth: z.number(),
pageHeight: z.number(),
customText: z.string(),
fieldMeta: ZFieldMetaSchema,
inserted: z.boolean(),
});
export const ZSuccessfulFieldCreationResponseSchema = z.object({
fields: z.union([ZSuccessfulFieldSchema, z.array(ZSuccessfulFieldSchema)]),
documentId: z.number(),
});
export const ZSuccessfulFieldResponseSchema = z.object({
id: z.number(),
documentId: z.number(),

View File

@@ -110,21 +110,24 @@ export const createField = async ({
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: {},
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: {},
}))
.exhaustive();
.with('RADIO', () => {
return ZRadioFieldMeta.safeParse(fieldMeta);
})
.with('CHECKBOX', () => {
return ZCheckboxFieldMeta.safeParse(fieldMeta);
})
.with('DROPDOWN', () => {
return ZDropdownFieldMeta.safeParse(fieldMeta);
})
.with('NUMBER', () => {
return ZNumberFieldMeta.safeParse(fieldMeta);
})
.with('TEXT', () => {
return ZTextFieldMeta.safeParse(fieldMeta);
})
.otherwise(() => {
return { success: false, data: {} };
});
if (!result.success) {
throw new Error('Field meta parsing failed');
@@ -142,7 +145,7 @@ export const createField = async ({
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
fieldMeta: advancedField ? result.data : undefined,
},
include: {
Recipient: true,

View File

@@ -37,10 +37,6 @@ export const updateField = async ({
requestMetadata,
fieldMeta,
}: UpdateFieldOptions) => {
if (type === 'FREE_SIGNATURE') {
throw new Error('Cannot update a FREE_SIGNATURE field');
}
const oldField = await prisma.field.findFirstOrThrow({
where: {
id: fieldId,

View File

@@ -140,6 +140,14 @@ msgstr "Genehmiger"
msgid "Approving"
msgstr "Genehmigung"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:271
msgid "Black"
msgstr ""
#: packages/ui/primitives/signature-pad/signature-pad.tsx:298
msgid "Blue"
msgstr ""
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
msgid "Cancel"
@@ -179,7 +187,7 @@ msgstr "Checkbox-Werte"
msgid "Clear filters"
msgstr "Filter löschen"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
#: packages/ui/primitives/signature-pad/signature-pad.tsx:321
msgid "Clear Signature"
msgstr "Unterschrift löschen"
@@ -312,6 +320,10 @@ msgstr "Globale Empfängerauthentifizierung"
msgid "Go Back"
msgstr "Zurück"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:307
msgid "Green"
msgstr ""
#: packages/lib/constants/recipient-roles.ts:72
msgid "I am a signer of this document"
msgstr "Ich bin ein Unterzeichner dieses Dokuments"
@@ -477,6 +489,10 @@ msgstr "Erhält Kopie"
msgid "Recipient action authentication"
msgstr "Empfängeraktion Authentifizierung"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:289
msgid "Red"
msgstr ""
#: packages/ui/primitives/document-flow/add-settings.tsx:298
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
msgid "Redirect URL"
@@ -733,6 +749,10 @@ msgstr "Viewer"
msgid "Viewing"
msgstr "Viewing"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
msgid "White"
msgstr ""
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -633,6 +633,14 @@ msgstr ""
msgid "Billing"
msgstr ""
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:207
#~ msgid "Black"
#~ msgstr ""
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:223
#~ msgid "Blue"
#~ msgstr ""
#: apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx:101
msgid "Browser"
msgstr ""
@@ -1604,6 +1612,10 @@ msgstr ""
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr ""
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:231
#~ msgid "Green"
#~ msgstr ""
#: apps/web/src/app/(dashboard)/settings/profile/page.tsx:29
msgid "Here you can edit your personal details."
msgstr ""
@@ -2412,6 +2424,10 @@ msgstr ""
msgid "Recovery codes"
msgstr ""
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:215
#~ msgid "Red"
#~ msgstr ""
#: apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx:78
#: apps/web/src/components/forms/signup.tsx:93
#: apps/web/src/components/forms/v2/signup.tsx:127

View File

@@ -135,6 +135,14 @@ msgstr "Approver"
msgid "Approving"
msgstr "Approving"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:271
msgid "Black"
msgstr "Black"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:298
msgid "Blue"
msgstr "Blue"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx:297
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:58
msgid "Cancel"
@@ -174,7 +182,7 @@ msgstr "Checkbox values"
msgid "Clear filters"
msgstr "Clear filters"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:256
#: packages/ui/primitives/signature-pad/signature-pad.tsx:321
msgid "Clear Signature"
msgstr "Clear Signature"
@@ -307,6 +315,10 @@ msgstr "Global recipient action authentication"
msgid "Go Back"
msgstr "Go Back"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:307
msgid "Green"
msgstr "Green"
#: packages/lib/constants/recipient-roles.ts:72
msgid "I am a signer of this document"
msgstr "I am a signer of this document"
@@ -472,6 +484,10 @@ msgstr "Receives copy"
msgid "Recipient action authentication"
msgstr "Recipient action authentication"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:289
msgid "Red"
msgstr "Red"
#: packages/ui/primitives/document-flow/add-settings.tsx:298
#: packages/ui/primitives/template-flow/add-template-settings.tsx:332
msgid "Redirect URL"
@@ -728,6 +744,10 @@ msgstr "Viewer"
msgid "Viewing"
msgstr "Viewing"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:280
msgid "White"
msgstr "White"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "You are about to send this document to the recipients. Are you sure you want to continue?"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -628,6 +628,14 @@ msgstr "Basic details"
msgid "Billing"
msgstr "Billing"
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:207
#~ msgid "Black"
#~ msgstr "Black"
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:223
#~ msgid "Blue"
#~ msgstr "Blue"
#: apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx:101
msgid "Browser"
msgstr "Browser"
@@ -1599,6 +1607,10 @@ msgstr "Go to owner"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Go to your <0>public profile settings</0> to add documents."
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:231
#~ msgid "Green"
#~ msgstr "Green"
#: apps/web/src/app/(dashboard)/settings/profile/page.tsx:29
msgid "Here you can edit your personal details."
msgstr "Here you can edit your personal details."
@@ -2407,6 +2419,10 @@ msgstr "Recovery code copied"
msgid "Recovery codes"
msgstr "Recovery codes"
#: apps/web/src/app/(signing)/sign/[token]/form.tsx:215
#~ msgid "Red"
#~ msgstr "Red"
#: apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx:78
#: apps/web/src/components/forms/signup.tsx:93
#: apps/web/src/components/forms/v2/signup.tsx:127

View File

@@ -9,6 +9,13 @@ import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
@@ -36,6 +43,7 @@ export const SignaturePad = ({
const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [selectedColor, setSelectedColor] = useState('black');
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@@ -85,6 +93,7 @@ export const SignaturePad = ({
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
lines.forEach((line) => {
const pathData = new Path2D(
@@ -129,6 +138,7 @@ export const SignaturePad = ({
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
newLines.forEach((line) => {
const pathData = new Path2D(
@@ -237,7 +247,7 @@ export const SignaturePad = ({
>
<canvas
ref={$el}
className={cn('relative block dark:invert', className)}
className={cn('relative block', className)}
style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)}
@@ -247,6 +257,61 @@ export const SignaturePad = ({
{...props}
/>
<div className="absolute right-2 top-2">
<Select onValueChange={(value) => setSelectedColor(value)}>
<SelectTrigger className="bg-background w-[90px]">
<SelectValue placeholder="Color" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="black">
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm"></div>
<Trans>Black</Trans>
</div>
</div>
</SelectItem>
<SelectItem value="white">
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<div className="mr-2 h-5 w-5 rounded-full border-2 shadow-sm"></div>
<Trans>White</Trans>
</div>
</div>
</SelectItem>
<SelectItem value="red">
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-red-500 shadow-sm"></div>
<Trans>Red</Trans>
</div>
</div>
</SelectItem>
<SelectItem value="blue">
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-blue-500 shadow-sm"></div>
<Trans>Blue</Trans>
</div>
</div>
</SelectItem>
<SelectItem value="green">
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<div className="mr-2 h-5 w-5 rounded-full border-2 bg-green-500 shadow-sm"></div>
<Trans>Green</Trans>
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="absolute bottom-4 right-4 flex gap-2">
<button
type="button"