From 2ff330f9d4c3a5cd16b37acbae2a825d8517c9d3 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:55:12 +0000 Subject: [PATCH 01/21] chore: update local seed data (#1622) ## Description Add multiple example documents, pending documents, and templates for both admin and example users ## Changes Made - Added seeding of multiple example documents and templates for both example and admin users ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] 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. --- package-lock.json | 15 ++++ packages/prisma/seed/initial-seed.ts | 103 +++++++++++++++++++++------ 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d13aa2ae..36153b31a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35722,6 +35722,21 @@ "engines": { "node": ">=6" } + }, + "packages/trpc/node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz", + "integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/prisma/seed/initial-seed.ts b/packages/prisma/seed/initial-seed.ts index d270b31fa..158b68475 100644 --- a/packages/prisma/seed/initial-seed.ts +++ b/packages/prisma/seed/initial-seed.ts @@ -5,6 +5,18 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash'; import { prisma } from '..'; import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client'; +import { seedPendingDocument } from './documents'; +import { seedDirectTemplate, seedTemplate } from './templates'; + +const createDocumentData = async ({ documentData }: { documentData: string }) => { + return prisma.documentData.create({ + data: { + type: DocumentDataType.BYTES_64, + data: documentData, + initialData: documentData, + }, + }); +}; export const seedDatabase = async () => { const examplePdf = fs @@ -39,35 +51,80 @@ export const seedDatabase = async () => { update: {}, }); - const examplePdfData = await prisma.documentData.upsert({ - where: { - id: 'clmn0kv5k0000pe04vcqg5zla', - }, - create: { - id: 'clmn0kv5k0000pe04vcqg5zla', - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - update: {}, - }); + for (let i = 1; i <= 4; i++) { + const documentData = await createDocumentData({ documentData: examplePdf }); - await prisma.document.create({ - data: { - source: DocumentSource.DOCUMENT, - title: 'Example Document', - documentDataId: examplePdfData.id, - userId: exampleUser.id, - recipients: { - create: { - name: String(adminUser.name), - email: adminUser.email, - token: Math.random().toString(36).slice(2, 9), + await prisma.document.create({ + data: { + source: DocumentSource.DOCUMENT, + title: `Example Document ${i}`, + documentDataId: documentData.id, + userId: exampleUser.id, + recipients: { + create: { + name: String(adminUser.name), + email: adminUser.email, + token: Math.random().toString(36).slice(2, 9), + }, }, }, + }); + } + + for (let i = 1; i <= 4; i++) { + const documentData = await createDocumentData({ documentData: examplePdf }); + + await prisma.document.create({ + data: { + source: DocumentSource.DOCUMENT, + title: `Document ${i}`, + documentDataId: documentData.id, + userId: adminUser.id, + recipients: { + create: { + name: String(exampleUser.name), + email: exampleUser.email, + token: Math.random().toString(36).slice(2, 9), + }, + }, + }, + }); + } + + await seedPendingDocument(exampleUser, [adminUser], { + key: 'example-pending', + createDocumentOptions: { + title: 'Pending Document', }, }); + await seedPendingDocument(adminUser, [exampleUser], { + key: 'admin-pending', + createDocumentOptions: { + title: 'Pending Document', + }, + }); + + await Promise.all([ + seedTemplate({ + title: 'Template 1', + userId: exampleUser.id, + }), + seedDirectTemplate({ + title: 'Direct Template 1', + userId: exampleUser.id, + }), + + seedTemplate({ + title: 'Template 1', + userId: adminUser.id, + }), + seedDirectTemplate({ + title: 'Direct Template 1', + userId: adminUser.id, + }), + ]); + const testUsers = [ 'test@documenso.com', 'test2@documenso.com', From 3c4863f285d10de16596bc42055912393040e0bb Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Mon, 17 Feb 2025 04:42:37 +0000 Subject: [PATCH 02/21] chore: add asssitant role to the docs (#1638) --- apps/documentation/pages/developers/_meta.json | 2 +- .../pages/users/signing-documents/index.mdx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/documentation/pages/developers/_meta.json b/apps/documentation/pages/developers/_meta.json index a9f3c3823..0057496b3 100644 --- a/apps/documentation/pages/developers/_meta.json +++ b/apps/documentation/pages/developers/_meta.json @@ -14,4 +14,4 @@ "public-api": "Public API", "embedding": "Embedding", "webhooks": "Webhooks" -} \ No newline at end of file +} diff --git a/apps/documentation/pages/users/signing-documents/index.mdx b/apps/documentation/pages/users/signing-documents/index.mdx index a0a32399d..f37afc8f7 100644 --- a/apps/documentation/pages/users/signing-documents/index.mdx +++ b/apps/documentation/pages/users/signing-documents/index.mdx @@ -85,12 +85,13 @@ You can also set the recipient's role, which determines their actions and permis Documenso has 4 roles for recipients with different permissions and actions. -| Role | Function | Action required | Signature | -| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: | -| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes | -| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional | -| Viewer | Needs to confirm they viewed the document. | Yes | No | -| BCC | Receives a copy of the signed document after completion. No action is required. | No | No | +| Role | Function | Action required | Signature | +| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: | +| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes | +| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional | +| Viewer | Needs to confirm they viewed the document. | Yes | No | +| Assistant | Can help prepare the document by filling in fields on behalf of other signers. | Yes | No | +| CC | Receives a copy of the signed document after completion. No action is required. | No | No | ### Fields From 11528090a53f287276119ba7576ee6e40b3c998a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 18 Feb 2025 15:17:47 +1100 Subject: [PATCH 03/21] fix: prepare auth migration (#1648) Add schema session migration in preparation for auth migration. --- .../migration.sql | 18 ++++++++++++++++++ packages/prisma/schema.prisma | 17 ++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 packages/prisma/migrations/20250217120859_add_session_fields/migration.sql diff --git a/packages/prisma/migrations/20250217120859_add_session_fields/migration.sql b/packages/prisma/migrations/20250217120859_add_session_fields/migration.sql new file mode 100644 index 000000000..5af838335 --- /dev/null +++ b/packages/prisma/migrations/20250217120859_add_session_fields/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost. + - Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "password" TEXT; + +-- AlterTable +ALTER TABLE "Session" DROP COLUMN "expires", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL, +ADD COLUMN "ipAddress" TEXT, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL, +ADD COLUMN "userAgent" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 0cdb1521e..e612921dc 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -270,18 +270,25 @@ model Account { scope String? id_token String? @db.Text session_state String? + password String? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { - id String @id @default(cuid()) - sessionToken String @unique + id String @id @default(cuid()) + sessionToken String @unique userId Int - expires DateTime - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + + ipAddress String? + userAgent String? + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) } enum DocumentStatus { From 11bc93a9a443860bc2b6e5ebc433fac87ab4113c Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 20 Feb 2025 11:34:19 +1100 Subject: [PATCH 04/21] feat: allow document rejection in embeds (#1662) Bing bang --- .../sign/[token]/reject-document-dialog.tsx | 9 +++- apps/web/src/app/embed/rejected.tsx | 40 +++++++++++++++ .../src/app/embed/sign/[[...url]]/client.tsx | 51 ++++++++++++++++++- .../src/app/embed/sign/[[...url]]/schema.ts | 1 + .../document/complete-document-with-token.ts | 8 +++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/embed/rejected.tsx diff --git a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx index 547a346d8..110fa8f99 100644 --- a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx @@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer; export interface RejectDocumentDialogProps { document: Pick; token: string; + onRejected?: (reason: string) => void | Promise; } -export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) { +export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) { const { toast } = useToast(); const router = useRouter(); const searchParams = useSearchParams(); @@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr setIsOpen(false); - router.push(`/sign/${token}/rejected`); + if (onRejected) { + await onRejected(reason); + } else { + router.push(`/sign/${token}/rejected`); + } } catch (err) { toast({ title: 'Error', diff --git a/apps/web/src/app/embed/rejected.tsx b/apps/web/src/app/embed/rejected.tsx new file mode 100644 index 000000000..deb8bd026 --- /dev/null +++ b/apps/web/src/app/embed/rejected.tsx @@ -0,0 +1,40 @@ +import { Trans } from '@lingui/macro'; +import { XCircle } from 'lucide-react'; + +import type { Signature } from '@documenso/prisma/client'; + +export type EmbedDocumentRejectedPageProps = { + name?: string; + signature?: Signature; +}; + +export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => { + return ( +
+
+
+ + +

+ Document Rejected +

+
+ +
+ You have rejected this document +
+ +

+ + The document owner has been notified of your decision. They may contact you with further + instructions if necessary. + +

+ +

+ No further action is required from you at this time. +

+
+
+ ); +}; diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index e8ee6ad52..f7635ddab 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -10,7 +10,13 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn' import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client'; -import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client'; +import { + type DocumentData, + type Field, + FieldType, + RecipientRole, + SigningStatus, +} from '@documenso/prisma/client'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; @@ -26,11 +32,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context'; +import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog'; import { Logo } from '~/components/branding/logo'; import { EmbedClientLoading } from '../../client-loading'; import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentFields } from '../../document-fields'; +import { EmbedDocumentRejected } from '../../rejected'; import { injectCss } from '../../util'; import { ZSignDocumentEmbedDataSchema } from './schema'; @@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({ const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted); + const [hasRejectedDocument, setHasRejectedDocument] = useState( + recipient.signingStatus === SigningStatus.REJECTED, + ); const [selectedSignerId, setSelectedSignerId] = useState( allRecipients.length > 0 ? allRecipients[0].id : null, ); @@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({ const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); + const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); + const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; @@ -161,6 +174,25 @@ export const EmbedSignDocumentClientPage = ({ } }; + const onDocumentRejected = (reason: string) => { + if (window.parent) { + window.parent.postMessage( + { + action: 'document-rejected', + data: { + token, + documentId, + recipientId: recipient.id, + reason, + }, + }, + '*', + ); + } + + setHasRejectedDocument(true); + }; + useLayoutEffect(() => { const hash = window.location.hash.slice(1); @@ -174,6 +206,7 @@ export const EmbedSignDocumentClientPage = ({ // Since a recipient can be provided a name we can lock it without requiring // a to be provided by the parent application, unlike direct templates. setIsNameLocked(!!data.lockName); + setAllowDocumentRejection(!!data.allowDocumentRejection); if (data.darkModeDisabled) { document.documentElement.classList.add('dark-mode-disabled'); @@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); + if (hasRejectedDocument) { + return ; + } + if (hasCompletedDocument) { return ( {(!hasFinishedInit || !hasDocumentLoaded) && } + {allowDocumentRejection && ( +
+ +
+ )} +
{/* Viewer */}
@@ -420,7 +467,7 @@ export const EmbedSignDocumentClientPage = ({ ) : ( + {isExpanded ? ( + + ) : pendingFields.length > 0 ? ( + + ) : ( + + )}
diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index ea024bde0..34289187d 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useId, useLayoutEffect, useState } from 'react'; +import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react'; import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; @@ -91,11 +91,14 @@ export const EmbedSignDocumentClientPage = ({ ); const [isExpanded, setIsExpanded] = useState(false); + const [hasAutoExpanded, setHasAutoExpanded] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); + const documentEndRef = useRef(null); + const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; @@ -241,6 +244,42 @@ export const EmbedSignDocumentClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); + // Set up intersection observer to auto-expand widget when user reaches bottom of document + useEffect(() => { + if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) { + return; + } + + // Add a delay to ensure document has fully rendered and stabilized + const timeoutId = setTimeout(() => { + const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length; + + // Only set up the observer if there's more than one page + if (pageCount <= 1) return; + + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + + if (entry.isIntersecting) { + setIsExpanded(true); + setHasAutoExpanded(true); + observer.disconnect(); + } + }, + { threshold: 1.0 }, + ); + + if (documentEndRef.current) { + observer.observe(documentEndRef.current); + } + }, 1500); // 1.5 second delay + + return () => { + clearTimeout(timeoutId); + }; + }, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]); + if (hasRejectedDocument) { return ; } @@ -283,6 +322,8 @@ export const EmbedSignDocumentClientPage = ({ documentData={documentData} onDocumentLoad={() => setHasDocumentLoaded(true)} /> + {/* Observer target at the bottom of the document */} +
{/* Widget */} @@ -303,19 +344,36 @@ export const EmbedSignDocumentClientPage = ({ )} - + {isExpanded ? ( + + ) : pendingFields.length > 0 ? ( + + ) : ( + + )} From 1560218d4a4efc693005a6200799e9b114c6c09f Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 27 Feb 2025 11:44:19 +1100 Subject: [PATCH 16/21] v1.9.1-rc.5 --- apps/web/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index c4984f0f5..ad6f7e51f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/web", - "version": "1.9.1-rc.4", + "version": "1.9.1-rc.5", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/package-lock.json b/package-lock.json index 791af3504..bfe60c3c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.9.1-rc.4", + "version": "1.9.1-rc.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.9.1-rc.4", + "version": "1.9.1-rc.5", "workspaces": [ "apps/*", "packages/*" @@ -106,7 +106,7 @@ }, "apps/web": { "name": "@documenso/web", - "version": "1.9.1-rc.4", + "version": "1.9.1-rc.5", "license": "AGPL-3.0", "dependencies": { "@documenso/api": "*", diff --git a/package.json b/package.json index 3f7ec551e..ee3a0b3a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.9.1-rc.4", + "version": "1.9.1-rc.5", "scripts": { "build": "turbo run build", "build:web": "turbo run build --filter=@documenso/web", From 255c33cdab17f9049cea8655bb4c58f19ef627cc Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 28 Feb 2025 09:04:25 +1100 Subject: [PATCH 17/21] fix: stripe price fetch (#1677) Currently Stripe prices search is omitting a price for an unknown reason. Changed our fetch logic to use `list` instead of `search` allows us to work around the issue. It's unknown on the performance impact of using `list` vs `search` --- packages/ee/server-only/stripe/get-prices-by-plan.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/ee/server-only/stripe/get-prices-by-plan.ts b/packages/ee/server-only/stripe/get-prices-by-plan.ts index 45906d54a..52c9d1e5e 100644 --- a/packages/ee/server-only/stripe/get-prices-by-plan.ts +++ b/packages/ee/server-only/stripe/get-prices-by-plan.ts @@ -4,15 +4,14 @@ import { stripe } from '@documenso/lib/server-only/stripe'; type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE]; export const getPricesByPlan = async (plan: PlanType | PlanType[]) => { - const planTypes = typeof plan === 'string' ? [plan] : plan; + const planTypes: string[] = typeof plan === 'string' ? [plan] : plan; - const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR '); - - const { data: prices } = await stripe.prices.search({ - query, + const prices = await stripe.prices.list({ expand: ['data.product'], limit: 100, }); - return prices.filter((price) => price.type === 'recurring'); + return prices.data.filter( + (price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan), + ); }; From 617e3a46e066759bd574a6f05136aa8731a4b301 Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 28 Feb 2025 09:10:16 +1100 Subject: [PATCH 18/21] v1.9.1-rc.6 --- apps/web/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index ad6f7e51f..1ab42dcf7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/web", - "version": "1.9.1-rc.5", + "version": "1.9.1-rc.6", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/package-lock.json b/package-lock.json index bfe60c3c5..03bd8da4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.9.1-rc.5", + "version": "1.9.1-rc.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.9.1-rc.5", + "version": "1.9.1-rc.6", "workspaces": [ "apps/*", "packages/*" @@ -106,7 +106,7 @@ }, "apps/web": { "name": "@documenso/web", - "version": "1.9.1-rc.5", + "version": "1.9.1-rc.6", "license": "AGPL-3.0", "dependencies": { "@documenso/api": "*", diff --git a/package.json b/package.json index ee3a0b3a6..04f8956bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.9.1-rc.5", + "version": "1.9.1-rc.6", "scripts": { "build": "turbo run build", "build:web": "turbo run build --filter=@documenso/web", From ca3d65ad101416b9ad67acf6e008209901c8cfbb Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 28 Feb 2025 10:11:08 +1100 Subject: [PATCH 19/21] fix: remove auto-expand in embeddding --- .../app/embed/direct/[[...url]]/client.tsx | 46 +------------------ .../src/app/embed/direct/[[...url]]/page.tsx | 4 +- .../src/app/embed/sign/[[...url]]/client.tsx | 45 +----------------- .../src/app/embed/sign/[[...url]]/page.tsx | 4 +- 4 files changed, 10 insertions(+), 89 deletions(-) diff --git a/apps/web/src/app/embed/direct/[[...url]]/client.tsx b/apps/web/src/app/embed/direct/[[...url]]/client.tsx index 7788ab012..6b6dbc4c7 100644 --- a/apps/web/src/app/embed/direct/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/direct/[[...url]]/client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; @@ -83,15 +83,12 @@ export const EmbedDirectTemplateClientPage = ({ const [hasCompletedDocument, setHasCompletedDocument] = useState(false); const [isExpanded, setIsExpanded] = useState(false); - const [hasAutoExpanded, setHasAutoExpanded] = useState(false); const [isEmailLocked, setIsEmailLocked] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); - const documentEndRef = useRef(null); - const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [localFields, setLocalFields] = useState(() => fields); @@ -320,43 +317,6 @@ export const EmbedDirectTemplateClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); - // Set up intersection observer to auto-expand widget when user reaches bottom of document - useEffect(() => { - if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) { - return; - } - - // Add a delay to ensure document has fully rendered and stabilized - const timeoutId = setTimeout(() => { - // Get the number of pages in the document - const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length; - - // Only set up the observer if there's more than one page - if (pageCount <= 1) return; - - const observer = new IntersectionObserver( - (entries) => { - const [entry] = entries; - - if (entry.isIntersecting) { - setIsExpanded(true); - setHasAutoExpanded(true); - observer.disconnect(); - } - }, - { threshold: 1.0 }, - ); - - if (documentEndRef.current) { - observer.observe(documentEndRef.current); - } - }, 1500); // 1.5 second delay - - return () => { - clearTimeout(timeoutId); - }; - }, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]); - if (hasCompletedDocument) { return ( setHasDocumentLoaded(true)} /> - {/* Observer target at the bottom of the document */} -
{/* Widget */}
diff --git a/apps/web/src/app/embed/direct/[[...url]]/page.tsx b/apps/web/src/app/embed/direct/[[...url]]/page.tsx index 85ef40c78..97f7bb953 100644 --- a/apps/web/src/app/embed/direct/[[...url]]/page.tsx +++ b/apps/web/src/app/embed/direct/[[...url]]/page.tsx @@ -110,7 +110,9 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem recipient={recipient} fields={fields} metadata={template.templateMeta} - hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy} + hidePoweredBy={ + isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy + } allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument} /> diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index 34289187d..4650f60ab 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react'; +import { useEffect, useId, useLayoutEffect, useState } from 'react'; import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; @@ -91,14 +91,11 @@ export const EmbedSignDocumentClientPage = ({ ); const [isExpanded, setIsExpanded] = useState(false); - const [hasAutoExpanded, setHasAutoExpanded] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); - const documentEndRef = useRef(null); - const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; @@ -244,42 +241,6 @@ export const EmbedSignDocumentClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); - // Set up intersection observer to auto-expand widget when user reaches bottom of document - useEffect(() => { - if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) { - return; - } - - // Add a delay to ensure document has fully rendered and stabilized - const timeoutId = setTimeout(() => { - const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length; - - // Only set up the observer if there's more than one page - if (pageCount <= 1) return; - - const observer = new IntersectionObserver( - (entries) => { - const [entry] = entries; - - if (entry.isIntersecting) { - setIsExpanded(true); - setHasAutoExpanded(true); - observer.disconnect(); - } - }, - { threshold: 1.0 }, - ); - - if (documentEndRef.current) { - observer.observe(documentEndRef.current); - } - }, 1500); // 1.5 second delay - - return () => { - clearTimeout(timeoutId); - }; - }, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]); - if (hasRejectedDocument) { return ; } @@ -322,14 +283,12 @@ export const EmbedSignDocumentClientPage = ({ documentData={documentData} onDocumentLoad={() => setHasDocumentLoaded(true)} /> - {/* Observer target at the bottom of the document */} -
{/* Widget */}
diff --git a/apps/web/src/app/embed/sign/[[...url]]/page.tsx b/apps/web/src/app/embed/sign/[[...url]]/page.tsx index 8b7223cb1..b6c36f113 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/page.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/page.tsx @@ -131,7 +131,9 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen fields={fields} metadata={document.documentMeta} isCompleted={document.status === DocumentStatus.COMPLETED} - hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy} + hidePoweredBy={ + isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy + } allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument} allRecipients={allRecipients} /> From 235d846d2b5fc6d85df44a8aaf46ad24e6460676 Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 28 Feb 2025 10:11:36 +1100 Subject: [PATCH 20/21] v1.9.1-rc.7 --- apps/web/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 1ab42dcf7..34cc78689 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@documenso/web", - "version": "1.9.1-rc.6", + "version": "1.9.1-rc.7", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/package-lock.json b/package-lock.json index 03bd8da4a..61fbbc9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.9.1-rc.6", + "version": "1.9.1-rc.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.9.1-rc.6", + "version": "1.9.1-rc.7", "workspaces": [ "apps/*", "packages/*" @@ -106,7 +106,7 @@ }, "apps/web": { "name": "@documenso/web", - "version": "1.9.1-rc.6", + "version": "1.9.1-rc.7", "license": "AGPL-3.0", "dependencies": { "@documenso/api": "*", diff --git a/package.json b/package.json index 04f8956bc..d054f4641 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.9.1-rc.6", + "version": "1.9.1-rc.7", "scripts": { "build": "turbo run build", "build:web": "turbo run build --filter=@documenso/web", From 1789eff564fedb5ea9ee9c12a354c55c495031fd Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 28 Feb 2025 12:09:38 +0200 Subject: [PATCH 21/21] chore: add label for checkbox and radio fields (#1607) --- .../sign/[token]/signing-field-container.tsx | 17 ++++++++ .../primitives/document-flow/field-item.tsx | 40 +++++++++++++++++++ .../checkbox-field.tsx | 12 ++++++ .../radio-field.tsx | 17 +++++++- 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx index a2cdfe9c7..33c7745c0 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx @@ -182,6 +182,23 @@ export const SigningFieldContainer = ({ )} + {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && + field.fieldMeta?.label && ( +
+ {field.fieldMeta.label} +
+ )} + {children}
diff --git a/packages/ui/primitives/document-flow/field-item.tsx b/packages/ui/primitives/document-flow/field-item.tsx index 6515816be..896c7a9d0 100644 --- a/packages/ui/primitives/document-flow/field-item.tsx +++ b/packages/ui/primitives/document-flow/field-item.tsx @@ -12,6 +12,7 @@ import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta'; +import { FieldType } from '@documenso/prisma/client'; import { useSignerColors } from '../../lib/signer-colors'; import { cn } from '../../lib/utils'; @@ -185,11 +186,35 @@ export const FieldItem = ({ () => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta), [field.fieldMeta], ); + const radioHasValues = useMemo( () => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta), [field.fieldMeta], ); + const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => { + if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) { + return false; + } + + if (type === FieldType.RADIO) { + const parsed = ZRadioFieldMeta.parse(fieldMeta); + return parsed.values?.some((value) => value.checked) ?? false; + } + + if (type === FieldType.CHECKBOX) { + const parsed = ZCheckboxFieldMeta.parse(fieldMeta); + return parsed.values?.some((value) => value.checked) ?? false; + } + + return false; + }; + + const fieldHasCheckedValues = useMemo( + () => hasCheckedValues(field.fieldMeta, field.type), + [field.fieldMeta, field.type], + ); + const fixedSize = checkBoxHasValues || radioHasValues; return createPortal( @@ -229,6 +254,21 @@ export const FieldItem = ({ onMove?.(d.node); }} > + {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && + field.fieldMeta?.label && ( +
+ {field.fieldMeta.label} +
+ )} +
+
+ + handleFieldChange('label', e.target.value)} + /> +